This repository has been archived on 2026-08-23. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
pi-web/scripts/build-plugins.mjs

194 lines
5.3 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") || isTestSource(entry.name)) 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");
}
function isTestSource(fileName) {
return /\.(?:test|spec)\.ts$/u.test(fileName);
}
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;
}