Two guards protect the core promise: a route with no islands emits no assets at all, and a page with several islands keeps React in one shared chunk. buildIslands now writes a generated entry per island instead of passing component sources directly. Two islands sharing a source deduped to a single entrypoint, and output order is not guaranteed to match input order, so island names could bind to the wrong bundle. Island modules are excluded from the editor compiler bundle: it globs packages/compiler/src, and island-bundle.ts calls Bun.build while island-codegen.ts imports @wrnexus/core — neither belongs in a Node-only VS Code artifact. Integration assertions share one build. bun test interferes with Bun.build's module reads after several build calls in one process, while the same calls succeed repeatedly outside the runner; production is unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
201 lines
7.1 KiB
JavaScript
201 lines
7.1 KiB
JavaScript
import { createHash } from "node:crypto";
|
|
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
import process from "node:process";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const root = resolve(here, "..");
|
|
const scriptPath = fileURLToPath(import.meta.url);
|
|
|
|
function normalizeText(value) {
|
|
return value.replace(/\r\n?/g, "\n");
|
|
}
|
|
|
|
function sha256(value) {
|
|
return createHash("sha256").update(value).digest("hex");
|
|
}
|
|
|
|
function loadTypeScript() {
|
|
const candidates = [process.env.TYPESCRIPT_PATH, "typescript"].filter(Boolean);
|
|
for (const candidate of candidates) {
|
|
try {
|
|
return require(candidate);
|
|
} catch {
|
|
// Try the next local or explicitly supplied compiler path.
|
|
}
|
|
}
|
|
throw new Error(
|
|
"TypeScript is required to build the editor compiler. Run `bun install` or set TYPESCRIPT_PATH.",
|
|
);
|
|
}
|
|
|
|
const ts = loadTypeScript();
|
|
|
|
// React island modules are not used by the editor: island-bundle.ts calls
|
|
// Bun.build and island-codegen.ts imports @wrnexus/core, neither of which
|
|
// exists in this Node-only bundle. They are unreachable from the editor entry,
|
|
// so excluding them keeps Bun-only code out of the extension entirely.
|
|
const EDITOR_EXCLUDED = ["island-bundle.ts", "island-codegen.ts"];
|
|
function isEditorExcluded(path) {
|
|
return EDITOR_EXCLUDED.some((name) => path.endsWith(name));
|
|
}
|
|
|
|
function walk(dir) {
|
|
const files = [];
|
|
for (const entry of readdirSync(dir)) {
|
|
const path = join(dir, entry);
|
|
const stat = statSync(path);
|
|
if (stat.isDirectory()) files.push(...walk(path));
|
|
else if (
|
|
stat.isFile() &&
|
|
path.endsWith(".ts") &&
|
|
!path.endsWith(".test.ts") &&
|
|
!isEditorExcluded(path)
|
|
)
|
|
files.push(path);
|
|
}
|
|
return files;
|
|
}
|
|
|
|
function moduleId(file) {
|
|
return relative(root, file).split(sep).join("/");
|
|
}
|
|
|
|
const sourceFiles = [
|
|
...walk(join(root, "packages", "syntax", "src")),
|
|
...walk(join(root, "packages", "compiler", "src")),
|
|
].sort();
|
|
|
|
const sourceHashBuilder = createHash("sha256");
|
|
const modules = [];
|
|
for (const file of sourceFiles) {
|
|
const id = moduleId(file);
|
|
const source = normalizeText(readFileSync(file, "utf8"));
|
|
sourceHashBuilder.update(id);
|
|
sourceHashBuilder.update("\0");
|
|
sourceHashBuilder.update(source);
|
|
sourceHashBuilder.update("\0");
|
|
const result = ts.transpileModule(source, {
|
|
fileName: file,
|
|
reportDiagnostics: true,
|
|
compilerOptions: {
|
|
target: ts.ScriptTarget.ES2022,
|
|
module: ts.ModuleKind.CommonJS,
|
|
esModuleInterop: true,
|
|
skipLibCheck: true,
|
|
sourceMap: false,
|
|
inlineSourceMap: false,
|
|
removeComments: false,
|
|
rewriteRelativeImportExtensions: true,
|
|
newLine: ts.NewLineKind.LineFeed,
|
|
},
|
|
});
|
|
const diagnostics = result.diagnostics ?? [];
|
|
if (diagnostics.some((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error)) {
|
|
const message = ts.formatDiagnosticsWithColorAndContext(diagnostics, {
|
|
getCanonicalFileName: (name) => name,
|
|
getCurrentDirectory: () => root,
|
|
getNewLine: () => "\n",
|
|
});
|
|
throw new Error(message);
|
|
}
|
|
modules.push(
|
|
`${JSON.stringify(id)}: function (module, exports, require, __filename, __dirname) {\n${normalizeText(result.outputText)}\n}`,
|
|
);
|
|
}
|
|
|
|
const sourceHash = sourceHashBuilder.digest("hex");
|
|
const generatorHash = sha256(normalizeText(readFileSync(scriptPath, "utf8")));
|
|
|
|
const output = `"use strict";
|
|
// Generated by scripts/build-editor-compiler.mjs. Do not edit directly.
|
|
// WRN editor compiler source hash: ${sourceHash}
|
|
// WRN editor compiler generator hash: ${generatorHash}
|
|
// Generated with TypeScript: ${ts.version}
|
|
const __nodeRequire = require;
|
|
const __path = __nodeRequire("node:path");
|
|
const __modules = {
|
|
${modules.join(",\n")}
|
|
};
|
|
const __aliases = {
|
|
"@wrnexus/syntax": "packages/syntax/src/index.ts",
|
|
"@wrnexus/syntax/parser": "packages/syntax/src/parser.ts",
|
|
"@wrnexus/syntax/tokenizer": "packages/syntax/src/tokenizer.ts",
|
|
"@wrnexus/syntax/types": "packages/syntax/src/types.ts",
|
|
"@wrnexus/syntax/diagnostics": "packages/syntax/src/diagnostics.ts",
|
|
"@wrnexus/syntax/spec": "packages/syntax/src/spec.ts"
|
|
};
|
|
const __cache = Object.create(null);
|
|
function __normalize(id) {
|
|
const normalized = id.split("\\\\").join("/");
|
|
return normalized.startsWith("./") ? normalized.slice(2) : normalized;
|
|
}
|
|
function __resolve(request, parent) {
|
|
if (__aliases[request]) return __aliases[request];
|
|
if (!request.startsWith(".")) return null;
|
|
const base = __normalize(__path.posix.join(__path.posix.dirname(parent), request));
|
|
const candidates = [
|
|
base,
|
|
base.endsWith(".js") ? base.slice(0, -3) + ".ts" : base,
|
|
base.endsWith(".ts") ? base : base + ".ts",
|
|
(base.endsWith("/") ? base.slice(0, -1) : base) + "/index.ts"
|
|
];
|
|
for (const candidate of candidates) {
|
|
if (__modules[candidate]) return candidate;
|
|
}
|
|
return null;
|
|
}
|
|
function __load(id) {
|
|
if (__cache[id]) return __cache[id].exports;
|
|
const factory = __modules[id];
|
|
if (!factory) throw new Error("WRN editor compiler module not found: " + id);
|
|
const module = { exports: {} };
|
|
__cache[id] = module;
|
|
const localRequire = (request) => {
|
|
const resolved = __resolve(request, id);
|
|
return resolved ? __load(resolved) : __nodeRequire(request);
|
|
};
|
|
factory(module, module.exports, localRequire, id, __path.posix.dirname(id));
|
|
return module.exports;
|
|
}
|
|
module.exports = __load("packages/compiler/src/index.ts");
|
|
`;
|
|
|
|
const destination = join(root, "editors", "vscode", "src", "compiler.cjs");
|
|
if (process.argv.includes("--check")) {
|
|
const existing = existsSync(destination) ? normalizeText(readFileSync(destination, "utf8")) : "";
|
|
const embeddedSourceHash = existing.match(
|
|
/^\/\/ WRN editor compiler source hash: ([a-f0-9]{64})$/m,
|
|
)?.[1];
|
|
const embeddedGeneratorHash = existing.match(
|
|
/^\/\/ WRN editor compiler generator hash: ([a-f0-9]{64})$/m,
|
|
)?.[1];
|
|
const embeddedTypeScriptVersion = existing.match(/^\/\/ Generated with TypeScript: (.+)$/m)?.[1];
|
|
|
|
const hashesMatch = embeddedSourceHash === sourceHash && embeddedGeneratorHash === generatorHash;
|
|
const exactMatch = existing === normalizeText(output);
|
|
const compatibleCompilerDifference =
|
|
hashesMatch && Boolean(embeddedTypeScriptVersion) && embeddedTypeScriptVersion !== ts.version;
|
|
|
|
if (!hashesMatch || (!exactMatch && !compatibleCompilerDifference)) {
|
|
throw new Error(
|
|
`${relative(root, destination)} is stale. Run bun run --cwd editors/vscode build and commit the result.`,
|
|
);
|
|
}
|
|
|
|
const versionDetail = exactMatch
|
|
? `TypeScript ${ts.version}`
|
|
: `source-compatible output generated with TypeScript ${embeddedTypeScriptVersion}; current TypeScript is ${ts.version}`;
|
|
process.stdout.write(
|
|
`Verified ${relative(root, destination)} matches ${sourceFiles.length} TypeScript modules (${versionDetail}).\n`,
|
|
);
|
|
} else {
|
|
writeFileSync(destination, output, "utf8");
|
|
process.stdout.write(
|
|
`Built ${relative(root, destination)} from ${sourceFiles.length} TypeScript modules with TypeScript ${ts.version}.\n`,
|
|
);
|
|
}
|