@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.7.0",
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.7.0",
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. Both point at the directory that contains
59
- // invoke.ts directly at its root: the installed @decocms/apps-vtex
60
- // package (no more vtex/ nesting — that directory IS the package root
61
- // now), or a raw apps-start checkout's vtex/ subdirectory as a legacy
62
- // fallback for anyone still developing against the pre-split monorepo.
63
- const candidates = [
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 c of candidates) {
68
- if (fs.existsSync(path.join(c, "invoke.ts"))) return c;
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;");