Archived
190 lines
5.2 KiB
JavaScript
190 lines
5.2 KiB
JavaScript
#!/usr/bin/env node
|
|
import { watch } from "node:fs";
|
|
import { copyFile, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { dirname, relative, resolve } from "node:path";
|
|
import ts from "typescript";
|
|
|
|
const rootDir = resolve("pi-web-plugins");
|
|
const outDir = resolve("dist/pi-web-plugins");
|
|
const watchMode = process.argv.includes("--watch");
|
|
const cwd = process.cwd();
|
|
|
|
if (watchMode) {
|
|
await watchAndBuild();
|
|
} else {
|
|
await buildAll();
|
|
}
|
|
|
|
async function buildAll() {
|
|
await rm(outDir, { recursive: true, force: true });
|
|
const result = await buildDirectory(rootDir, outDir);
|
|
const suffix = result.transpiled === 1 ? "file" : "files";
|
|
console.log(`[plugins] built ${String(result.transpiled)} TypeScript plugin ${suffix} into ${relative(cwd, outDir)}`);
|
|
}
|
|
|
|
async function buildDirectory(sourceDir, targetDir) {
|
|
const entries = await readDirectory(sourceDir);
|
|
let copied = 0;
|
|
let transpiled = 0;
|
|
|
|
for (const entry of entries) {
|
|
const sourcePath = resolve(sourceDir, entry.name);
|
|
const targetPath = resolve(targetDir, entry.name);
|
|
|
|
if (entry.isDirectory()) {
|
|
if (entry.name === "node_modules") continue;
|
|
const result = await buildDirectory(sourcePath, targetPath);
|
|
copied += result.copied;
|
|
transpiled += result.transpiled;
|
|
continue;
|
|
}
|
|
|
|
if (!entry.isFile()) continue;
|
|
if (entry.name.endsWith(".d.ts")) continue;
|
|
|
|
if (isPluginSource(entry.name)) {
|
|
await buildFile(sourcePath, targetPath.replace(/\.ts$/u, ".js"));
|
|
transpiled += 1;
|
|
continue;
|
|
}
|
|
|
|
if (entry.name.endsWith(".js") && await hasTypeScriptSource(sourcePath)) continue;
|
|
await mkdir(dirname(targetPath), { recursive: true });
|
|
await copyFile(sourcePath, targetPath);
|
|
copied += 1;
|
|
}
|
|
|
|
return { copied, transpiled };
|
|
}
|
|
|
|
async function buildFile(file, outputPath) {
|
|
const source = await readFile(file, "utf8");
|
|
const transpiled = ts.transpileModule(source, {
|
|
fileName: file,
|
|
reportDiagnostics: true,
|
|
compilerOptions: {
|
|
target: ts.ScriptTarget.ES2022,
|
|
module: ts.ModuleKind.ESNext,
|
|
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
verbatimModuleSyntax: true,
|
|
sourceMap: false,
|
|
inlineSourceMap: false,
|
|
},
|
|
});
|
|
|
|
const errors = (transpiled.diagnostics ?? []).filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error);
|
|
if (errors.length > 0) throw new Error(formatDiagnostics(errors));
|
|
|
|
const output = `// Generated from ${relative(cwd, file)}. Do not edit directly.\n${transpiled.outputText}`;
|
|
await mkdir(dirname(outputPath), { recursive: true });
|
|
await writeFile(outputPath, output);
|
|
}
|
|
|
|
async function findPluginDirs(dir) {
|
|
const entries = await readDirectory(dir);
|
|
const dirs = [dir];
|
|
for (const entry of entries) {
|
|
if (!entry.isDirectory() || entry.name === "node_modules") continue;
|
|
dirs.push(...await findPluginDirs(resolve(dir, entry.name)));
|
|
}
|
|
return dirs.sort((left, right) => left.localeCompare(right));
|
|
}
|
|
|
|
function isPluginSource(fileName) {
|
|
return fileName.endsWith(".ts") && !fileName.endsWith(".d.ts");
|
|
}
|
|
|
|
async function hasTypeScriptSource(javaScriptPath) {
|
|
const typeScriptPath = javaScriptPath.replace(/\.js$/u, ".ts");
|
|
try {
|
|
await readFile(typeScriptPath, "utf8");
|
|
return true;
|
|
} catch (error) {
|
|
if (isNodeError(error) && error.code === "ENOENT") return false;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function readDirectory(dir) {
|
|
try {
|
|
return await readdir(dir, { withFileTypes: true });
|
|
} catch (error) {
|
|
if (isNodeError(error) && error.code === "ENOENT") return [];
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function watchAndBuild() {
|
|
let watchers = [];
|
|
let timer;
|
|
let building = false;
|
|
let pending = false;
|
|
|
|
const closeWatchers = () => {
|
|
for (const watcher of watchers) watcher.close();
|
|
watchers = [];
|
|
};
|
|
|
|
const refreshWatchers = async () => {
|
|
closeWatchers();
|
|
const dirs = await findPluginDirs(rootDir);
|
|
watchers = dirs.map((dir) => watch(dir, () => scheduleBuild()));
|
|
};
|
|
|
|
const runBuild = async () => {
|
|
if (building) {
|
|
pending = true;
|
|
return;
|
|
}
|
|
building = true;
|
|
try {
|
|
do {
|
|
pending = false;
|
|
await refreshWatchers();
|
|
await buildAll();
|
|
} while (pending);
|
|
} catch (error) {
|
|
console.error(`[plugins] ${formatUnknownError(error)}`);
|
|
} finally {
|
|
building = false;
|
|
}
|
|
};
|
|
|
|
const scheduleBuild = () => {
|
|
if (timer !== undefined) clearTimeout(timer);
|
|
timer = setTimeout(() => {
|
|
timer = undefined;
|
|
void runBuild();
|
|
}, 100);
|
|
};
|
|
|
|
const stop = () => {
|
|
if (timer !== undefined) clearTimeout(timer);
|
|
closeWatchers();
|
|
process.exit(0);
|
|
};
|
|
|
|
process.on("SIGINT", stop);
|
|
process.on("SIGTERM", stop);
|
|
|
|
await runBuild();
|
|
console.log(`[plugins] watching ${relative(cwd, rootDir)}`);
|
|
await new Promise(() => undefined);
|
|
}
|
|
|
|
function formatDiagnostics(diagnostics) {
|
|
return ts.formatDiagnosticsWithColorAndContext(diagnostics, {
|
|
getCanonicalFileName: (fileName) => fileName,
|
|
getCurrentDirectory: () => cwd,
|
|
getNewLine: () => "\n",
|
|
});
|
|
}
|
|
|
|
function formatUnknownError(error) {
|
|
return error instanceof Error ? error.message : String(error);
|
|
}
|
|
|
|
function isNodeError(error) {
|
|
return error instanceof Error && "code" in error;
|
|
}
|