@decocms/blocks-cli 7.7.0 → 7.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/blocks-cli",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco codegen (generate-blocks, generate-schema, generate-invoke) and Fresh-to-TanStack migration tooling",
|
|
6
6
|
"repository": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"lint:unused": "knip"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@decocms/blocks": "7.
|
|
37
|
+
"@decocms/blocks": "7.8.0",
|
|
38
38
|
"ts-morph": "^27.0.0",
|
|
39
39
|
"tsx": "^4.22.5"
|
|
40
40
|
},
|
|
@@ -190,3 +190,81 @@ describe("generate-invoke.ts — output shape", () => {
|
|
|
190
190
|
expect(generatedOutput).toContain("const result = await simulateCart(data);");
|
|
191
191
|
});
|
|
192
192
|
});
|
|
193
|
+
|
|
194
|
+
describe("generate-invoke.ts — default --apps-dir resolution", () => {
|
|
195
|
+
// Regression for the published-tarball layout: @decocms/apps-vtex 7.x
|
|
196
|
+
// ships its sources under src/ (`"files": ["src"]`), so invoke.ts lives
|
|
197
|
+
// at node_modules/@decocms/apps-vtex/src/invoke.ts — NOT at the package
|
|
198
|
+
// root. The old default only probed the root, never resolved on a site
|
|
199
|
+
// with npm-installed packages, and forced sites to pass
|
|
200
|
+
// `--apps-dir node_modules/@decocms/apps-vtex/src` by hand (granadobr's
|
|
201
|
+
// migration workaround). The default must probe <pkg>/invoke.ts first,
|
|
202
|
+
// then <pkg>/src/invoke.ts.
|
|
203
|
+
|
|
204
|
+
function makeSite(layout: "root" | "src"): { siteDir: string; cleanup: () => void } {
|
|
205
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "gen-invoke-default-"));
|
|
206
|
+
const siteDir = path.join(tmp, "site");
|
|
207
|
+
const pkgDir = path.join(siteDir, "node_modules", "@decocms", "apps-vtex");
|
|
208
|
+
const appsDir = layout === "src" ? path.join(pkgDir, "src") : pkgDir;
|
|
209
|
+
fs.mkdirSync(path.join(appsDir, "actions"), { recursive: true });
|
|
210
|
+
fs.writeFileSync(path.join(appsDir, "invoke.ts"), FIXTURE_INVOKE_TS);
|
|
211
|
+
fs.writeFileSync(path.join(appsDir, "actions", "checkout.ts"), FIXTURE_ACTIONS_CHECKOUT_TS);
|
|
212
|
+
fs.writeFileSync(path.join(appsDir, "actions", "session.ts"), FIXTURE_ACTIONS_SESSION_TS);
|
|
213
|
+
fs.writeFileSync(path.join(appsDir, "types.ts"), FIXTURE_TYPES_TS);
|
|
214
|
+
return { siteDir, cleanup: () => fs.rmSync(tmp, { recursive: true, force: true }) };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
it("resolves the published layout (node_modules/@decocms/apps-vtex/src/invoke.ts) without --apps-dir", () => {
|
|
218
|
+
const { siteDir, cleanup } = makeSite("src");
|
|
219
|
+
try {
|
|
220
|
+
const outFile = path.join(siteDir, "src", "server", "invoke.gen.ts");
|
|
221
|
+
const result = spawnSync("npx", ["tsx", GENERATOR, "--out-file", outFile], {
|
|
222
|
+
cwd: siteDir,
|
|
223
|
+
encoding: "utf8",
|
|
224
|
+
});
|
|
225
|
+
expect(result.status, result.stderr).toBe(0);
|
|
226
|
+
const generated = fs.readFileSync(outFile, "utf8");
|
|
227
|
+
// Relative `./actions/*` imports must still be rewritten to package
|
|
228
|
+
// subpaths — the exports map points them back into src/, so the src/
|
|
229
|
+
// nesting must not leak into the emitted specifiers.
|
|
230
|
+
expect(generated).toContain('from "@decocms/apps-vtex/actions/checkout"');
|
|
231
|
+
expect(generated).not.toContain("apps-vtex/src/");
|
|
232
|
+
expect(generated).toContain("export const vtexActions");
|
|
233
|
+
} finally {
|
|
234
|
+
cleanup();
|
|
235
|
+
}
|
|
236
|
+
}, 30_000);
|
|
237
|
+
|
|
238
|
+
it("still resolves invoke.ts at the package root (legacy dev-checkout layout) without --apps-dir", () => {
|
|
239
|
+
const { siteDir, cleanup } = makeSite("root");
|
|
240
|
+
try {
|
|
241
|
+
const outFile = path.join(siteDir, "src", "server", "invoke.gen.ts");
|
|
242
|
+
const result = spawnSync("npx", ["tsx", GENERATOR, "--out-file", outFile], {
|
|
243
|
+
cwd: siteDir,
|
|
244
|
+
encoding: "utf8",
|
|
245
|
+
});
|
|
246
|
+
expect(result.status, result.stderr).toBe(0);
|
|
247
|
+
const generated = fs.readFileSync(outFile, "utf8");
|
|
248
|
+
expect(generated).toContain('from "@decocms/apps-vtex/actions/checkout"');
|
|
249
|
+
expect(generated).toContain("export const vtexActions");
|
|
250
|
+
} finally {
|
|
251
|
+
cleanup();
|
|
252
|
+
}
|
|
253
|
+
}, 30_000);
|
|
254
|
+
|
|
255
|
+
it("fails with a clear error when @decocms/apps-vtex cannot be found", () => {
|
|
256
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "gen-invoke-missing-"));
|
|
257
|
+
try {
|
|
258
|
+
const outFile = path.join(tmp, "src", "server", "invoke.gen.ts");
|
|
259
|
+
const result = spawnSync("npx", ["tsx", GENERATOR, "--out-file", outFile], {
|
|
260
|
+
cwd: tmp,
|
|
261
|
+
encoding: "utf8",
|
|
262
|
+
});
|
|
263
|
+
expect(result.status).not.toBe(0);
|
|
264
|
+
expect(result.stderr).toContain("Could not find @decocms/apps-vtex");
|
|
265
|
+
expect(result.stderr).toContain("--apps-dir");
|
|
266
|
+
} finally {
|
|
267
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
268
|
+
}
|
|
269
|
+
}, 30_000);
|
|
270
|
+
});
|
|
@@ -55,17 +55,26 @@ function resolveAppsDir(): string {
|
|
|
55
55
|
const explicit = arg("apps-dir", "");
|
|
56
56
|
if (explicit) return path.resolve(cwd, explicit);
|
|
57
57
|
|
|
58
|
-
// Try common locations
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
58
|
+
// Try common locations: the installed @decocms/apps-vtex package first,
|
|
59
|
+
// then a raw apps-start checkout's vtex/ subdirectory as a legacy fallback
|
|
60
|
+
// for anyone still developing against the pre-split monorepo.
|
|
61
|
+
//
|
|
62
|
+
// For each root, invoke.ts may sit directly at the root (legacy dev
|
|
63
|
+
// checkouts) or under src/ — the published @decocms/apps-vtex tarball
|
|
64
|
+
// ships its sources under src/ (`"files": ["src"]`, `"main":
|
|
65
|
+
// "./src/index.ts"`), so on any site with npm-installed 7.x packages the
|
|
66
|
+
// file lives at node_modules/@decocms/apps-vtex/src/invoke.ts. The src/
|
|
67
|
+
// nesting doesn't affect the emitted imports: relative `./actions/*`
|
|
68
|
+
// specifiers are rewritten to `@decocms/apps-vtex/actions/*`, which the
|
|
69
|
+
// package's exports map points back into src/.
|
|
70
|
+
const roots = [
|
|
64
71
|
path.resolve(cwd, "node_modules/@decocms/apps-vtex"),
|
|
65
72
|
path.resolve(cwd, "../apps-start/vtex"),
|
|
66
73
|
];
|
|
67
|
-
for (const
|
|
68
|
-
|
|
74
|
+
for (const root of roots) {
|
|
75
|
+
for (const c of [root, path.join(root, "src")]) {
|
|
76
|
+
if (fs.existsSync(path.join(c, "invoke.ts"))) return c;
|
|
77
|
+
}
|
|
69
78
|
}
|
|
70
79
|
throw new Error("Could not find @decocms/apps-vtex. Use --apps-dir to specify its location.");
|
|
71
80
|
}
|
|
@@ -281,6 +281,75 @@ describe("generate-sections --registry", () => {
|
|
|
281
281
|
}, 30_000);
|
|
282
282
|
});
|
|
283
283
|
|
|
284
|
+
describe("generate-sections neverDefer convention", () => {
|
|
285
|
+
// Regression: the scanner recognized `export const neverDefer = true` and
|
|
286
|
+
// emitted `neverDefer: true` on the section's sectionMeta entry, but the
|
|
287
|
+
// SectionMetaEntry interface the SAME file declares omitted the field —
|
|
288
|
+
// so every generated file containing a neverDefer section failed the
|
|
289
|
+
// site's typecheck (TS2353 excess property), and sites hand-patched the
|
|
290
|
+
// interface only to have the next regeneration wipe the patch (miess's
|
|
291
|
+
// .deco/sections.gen.ts carried exactly that TODO). The emitted interface
|
|
292
|
+
// must match SectionMetaEntry in @decocms/blocks/cms.
|
|
293
|
+
let tmpDir: string;
|
|
294
|
+
let sectionsDir: string;
|
|
295
|
+
let outFile: string;
|
|
296
|
+
|
|
297
|
+
beforeEach(() => {
|
|
298
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "generate-sections-neverdefer-"));
|
|
299
|
+
sectionsDir = path.join(tmpDir, "sections");
|
|
300
|
+
outFile = path.join(tmpDir, "out", "sections.gen.ts");
|
|
301
|
+
fs.mkdirSync(sectionsDir, { recursive: true });
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
afterEach(() => {
|
|
305
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("declares neverDefer on the emitted SectionMetaEntry interface and the generated file typechecks + imports", () => {
|
|
309
|
+
// Mirrors miess's src/sections/Product/SearchResult.tsx.
|
|
310
|
+
fs.writeFileSync(
|
|
311
|
+
path.join(sectionsDir, "SearchResult.tsx"),
|
|
312
|
+
"export const neverDefer = true;\nexport default function SearchResult() { return null; }\n",
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
const { code } = runGenerator(["--sections-dir", sectionsDir, "--out-file", outFile]);
|
|
316
|
+
expect(code).toBe(0);
|
|
317
|
+
|
|
318
|
+
const generated = fs.readFileSync(outFile, "utf-8");
|
|
319
|
+
// Entry carries the convention…
|
|
320
|
+
expect(generated).toMatch(/"site\/sections\/SearchResult\.tsx": \{ neverDefer: true \}/);
|
|
321
|
+
// …and the interface declares the field (optional boolean, matching
|
|
322
|
+
// SectionMetaEntry in @decocms/blocks/cms/applySectionConventions.ts).
|
|
323
|
+
expect(generated).toContain("neverDefer?: boolean;");
|
|
324
|
+
|
|
325
|
+
// The actual failure mode was a TYPECHECK error (excess property on the
|
|
326
|
+
// Record<string, SectionMetaEntry> literal), which a runtime import
|
|
327
|
+
// through tsx/esbuild would never catch — so run tsc on the output.
|
|
328
|
+
const tscResult = cp.spawnSync(
|
|
329
|
+
"npx",
|
|
330
|
+
["tsc", "--noEmit", "--strict", "--skipLibCheck", outFile],
|
|
331
|
+
{ encoding: "utf8" },
|
|
332
|
+
);
|
|
333
|
+
expect(tscResult.status, tscResult.stdout + tscResult.stderr).toBe(0);
|
|
334
|
+
|
|
335
|
+
// And keep the importability guarantee from the earlier template
|
|
336
|
+
// regression: the file must load as valid TS/ESM with the expected
|
|
337
|
+
// exports.
|
|
338
|
+
const checkerFile = path.join(tmpDir, "check-import.mjs");
|
|
339
|
+
fs.writeFileSync(
|
|
340
|
+
checkerFile,
|
|
341
|
+
[
|
|
342
|
+
`const m = await import(${JSON.stringify(pathToFileURL(outFile).href)});`,
|
|
343
|
+
`if (m.sectionMeta?.["site/sections/SearchResult.tsx"]?.neverDefer !== true) {`,
|
|
344
|
+
` throw new Error("neverDefer entry missing from sectionMeta");`,
|
|
345
|
+
`}`,
|
|
346
|
+
].join("\n"),
|
|
347
|
+
);
|
|
348
|
+
const importResult = cp.spawnSync("npx", ["tsx", checkerFile], { encoding: "utf8" });
|
|
349
|
+
expect(importResult.status, importResult.stderr).toBe(0);
|
|
350
|
+
}, 60_000);
|
|
351
|
+
});
|
|
352
|
+
|
|
284
353
|
describe("generate-sections output hygiene (non-registry)", () => {
|
|
285
354
|
let tmpDir: string;
|
|
286
355
|
let sectionsDir: string;
|
|
@@ -194,8 +194,14 @@ for (let i = 0; i < nonSyncFallbacks.length; i++) {
|
|
|
194
194
|
lines.push("");
|
|
195
195
|
|
|
196
196
|
// Metadata map
|
|
197
|
+
// Keep this emitted interface in sync with SectionMetaEntry in
|
|
198
|
+
// @decocms/blocks/cms (applySectionConventions.ts) — every convention the
|
|
199
|
+
// scanner can set on an entry must be declared here, or the generated file
|
|
200
|
+
// fails the site's typecheck (excess-property error) and sites end up
|
|
201
|
+
// hand-patching a file that the next regeneration wipes.
|
|
197
202
|
lines.push("export interface SectionMetaEntry {");
|
|
198
203
|
lines.push(" eager?: boolean;");
|
|
204
|
+
lines.push(" neverDefer?: boolean;");
|
|
199
205
|
lines.push(" cache?: string;");
|
|
200
206
|
lines.push(" layout?: boolean;");
|
|
201
207
|
lines.push(" sync?: boolean;");
|