Archived
feat: build bundled plugins from TypeScript
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
#!/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;
|
||||
}
|
||||
Reference in New Issue
Block a user