@decocms/blocks-cli 7.39.0 → 7.40.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.40.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": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"lint:unused": "knip"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@decocms/blocks": "7.
|
|
34
|
+
"@decocms/blocks": "7.40.0",
|
|
35
35
|
"ts-morph": "^27.0.0",
|
|
36
36
|
"tsx": "^4.22.5"
|
|
37
37
|
},
|
|
@@ -336,6 +336,89 @@ describe("generate-sections neverDefer convention", () => {
|
|
|
336
336
|
}, 60_000);
|
|
337
337
|
});
|
|
338
338
|
|
|
339
|
+
describe("generate-sections renderJson convention", () => {
|
|
340
|
+
let tmpDir: string;
|
|
341
|
+
let sectionsDir: string;
|
|
342
|
+
let outFile: string;
|
|
343
|
+
|
|
344
|
+
beforeEach(() => {
|
|
345
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "generate-sections-renderjson-"));
|
|
346
|
+
sectionsDir = path.join(tmpDir, "sections");
|
|
347
|
+
outFile = path.join(tmpDir, "out", "sections.gen.ts");
|
|
348
|
+
fs.mkdirSync(sectionsDir, { recursive: true });
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
afterEach(() => {
|
|
352
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("emits `renderJson: false` into sectionMeta and a `renderJsons` map for projection fns; the output typechecks + imports", () => {
|
|
356
|
+
// `= false` opt-out: web-only section dropped from ?renderJson.
|
|
357
|
+
fs.writeFileSync(
|
|
358
|
+
path.join(sectionsDir, "Theme.tsx"),
|
|
359
|
+
"export const renderJson = false;\nexport default function Theme() { return null; }\n",
|
|
360
|
+
);
|
|
361
|
+
// projection function: trims props for the mobile app.
|
|
362
|
+
fs.writeFileSync(
|
|
363
|
+
path.join(sectionsDir, "ProductDetails.tsx"),
|
|
364
|
+
[
|
|
365
|
+
"export const renderJson = (props: Record<string, unknown>) => {",
|
|
366
|
+
" const { storeConfig: _s, ...rest } = props;",
|
|
367
|
+
" return rest;",
|
|
368
|
+
"};",
|
|
369
|
+
"export default function ProductDetails() { return null; }",
|
|
370
|
+
"",
|
|
371
|
+
].join("\n"),
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
const { code } = runGenerator(["--sections-dir", sectionsDir, "--out-file", outFile]);
|
|
375
|
+
expect(code).toBe(0);
|
|
376
|
+
|
|
377
|
+
const generated = fs.readFileSync(outFile, "utf-8");
|
|
378
|
+
|
|
379
|
+
// The `= false` section carries the literal false flag; the fn section
|
|
380
|
+
// carries hasRenderJson (its actual fn lives in the renderJsons map).
|
|
381
|
+
expect(generated).toMatch(/"site\/sections\/Theme\.tsx":\s*\{[^}]*renderJson:\s*false/);
|
|
382
|
+
expect(generated).toMatch(/"site\/sections\/ProductDetails\.tsx":\s*\{[^}]*hasRenderJson:\s*true/);
|
|
383
|
+
// The emitted interface declares both (excess-property guard, cf. neverDefer).
|
|
384
|
+
expect(generated).toContain("renderJson?: false;");
|
|
385
|
+
expect(generated).toContain("hasRenderJson?: boolean;");
|
|
386
|
+
// The fn is imported and wired into renderJsons; the `= false` one is NOT.
|
|
387
|
+
expect(generated).toContain("import { renderJson as _rj0 }");
|
|
388
|
+
expect(generated).toMatch(/export const renderJsons: Record<string, any> = \{[\s\S]*ProductDetails\.tsx": _rj0/);
|
|
389
|
+
expect(generated).not.toMatch(/renderJsons: Record<string, any> = \{[\s\S]*Theme\.tsx/);
|
|
390
|
+
|
|
391
|
+
// Typecheck the output (the real failure mode for a bad emit is TS2353, not
|
|
392
|
+
// a runtime error) and confirm it imports with a working projection fn.
|
|
393
|
+
// --jsx: the generated file imports a projection fn from a `.tsx` section,
|
|
394
|
+
// so tsc needs jsx set (a real site's tsconfig always does; the standalone
|
|
395
|
+
// invocation must too, or it fails with TS6142).
|
|
396
|
+
const tscResult = cp.spawnSync(
|
|
397
|
+
"npx",
|
|
398
|
+
["tsc", "--noEmit", "--strict", "--skipLibCheck", "--jsx", "react-jsx", outFile],
|
|
399
|
+
{ encoding: "utf8" },
|
|
400
|
+
);
|
|
401
|
+
expect(tscResult.status, tscResult.stdout + tscResult.stderr).toBe(0);
|
|
402
|
+
|
|
403
|
+
const checkerFile = path.join(tmpDir, "check-import.mjs");
|
|
404
|
+
fs.writeFileSync(
|
|
405
|
+
checkerFile,
|
|
406
|
+
[
|
|
407
|
+
`const m = await import(${JSON.stringify(pathToFileURL(outFile).href)});`,
|
|
408
|
+
`if (m.sectionMeta?.["site/sections/Theme.tsx"]?.renderJson !== false) {`,
|
|
409
|
+
` throw new Error("Theme renderJson:false missing from sectionMeta");`,
|
|
410
|
+
`}`,
|
|
411
|
+
`const fn = m.renderJsons?.["site/sections/ProductDetails.tsx"];`,
|
|
412
|
+
`if (typeof fn !== "function") throw new Error("ProductDetails renderJson fn missing");`,
|
|
413
|
+
`const out = fn({ storeConfig: 1, page: 2 });`,
|
|
414
|
+
`if (out.storeConfig !== undefined || out.page !== 2) throw new Error("projection fn wrong");`,
|
|
415
|
+
].join("\n"),
|
|
416
|
+
);
|
|
417
|
+
const importResult = cp.spawnSync("npx", ["tsx", checkerFile], { encoding: "utf8" });
|
|
418
|
+
expect(importResult.status, importResult.stderr).toBe(0);
|
|
419
|
+
}, 60_000);
|
|
420
|
+
});
|
|
421
|
+
|
|
339
422
|
describe("generate-sections output hygiene (non-registry)", () => {
|
|
340
423
|
let tmpDir: string;
|
|
341
424
|
let sectionsDir: string;
|
|
@@ -54,9 +54,19 @@ interface SectionMeta {
|
|
|
54
54
|
clientOnly?: boolean;
|
|
55
55
|
seo?: boolean;
|
|
56
56
|
hasLoadingFallback?: boolean;
|
|
57
|
+
renderJson?: false;
|
|
58
|
+
hasRenderJson?: boolean;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
const EXPORT_CONST_RE = /export\s+const\s+(eager|neverDefer|deferred|cache|layout|sync|clientOnly|seo)\s*=\s*(.+?)(?:;|\n)/g;
|
|
62
|
+
// renderJson (?renderJson mobile path). `= false` drops the section; a function
|
|
63
|
+
// (`export const renderJson = (props) => ...` / `export function renderJson`)
|
|
64
|
+
// projects its props. Kept separate from EXPORT_CONST_RE because the value is a
|
|
65
|
+
// literal `false` OR a function, not the `= true` booleans that regex handles.
|
|
66
|
+
const RENDER_JSON_FALSE_RE = /export\s+const\s+renderJson\s*=\s*false\b/;
|
|
67
|
+
const RENDER_JSON_FN_INLINE_RE =
|
|
68
|
+
/export\s+(?:function\s+renderJson\b|const\s+renderJson\s*=\s*(?!false\b)\S)/;
|
|
69
|
+
const RENDER_JSON_REEXPORT_RE = /export\s*\{[^}]*\brenderJson\b[^}]*\}/;
|
|
60
70
|
// Detects `export function LoadingFallback(...)`, `export const LoadingFallback = ...`, etc.
|
|
61
71
|
const LOADING_FALLBACK_INLINE_RE = /export\s+(?:function|const|let|var)\s+LoadingFallback\b/;
|
|
62
72
|
// Detects re-exports like:
|
|
@@ -96,6 +106,14 @@ function extractMeta(content: string): SectionMeta | null {
|
|
|
96
106
|
found = true;
|
|
97
107
|
}
|
|
98
108
|
|
|
109
|
+
if (RENDER_JSON_FALSE_RE.test(content)) {
|
|
110
|
+
meta.renderJson = false;
|
|
111
|
+
found = true;
|
|
112
|
+
} else if (RENDER_JSON_FN_INLINE_RE.test(content) || RENDER_JSON_REEXPORT_RE.test(content)) {
|
|
113
|
+
meta.hasRenderJson = true;
|
|
114
|
+
found = true;
|
|
115
|
+
}
|
|
116
|
+
|
|
99
117
|
return found ? meta : null;
|
|
100
118
|
}
|
|
101
119
|
|
|
@@ -168,6 +186,8 @@ const lines: string[] = [
|
|
|
168
186
|
"// export const clientOnly = true → skip SSR (client-only rendering)",
|
|
169
187
|
"// export const seo = true → SEO section (provides page head data)",
|
|
170
188
|
"// export function LoadingFallback → skeleton shown while section loads",
|
|
189
|
+
"// export const renderJson = false → drop section from ?renderJson (mobile)",
|
|
190
|
+
"// export const renderJson = (p)=> → project props for ?renderJson (mobile)",
|
|
171
191
|
"",
|
|
172
192
|
];
|
|
173
193
|
|
|
@@ -187,6 +207,16 @@ for (let i = 0; i < nonSyncFallbacks.length; i++) {
|
|
|
187
207
|
lines.push(`import { LoadingFallback as _fb${i} } from "${importPath}";`);
|
|
188
208
|
}
|
|
189
209
|
|
|
210
|
+
// renderJson projection-function imports — sections whose renderJson is a
|
|
211
|
+
// function (not `= false`) and aren't sync-imported.
|
|
212
|
+
const renderJsonEntries = entries.filter((e) => e.meta.hasRenderJson);
|
|
213
|
+
const nonSyncRenderJsons = renderJsonEntries.filter((e) => !e.meta.sync);
|
|
214
|
+
for (let i = 0; i < nonSyncRenderJsons.length; i++) {
|
|
215
|
+
const e = nonSyncRenderJsons[i];
|
|
216
|
+
const importPath = relativeImportPath(outFile, e.filePath);
|
|
217
|
+
lines.push(`import { renderJson as _rj${i} } from "${importPath}";`);
|
|
218
|
+
}
|
|
219
|
+
|
|
190
220
|
lines.push("");
|
|
191
221
|
|
|
192
222
|
// Metadata map
|
|
@@ -205,6 +235,8 @@ lines.push(" sync?: boolean;");
|
|
|
205
235
|
lines.push(" clientOnly?: boolean;");
|
|
206
236
|
lines.push(" seo?: boolean;");
|
|
207
237
|
lines.push(" hasLoadingFallback?: boolean;");
|
|
238
|
+
lines.push(" renderJson?: false;");
|
|
239
|
+
lines.push(" hasRenderJson?: boolean;");
|
|
208
240
|
lines.push("}");
|
|
209
241
|
lines.push("");
|
|
210
242
|
lines.push("export const sectionMeta: Record<string, SectionMetaEntry> = {");
|
|
@@ -248,6 +280,28 @@ if (allFallbacks.length > 0) {
|
|
|
248
280
|
}
|
|
249
281
|
lines.push("");
|
|
250
282
|
|
|
283
|
+
// renderJson projection-functions map (?renderJson mobile path). `= false`
|
|
284
|
+
// sections are NOT here — they carry `renderJson: false` in sectionMeta, which
|
|
285
|
+
// applySectionConventions reads directly. Only projection functions need a live
|
|
286
|
+
// reference. Typed `any` (like syncComponents) to avoid importing the RenderJson
|
|
287
|
+
// type into the generated file.
|
|
288
|
+
if (renderJsonEntries.length > 0) {
|
|
289
|
+
lines.push("export const renderJsons: Record<string, any> = {");
|
|
290
|
+
for (const e of renderJsonEntries) {
|
|
291
|
+
if (e.meta.sync) {
|
|
292
|
+
const syncIdx = syncEntries.indexOf(e);
|
|
293
|
+
lines.push(` "${e.key}": _sync${syncIdx}.renderJson,`);
|
|
294
|
+
} else {
|
|
295
|
+
const rjIdx = nonSyncRenderJsons.indexOf(e);
|
|
296
|
+
lines.push(` "${e.key}": _rj${rjIdx},`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
lines.push("};");
|
|
300
|
+
} else {
|
|
301
|
+
lines.push("export const renderJsons: Record<string, any> = {};");
|
|
302
|
+
}
|
|
303
|
+
lines.push("");
|
|
304
|
+
|
|
251
305
|
// Lazy section-import registry — opt-in via --registry, built from every
|
|
252
306
|
// scanned section file (not just convention-carrying `entries`).
|
|
253
307
|
if (EMIT_REGISTRY) {
|
|
@@ -103,7 +103,7 @@ import { createInstrumentedFetch } from "@decocms/blocks/sdk/instrumentedFetch";
|
|
|
103
103
|
import { initVtexFromBlocks, setVtexFetch } from "@decocms/apps-vtex";` : ""}${hasLocationMatcher ? `
|
|
104
104
|
import { registerLocationMatcher } from "./matchers/location";` : ""}
|
|
105
105
|
import { blocks as generatedBlocks } from "../.deco/blocks.gen";
|
|
106
|
-
import { sectionMeta, syncComponents, loadingFallbacks } from "../.deco/sections.gen";
|
|
106
|
+
import { sectionMeta, syncComponents, loadingFallbacks, renderJsons } from "../.deco/sections.gen";
|
|
107
107
|
import { PreviewProviders } from "@decocms/tanstack";
|
|
108
108
|
// @ts-ignore Vite ?url import
|
|
109
109
|
import appCss from "./styles/app.css?url";
|
|
@@ -140,6 +140,7 @@ applySectionConventions({
|
|
|
140
140
|
meta: sectionMeta,
|
|
141
141
|
syncComponents,
|
|
142
142
|
loadingFallbacks,
|
|
143
|
+
renderJsons,
|
|
143
144
|
sectionGlob: import.meta.glob("./sections/**/*.tsx") as Record<string, () => Promise<any>>,
|
|
144
145
|
});
|
|
145
146
|
|