fix(build): scope standalone type:module to worker dirs so server.js stays CJS (#10936)

Validado no worktree combinado: typecheck:core, changelog-integrity, complexity, cognitive-complexity, file-size, lint e teste focado (colocate-standalone-esm-scope) todos verdes. Fix real, correção de regressão introduzida por #10836 (server.js CommonJS quebrando com type:module reintroduzido). CI vermelho é o base-red já rastreado em #9985. Obrigado!
This commit is contained in:
Armin Anton” ∴
2026-08-21 00:23:42 -07:00
committed by GitHub
parent 4c0b54abc1
commit 10deb5c307
3 changed files with 270 additions and 93 deletions

View File

@@ -16,14 +16,20 @@
*
* Run manually after a build, or automatically via the `postbuild` npm hook.
*/
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { cpSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { execFileSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { fileURLToPath, pathToFileURL } from "node:url";
import { computeDependencyClosure } from "./colocateOptionals.mjs";
const ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
const STANDALONE = join(ROOT, ".build", "next", "standalone");
// STANDALONE defaults to the real build output; OMNIROUTE_STANDALONE_DIR overrides
// it so tests can drive the co-location logic against a synthetic tree without a
// full `next build`. Mirrors the OMNIROUTE_* override seams in the sibling build
// scripts (write-build-sha.mjs, write-build-base-path.mjs, optionalPackStaging.mjs).
const STANDALONE = process.env.OMNIROUTE_STANDALONE_DIR
? process.env.OMNIROUTE_STANDALONE_DIR
: join(ROOT, ".build", "next", "standalone");
const CALL_LOG_WORKER_REL = join("src", "lib", "usage", "callLogArtifactWorker.js");
const CALL_LOG_WORKER_SRC = join(ROOT, "src", "lib", "usage", "callLogArtifactWorker.ts");
@@ -35,97 +41,138 @@ const WORKER_REL = join(
"llmlingua",
"onnxWorker.js"
);
const GATE_PKG = join("node_modules", "@atjsh", "llmlingua-2", "package.json");
const hasOptionals = existsSync(
join(ROOT, "node_modules", "@atjsh", "llmlingua-2", "package.json")
);
if (!existsSync(STANDALONE)) {
console.log("[colocate-standalone] .build/next/standalone not found — nothing to do.");
process.exit(0);
}
const callLogWorkerDest = join(STANDALONE, CALL_LOG_WORKER_REL);
mkdirSync(dirname(callLogWorkerDest), { recursive: true });
execFileSync(
join(ROOT, "node_modules", ".bin", "esbuild"),
[
CALL_LOG_WORKER_SRC,
"--bundle",
"--platform=node",
"--packages=external",
"--format=esm",
`--outfile=${callLogWorkerDest}`,
],
{ stdio: "inherit" }
);
console.log("[colocate-standalone] ✅ call-log artifact worker bundled");
if (!hasOptionals) {
console.log(
"[colocate-standalone] optional SLM deps absent at root node_modules — LLMLingua stays fail-open (slim install)."
);
process.exit(0);
}
// 1) Bundle the worker the resolver expects: <standalone>/open-sse/.../onnxWorker.js
const workerDest = join(STANDALONE, WORKER_REL);
if (!existsSync(workerDest)) {
mkdirSync(dirname(workerDest), { recursive: true });
try {
execFileSync(
join(ROOT, "node_modules", ".bin", "esbuild"),
[
join(ROOT, "open-sse", "services", "compression", "engines", "llmlingua", "onnxWorker.ts"),
"--bundle",
"--platform=node",
"--packages=external",
"--format=esm",
`--outfile=${workerDest}`,
],
{ stdio: "inherit" }
);
console.log("[colocate-standalone] ✅ LLMLingua worker bundled into standalone tree");
} catch (err) {
console.warn("[colocate-standalone] ⚠️ worker bundle error:", err.message);
}
} else {
console.log("[colocate-standalone] worker already present (skipping bundle)");
}
// 2) Co-locate the optional-dep closure (NO-CLOBBER, same semantics as colocateOptionals.mjs)
const srcNm = join(ROOT, "node_modules");
const dstNm = join(STANDALONE, "node_modules");
const closure = computeDependencyClosure(srcNm);
let copied = 0;
for (const pkg of closure) {
const src = join(srcNm, pkg);
const dst = join(dstNm, pkg);
if (!existsSync(src)) continue;
if (existsSync(dst)) continue; // no-clobber: keep traced instances (e.g. pinned @huggingface/transformers)
mkdirSync(dirname(dst), { recursive: true });
cpSync(src, dst, { recursive: true });
copied++;
}
console.log(
`[colocate-standalone] ✅ optional-dep closure: ${closure.length} packages (copied ${copied})`
);
// 3) Ensure standalone package.json declares "type": "module" so Node 24 runs ESM worker bundles without warning
const standalonePkgPath = join(STANDALONE, "package.json");
if (existsSync(standalonePkgPath)) {
try {
const rawPkg = readFileSync(standalonePkgPath, "utf8");
const pkgJson = JSON.parse(rawPkg);
if (!pkgJson.type) {
pkgJson.type = "module";
writeFileSync(standalonePkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
console.log("[colocate-standalone] ✅ standalone package.json configured with type: module");
/**
* Give each esbuild'd ESM worker its OWN `"type":"module"` scope.
*
* The worker bundles are emitted with `--format=esm` under `.js` names, so Node
* needs a nearest-ancestor package.json declaring `"type":"module"` to load them
* as ESM. It is tempting to set that on the standalone ROOT package.json, but the
* standalone entrypoint `server.js` is CommonJS (`require()`, `__dirname`); a root
* `"type":"module"` makes Node parse server.js as ESM and it crashes at startup
* with `ReferenceError: require is not defined in ES module scope`.
* assembleStandalone.mjs::patchStandalonePackageJson strips `type` for exactly
* this reason — re-adding it on the root here reintroduced that crash.
*
* Node resolves module type from the NEAREST package.json, so a scoped
* `{"type":"module"}` beside each worker makes the worker ESM while the root stays
* CommonJS for server.js. Both coexist with no format change and no root edit.
*
* @param {string[]} workerDirs Absolute directories that hold an ESM worker bundle.
* @returns {string[]} The package.json paths that were written (existing ones are left intact).
*/
export function writeEsmWorkerScopes(workerDirs) {
const written = [];
for (const dir of workerDirs) {
const scopedPkgPath = join(dir, "package.json");
if (existsSync(scopedPkgPath)) continue; // never clobber a traced package.json
try {
writeFileSync(scopedPkgPath, JSON.stringify({ type: "module" }, null, 2) + "\n", "utf8");
written.push(scopedPkgPath);
console.log(`[colocate-standalone] ✅ ESM scope written: ${scopedPkgPath}`);
} catch (err) {
console.warn(`[colocate-standalone] ⚠️ could not write ESM scope for ${dir}:`, err.message);
}
} catch (err) {
console.warn(
"[colocate-standalone] ⚠️ could not update standalone package.json:",
err.message
);
}
return written;
}
function main() {
const hasOptionals = existsSync(
join(ROOT, "node_modules", "@atjsh", "llmlingua-2", "package.json")
);
if (!existsSync(STANDALONE)) {
console.log("[colocate-standalone] .build/next/standalone not found — nothing to do.");
return;
}
const callLogWorkerDest = join(STANDALONE, CALL_LOG_WORKER_REL);
mkdirSync(dirname(callLogWorkerDest), { recursive: true });
execFileSync(
join(ROOT, "node_modules", ".bin", "esbuild"),
[
CALL_LOG_WORKER_SRC,
"--bundle",
"--platform=node",
"--packages=external",
"--format=esm",
`--outfile=${callLogWorkerDest}`,
],
{ stdio: "inherit" }
);
console.log("[colocate-standalone] ✅ call-log artifact worker bundled");
// The call-log worker is always present; scope it to ESM immediately. The
// optional LLMLingua worker dir is added below only when its deps are installed.
const workerDirs = [dirname(callLogWorkerDest)];
if (!hasOptionals) {
console.log(
"[colocate-standalone] optional SLM deps absent at root node_modules — LLMLingua stays fail-open (slim install)."
);
writeEsmWorkerScopes(workerDirs);
return;
}
// 1) Bundle the worker the resolver expects: <standalone>/open-sse/.../onnxWorker.js
const workerDest = join(STANDALONE, WORKER_REL);
if (!existsSync(workerDest)) {
mkdirSync(dirname(workerDest), { recursive: true });
try {
execFileSync(
join(ROOT, "node_modules", ".bin", "esbuild"),
[
join(
ROOT,
"open-sse",
"services",
"compression",
"engines",
"llmlingua",
"onnxWorker.ts"
),
"--bundle",
"--platform=node",
"--packages=external",
"--format=esm",
`--outfile=${workerDest}`,
],
{ stdio: "inherit" }
);
console.log("[colocate-standalone] ✅ LLMLingua worker bundled into standalone tree");
} catch (err) {
console.warn("[colocate-standalone] ⚠️ worker bundle error:", err.message);
}
} else {
console.log("[colocate-standalone] worker already present (skipping bundle)");
}
workerDirs.push(dirname(workerDest));
// 2) Co-locate the optional-dep closure (NO-CLOBBER, same semantics as colocateOptionals.mjs)
const srcNm = join(ROOT, "node_modules");
const dstNm = join(STANDALONE, "node_modules");
const closure = computeDependencyClosure(srcNm);
let copied = 0;
for (const pkg of closure) {
const src = join(srcNm, pkg);
const dst = join(dstNm, pkg);
if (!existsSync(src)) continue;
if (existsSync(dst)) continue; // no-clobber: keep traced instances (e.g. pinned @huggingface/transformers)
mkdirSync(dirname(dst), { recursive: true });
cpSync(src, dst, { recursive: true });
copied++;
}
console.log(
`[colocate-standalone] ✅ optional-dep closure: ${closure.length} packages (copied ${copied})`
);
// 3) Give each esbuild'd ESM worker its own "type":"module" scope (see helper doc).
writeEsmWorkerScopes(workerDirs);
}
// Run as a script (npm `postbuild` hook), but stay importable for unit tests.
const entryScript = process.argv[1] ? pathToFileURL(process.argv[1]).href : null;
if (entryScript === import.meta.url) {
main();
}