Skip to content

Commit ee0023e

Browse files
dmealingclaude
andcommitted
fix(cli): sweep stale config temp files + document dynamic-import gap; bump TPH lane timeout
F1: loadMetaobjectsConfig now sweeps any .metaobjects-config-proc-*.ts stranded by an abnormal exit (SIGKILL between temp-write and the finally cleanup), so a consumer's git status never surfaces the pre-processed artifact. Extracted the prefix to a PROC_TEMP_PREFIX constant shared by the writer and the sweep. F2: documented that dynamic import() specifiers are intentionally not rewritten. F3: api-contract TPH scenario timeout 60s -> 120s (shared TPH_SCENARIO_TIMEOUT_MS) — the generated lane hit the 60s ceiling once under cold 4-way parallel docker load on the local-ci runner. From the final whole-branch review follow-ups (F1/F2/F3). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SKaSE8U5cwN4ZGFwhJwxSY
1 parent cfd96cc commit ee0023e

3 files changed

Lines changed: 54 additions & 5 deletions

File tree

server/typescript/packages/cli/src/lib/load-metaobjects-config.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { existsSync, unlinkSync } from "node:fs";
1+
import { existsSync, readdirSync, unlinkSync } from "node:fs";
22
import { readFile, writeFile } from "node:fs/promises";
33
import { createRequire } from "node:module";
44
import { dirname, resolve } from "node:path";
@@ -9,6 +9,12 @@ import type { MetaobjectsGenConfig } from "@metaobjectsdev/codegen-ts";
99

1010
const CONFIG_FILE = "metaobjects.config.ts";
1111

12+
// Prefix for the transient, pre-processed config the loader writes next to the
13+
// user's config (see the rewrite step below). Normally deleted in a finally
14+
// block; the prefix is also used to sweep any copies stranded by an abnormal
15+
// exit so a consumer's `git status` never surfaces one.
16+
const PROC_TEMP_PREFIX = ".metaobjects-config-proc-";
17+
1218
// Resolve @metaobjectsdev/codegen-ts from the CLI's own node_modules so that
1319
// metaobjects.config.ts (which lives in the user's project) can import it even
1420
// when the user's project has no direct dependency on the package.
@@ -116,6 +122,11 @@ function resolveCliPkg(specifier: string): string {
116122
*
117123
* Relative imports (`./foo`, `../bar`) are intentionally NOT rewritten; they
118124
* must continue to resolve relative to the config file's own directory.
125+
*
126+
* Dynamic `import("specifier")` calls are NOT rewritten either — configs are
127+
* loaded eagerly and none of the scaffolded templates use them; a dynamic
128+
* import of an aliased package falls back to normal module resolution (and to
129+
* jiti's alias map on non-Bun runtimes).
119130
*/
120131
function rewriteImportSpecifiers(source: string, aliasMap: Record<string, string>): string {
121132
let result = source;
@@ -144,6 +155,17 @@ export async function loadMetaobjectsConfig(projectRoot: string): Promise<Metaob
144155
);
145156
}
146157

158+
// Self-heal: a SIGKILL mid-load can strand a pre-processed temp config
159+
// (PROC_TEMP_PREFIX*.ts) next to the user's config, since deletion normally
160+
// happens in the finally block below. Sweep any stale ones before loading so
161+
// an abnormal exit never pollutes the consumer's working tree.
162+
const configDir = dirname(fullPath);
163+
for (const entry of readdirSync(configDir)) {
164+
if (entry.startsWith(PROC_TEMP_PREFIX) && entry.endsWith(".ts")) {
165+
try { unlinkSync(resolve(configDir, entry)); } catch { /* best-effort */ }
166+
}
167+
}
168+
147169
// Build the canonical alias map: specifier → resolved absolute path.
148170
const aliasMap: Record<string, string> = {};
149171
for (const specifier of Object.keys(CLI_PKG_PATHS)) {
@@ -165,7 +187,7 @@ export async function loadMetaobjectsConfig(projectRoot: string): Promise<Metaob
165187
let loadPath = fullPath;
166188
let tempCreated = false;
167189
if (processed !== original) {
168-
const tempName = `.metaobjects-config-proc-${randomBytes(4).toString("hex")}.ts`;
190+
const tempName = `${PROC_TEMP_PREFIX}${randomBytes(4).toString("hex")}.ts`;
169191
const tempPath = resolve(dirname(fullPath), tempName);
170192
try {
171193
await writeFile(tempPath, processed, "utf8");

server/typescript/packages/cli/test/unit/load-metaobjects-config.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2-
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
2+
import { mkdtempSync, rmSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
33
import { tmpdir } from "node:os";
44
import { join, resolve } from "node:path";
55
import { loadMetaobjectsConfig } from "../../src/lib/load-metaobjects-config.js";
@@ -78,6 +78,27 @@ describe("loadMetaobjectsConfig", () => {
7878
}
7979
});
8080

81+
test("sweeps a stranded .metaobjects-config-proc-*.ts temp file left by an abnormal exit", async () => {
82+
// A SIGKILL during a prior load can strand the pre-processed temp config
83+
// (whose deletion normally happens in a finally block). The next load must
84+
// self-heal by removing any such stale files before proceeding, so a
85+
// consumer's `git status` never shows the artifact.
86+
const stale = join(tmp, ".metaobjects-config-proc-deadbeef.ts");
87+
writeFileSync(stale, `export default { generators: [] };`);
88+
writeFileSync(join(tmp, "metaobjects.config.ts"), `
89+
import { defineConfig } from "@metaobjectsdev/codegen-ts";
90+
import { entityFile, barrel } from "@metaobjectsdev/codegen-ts/generators";
91+
export default defineConfig({
92+
outDir: "out", extStyle: "none", dbImport: "../db", dialect: "sqlite",
93+
generators: [entityFile(), barrel()],
94+
});
95+
`);
96+
expect(existsSync(stale)).toBe(true);
97+
const cfg = await loadMetaobjectsConfig(tmp);
98+
expect(cfg.generators.length).toBe(2); // normal load still works
99+
expect(existsSync(stale)).toBe(false); // stale temp file swept
100+
});
101+
81102
test("loads a config that imports from @metaobjectsdev/codegen-ts-tanstack", async () => {
82103
const osTmp = mkdtempSync(join(tmpdir(), "metaobjects-config-tanstack-"));
83104
try {

server/typescript/packages/integration-tests/test/api-contract-tph.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ import {
2727
const SEED = JSON.parse(readFileSync(join(API_CONTRACT_TPH_DIR, "seed.json"), "utf8")) as TphSeed;
2828
const META_PATH = join(API_CONTRACT_TPH_DIR, "meta.json");
2929

30+
// 120s per scenario: each spins up its own server + Testcontainers PG. The old
31+
// 60s ceiling was hit once by the generated lane under cold 4-way parallel
32+
// docker load on the local-ci runner; the headroom absorbs that startup jitter
33+
// without weakening the assertions. Both lanes share the same boot cost.
34+
const TPH_SCENARIO_TIMEOUT_MS = 120_000;
35+
3036
interface Lane { baseUrl: string; applySeed(s: TphSeed): Promise<void>; close(): Promise<void>; }
3137

3238
describe("api contract TPH — hand-rolled reference lane", () => {
@@ -42,7 +48,7 @@ describe("api contract TPH — hand-rolled reference lane", () => {
4248
if (server) await server.close();
4349
await pg.stop();
4450
}
45-
}, { timeout: 60_000 });
51+
}, { timeout: TPH_SCENARIO_TIMEOUT_MS });
4652
}
4753
});
4854

@@ -59,7 +65,7 @@ describe("api contract TPH — GENERATED routes lane", () => {
5965
if (server) await server.close();
6066
await pg.stop();
6167
}
62-
}, { timeout: 60_000 });
68+
}, { timeout: TPH_SCENARIO_TIMEOUT_MS });
6369
}
6470
});
6571

0 commit comments

Comments
 (0)