Skip to content

Commit 24dba4b

Browse files
headbouyJBclaudevincentkoc
authored
fix(build): forward default exports through stable runtime aliases (#99678)
* fix(build): forward default exports through stable runtime aliases runtime-postbuild writes stable aliases (`X.runtime.js`) for hashed runtime chunks as bare `export * from "./X-HASH.js"` — but `export * from` never re-exports `default`. In the shipped 2026.6.10/2026.6.11 artifacts the compaction runtime alias points at a chunk whose only export is default, so its lazy consumer destructuring `{ default: reconcile }` gets undefined: every successful auto-compaction logs "late compaction count reconcile failed: TypeError: reconcile is not a function" and the persisted compactionCount never updates. On local-model deployments the resulting repeat compactions each cost a multi-minute full re-prefill (observed: 3 compactions in 25 minutes). Other stable aliases are latent instances of the same generator defect for any target that gains a default export. Fix: when the alias target has a default export, also emit `export { default } from ...` (applies to both the stable-alias and legacy-compat writers). Detection is an anchored export-statement pattern, verified against all 7 shipped runtime chunks (no false positives; naive text matching would both miss and over-match — an alias claiming a default its target lacks is a hard SyntaxError at import). Adds a regression test mirroring the real compaction-reconcile chunk shape. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01H6Hz9UEpxQ4W3d8XecvupH * fix(build): preserve defaults through legacy runtime aliases --------- Co-authored-by: headbouyJB <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> Co-authored-by: Vincent Koc <[email protected]>
1 parent 61564f2 commit 24dba4b

2 files changed

Lines changed: 96 additions & 3 deletions

File tree

scripts/runtime-postbuild.mjs

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ function resolveStableRootRuntimeAliasCandidate(params) {
200200
return { candidate, source };
201201
});
202202
const implementationCandidates = candidatesWithSources.filter(
203-
({ source }) => source.trim() !== `export * from "./${aliasFileName}";`,
203+
({ source }) => !isRuntimeAliasSource(source, aliasFileName),
204204
);
205205
const candidateNames = implementationCandidates.map(({ candidate }) => candidate);
206206
if (candidateNames.length === 1) {
@@ -299,6 +299,45 @@ export function listCoreRuntimePostBuildOutputs(params = {}) {
299299
].toSorted((left, right) => left.localeCompare(right));
300300
}
301301

302+
const RUNTIME_CHUNK_DEFAULT_EXPORT_PATTERN =
303+
/(^|\n)\s*export\b\s*(?:default\b|\{[^}]*(?:\bas\s+default\b|\bdefault\b\s*(?=[,}]))[^}]*\})/u;
304+
305+
function formatRuntimeAliasSource(targetFileName, forwardDefault = false) {
306+
const specifier = JSON.stringify(`./${targetFileName}`);
307+
const starSource = `export * from ${specifier};\n`;
308+
return forwardDefault ? `${starSource}export { default } from ${specifier};\n` : starSource;
309+
}
310+
311+
function isRuntimeAliasSource(source, targetFileName) {
312+
const normalizedSource = source.trim();
313+
return (
314+
normalizedSource === formatRuntimeAliasSource(targetFileName).trim() ||
315+
normalizedSource === formatRuntimeAliasSource(targetFileName, true).trim()
316+
);
317+
}
318+
319+
/**
320+
* Builds alias module source for a runtime chunk target. `export * from`
321+
* never re-exports `default`, so when the target chunk has a default export
322+
* the alias must forward it explicitly — otherwise lazy `import()` consumers
323+
* that destructure `default` receive `undefined` (e.g. the post-compaction
324+
* count reconcile failed this way with "TypeError: reconcile is not a
325+
* function" on every compaction).
326+
*/
327+
function buildRuntimeAliasSource(params) {
328+
const { distDir, fsImpl, targetFileName } = params;
329+
let targetSource;
330+
try {
331+
targetSource = fsImpl.readFileSync(path.join(distDir, targetFileName), "utf8");
332+
} catch {
333+
return formatRuntimeAliasSource(targetFileName);
334+
}
335+
return formatRuntimeAliasSource(
336+
targetFileName,
337+
RUNTIME_CHUNK_DEFAULT_EXPORT_PATTERN.test(targetSource),
338+
);
339+
}
340+
302341
/**
303342
* Writes stable aliases for current hashed runtime chunks.
304343
*/
@@ -320,7 +359,10 @@ export function writeStableRootRuntimeAliases(params = {}) {
320359
fsImpl.rmSync?.(aliasPath, { force: true });
321360
continue;
322361
}
323-
writeTextFileIfChanged(aliasPath, `export * from "./${candidate}";\n`);
362+
writeTextFileIfChanged(
363+
aliasPath,
364+
buildRuntimeAliasSource({ distDir, fsImpl, targetFileName: candidate }),
365+
);
324366
}
325367
}
326368

@@ -480,7 +522,10 @@ export function writeLegacyRootRuntimeCompatAliases(params = {}) {
480522
if (!targetFileName) {
481523
continue;
482524
}
483-
writeTextFileIfChanged(legacyPath, `export * from "./${targetFileName}";\n`);
525+
writeTextFileIfChanged(
526+
legacyPath,
527+
buildRuntimeAliasSource({ distDir, fsImpl, targetFileName }),
528+
);
484529
}
485530
}
486531

test/scripts/runtime-postbuild.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Runtime Postbuild tests cover runtime postbuild script behavior.
22
import fs from "node:fs/promises";
33
import path from "node:path";
4+
import { pathToFileURL } from "node:url";
45
import { describe, expect, it, vi } from "vitest";
56
import {
67
copyStaticExtensionAssetsToRuntimeOverlay,
@@ -361,6 +362,53 @@ describe("runtime postbuild static assets", () => {
361362
await expectPathMissing(path.join(distDir, "library.js"));
362363
});
363364

365+
it("forwards default exports through stable and legacy aliases", async () => {
366+
const rootDir = createTempDir("openclaw-runtime-postbuild-");
367+
const distDir = path.join(rootDir, "dist");
368+
await fs.mkdir(distDir, { recursive: true });
369+
await fs.writeFile(path.join(rootDir, "package.json"), '{"type":"module"}\n', "utf8");
370+
await fs.writeFile(
371+
path.join(distDir, "runtime-plugins.runtime-Hash111.js"),
372+
"function reconcile(value) { return value; }\nexport { reconcile as default };\n",
373+
"utf8",
374+
);
375+
await fs.writeFile(
376+
path.join(distDir, "mixed.runtime-Hash222.js"),
377+
"export const named = true;\nexport default function run() {}\n",
378+
"utf8",
379+
);
380+
await fs.writeFile(
381+
path.join(distDir, "named-only.runtime-Hash333.js"),
382+
'const marker = "export { marker as default }";\nexport { marker };\n',
383+
"utf8",
384+
);
385+
386+
writeStableRootRuntimeAliases({ rootDir });
387+
writeLegacyRootRuntimeCompatAliases({ rootDir });
388+
389+
const stable = await import(
390+
pathToFileURL(path.join(distDir, "runtime-plugins.runtime.js")).href
391+
);
392+
const legacy = await import(
393+
pathToFileURL(path.join(distDir, "runtime-plugins.runtime-fLHuT7Vs.js")).href
394+
);
395+
const mixed = await import(pathToFileURL(path.join(distDir, "mixed.runtime.js")).href);
396+
const namedOnly = await import(pathToFileURL(path.join(distDir, "named-only.runtime.js")).href);
397+
398+
expect(stable.default("stable")).toBe("stable");
399+
expect(legacy.default("legacy")).toBe("legacy");
400+
expect(mixed.default).toBeTypeOf("function");
401+
expect(namedOnly).not.toHaveProperty("default");
402+
403+
writeStableRootRuntimeAliases({ rootDir });
404+
writeLegacyRootRuntimeCompatAliases({ rootDir });
405+
406+
const stableAfterRerun = await import(
407+
`${pathToFileURL(path.join(distDir, "runtime-plugins.runtime.js")).href}?rerun=1`
408+
);
409+
expect(stableAfterRerun.default("rerun")).toBe("rerun");
410+
});
411+
364412
it("does not write ambiguous stable aliases for colliding root runtime chunks", async () => {
365413
const rootDir = createTempDir("openclaw-runtime-postbuild-");
366414
const distDir = path.join(rootDir, "dist");

0 commit comments

Comments
 (0)