@decocms/blocks-cli 7.34.2 → 7.34.4

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.34.2",
3
+ "version": "7.34.4",
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.2",
34
+ "@decocms/blocks": "7.34.4",
35
35
  "ts-morph": "^27.0.0",
36
36
  "tsx": "^4.22.5"
37
37
  },
@@ -95,10 +95,12 @@ describe("generate-loaders legacy artifact sync", () => {
95
95
  // New path must exist.
96
96
  expect(fs.existsSync(path.join(tmpDir, ".deco", "loaders.gen.ts"))).toBe(true);
97
97
 
98
- // Old file must be synced (not stale placeholder).
98
+ // Old file must be synced as a re-export shim (not the stale placeholder,
99
+ // and not a verbatim copy — a byte copy would carry `.deco/`-relative
100
+ // import paths that break at the deeper legacy location).
99
101
  const oldContent = fs.readFileSync(path.join(oldDir, "loaders.gen.ts"), "utf-8");
100
102
  expect(oldContent).not.toBe("// stale\n");
101
- expect(oldContent).toContain("site/loaders/related");
103
+ expect(oldContent).toContain('export * from "../../../.deco/loaders.gen"');
102
104
  }, 30_000);
103
105
 
104
106
  it("does not warn and does not sync when an explicit --out-file is passed", () => {
@@ -72,6 +72,22 @@ describe("generate-sections walkDir exclusions", () => {
72
72
  expect(generated).not.toContain("Hero.stories.tsx");
73
73
  expect(generated).not.toContain("sections.gen.ts");
74
74
  }, 30_000);
75
+
76
+ it("extracts `export const deferred = true` into sectionMeta", () => {
77
+ fs.writeFileSync(
78
+ path.join(sectionsDir, "HeavyPLP.tsx"),
79
+ "export const deferred = true;\nexport default function HeavyPLP() { return null; }\n",
80
+ );
81
+
82
+ const { code } = runGenerator(["--sections-dir", sectionsDir, "--out-file", outFile]);
83
+ expect(code).toBe(0);
84
+
85
+ const generated = fs.readFileSync(outFile, "utf-8");
86
+ // The per-section deferral flag must survive the regex scan → sectionMeta,
87
+ // or applySectionConventions can never register it as always-defer.
88
+ expect(generated).toMatch(/"site\/sections\/HeavyPLP\.tsx":\s*\{[^}]*deferred:\s*true/);
89
+ expect(generated).toContain("deferred?: boolean;");
90
+ }, 30_000);
75
91
  });
76
92
 
77
93
  describe("generate-sections default output path (.deco/)", () => {
@@ -119,9 +135,14 @@ describe("generate-sections default output path (.deco/)", () => {
119
135
  expect(fs.existsSync(newDefault)).toBe(true);
120
136
  expect(fs.readFileSync(newDefault, "utf-8")).toContain("site/sections/Hero.tsx");
121
137
 
122
- // Old file must be kept in sync so mid-migration imports still work.
138
+ // Old file is kept in sync as a re-export shim (NOT a verbatim copy — a
139
+ // byte copy would carry `.deco/`-relative import paths that break at the
140
+ // deeper legacy location). It re-exports the new file so mid-migration
141
+ // imports still resolve at any depth.
123
142
  const oldDefault = path.join(tmpDir, "src", "server", "cms", "sections.gen.ts");
124
- expect(fs.readFileSync(oldDefault, "utf-8")).toContain("site/sections/Hero.tsx");
143
+ const oldContent = fs.readFileSync(oldDefault, "utf-8");
144
+ expect(oldContent).toContain('export * from "../../../.deco/sections.gen"');
145
+ expect(oldContent).not.toContain("site/sections/Hero.tsx");
125
146
  }, 30_000);
126
147
 
127
148
  it("does not warn when an explicit --out-file is passed, even if the OLD default file exists", () => {
@@ -59,6 +59,7 @@ const EMIT_REGISTRY = args.includes("--registry");
59
59
  interface SectionMeta {
60
60
  eager?: boolean;
61
61
  neverDefer?: boolean;
62
+ deferred?: boolean;
62
63
  cache?: string;
63
64
  layout?: boolean;
64
65
  sync?: boolean;
@@ -67,7 +68,7 @@ interface SectionMeta {
67
68
  hasLoadingFallback?: boolean;
68
69
  }
69
70
 
70
- const EXPORT_CONST_RE = /export\s+const\s+(eager|neverDefer|cache|layout|sync|clientOnly|seo)\s*=\s*(.+?)(?:;|\n)/g;
71
+ const EXPORT_CONST_RE = /export\s+const\s+(eager|neverDefer|deferred|cache|layout|sync|clientOnly|seo)\s*=\s*(.+?)(?:;|\n)/g;
71
72
  // Detects `export function LoadingFallback(...)`, `export const LoadingFallback = ...`, etc.
72
73
  const LOADING_FALLBACK_INLINE_RE = /export\s+(?:function|const|let|var)\s+LoadingFallback\b/;
73
74
  // Detects re-exports like:
@@ -209,6 +210,7 @@ lines.push("");
209
210
  lines.push("export interface SectionMetaEntry {");
210
211
  lines.push(" eager?: boolean;");
211
212
  lines.push(" neverDefer?: boolean;");
213
+ lines.push(" deferred?: boolean;");
212
214
  lines.push(" cache?: string;");
213
215
  lines.push(" layout?: boolean;");
214
216
  lines.push(" sync?: boolean;");
@@ -23,12 +23,42 @@ export function warnLegacyArtifact(oldPath: string, newPath: string): void {
23
23
  }
24
24
 
25
25
  /**
26
- * Copies the freshly-generated file at `newPath` back to `oldPath` so that
27
- * sites mid-migration whose imports still point at the old location see
28
- * up-to-date output without requiring a full migration first.
29
- * Call this after generation completes, only when the old file was detected.
26
+ * Keeps the legacy `oldPath` in sync with the freshly-generated `newPath` so
27
+ * sites mid-migration whose imports still point at the old location keep
28
+ * working without a full migration first. Call this after generation
29
+ * completes, only when the old file was detected.
30
+ *
31
+ * For `.ts`/`.tsx` artifacts a verbatim copy would be broken: the generated
32
+ * file's import specifiers are relative to `newPath`'s directory (`.deco/`,
33
+ * one level deep), but `oldPath` lives deeper (`src/server/cms/`), so every
34
+ * `../src/...` specifier would resolve to a nonexistent path and crash the
35
+ * dev server on boot. Instead we write a re-export shim pointing at `newPath`
36
+ * — one specifier, correct at any depth, and self-documenting the deprecation.
37
+ * The generated `.gen.ts` files only have named exports, so `export *` forwards
38
+ * everything (including type exports). Non-TS artifacts (e.g. `.json` schema)
39
+ * have no import paths, so a byte copy is still correct.
30
40
  */
31
41
  export function syncLegacyArtifact(oldPath: string, newPath: string): void {
32
42
  fs.mkdirSync(path.dirname(oldPath), { recursive: true });
33
- fs.copyFileSync(newPath, oldPath);
43
+
44
+ if (!/\.tsx?$/.test(oldPath)) {
45
+ fs.copyFileSync(newPath, oldPath);
46
+ return;
47
+ }
48
+
49
+ const newRel = path.relative(process.cwd(), newPath).replace(/\\/g, "/");
50
+ let specifier = path
51
+ .relative(path.dirname(oldPath), newPath)
52
+ .replace(/\\/g, "/")
53
+ .replace(/\.tsx?$/, "");
54
+ if (!specifier.startsWith(".")) specifier = `./${specifier}`;
55
+
56
+ fs.writeFileSync(
57
+ oldPath,
58
+ `// AUTO-GENERATED — DO NOT EDIT.\n` +
59
+ `// Deprecated path: the generator now writes to "${newRel}".\n` +
60
+ `// This file only re-exports it so mid-migration imports keep working.\n` +
61
+ `// Update your imports to the new path, then delete this file.\n` +
62
+ `export * from "${specifier}";\n`,
63
+ );
34
64
  }
@@ -57,3 +57,34 @@ describe("scaffolded deco admin routes use HMR-safe factories", () => {
57
57
  });
58
58
  }
59
59
  });
60
+
61
+ /**
62
+ * Regression guard: the page renderers must wire `loadDeferredSectionFn` to the
63
+ * `deferredSectionLoader` shim, never the raw `loadDeferredSection` server fn.
64
+ * `loadDeferredSection` is a createServerFn with an inputValidator, so it must
65
+ * be called as `fn({ data })`; DeferredSectionWrapper calls `loadFn(bareArgs)`,
66
+ * so passing the raw fn leaves `ctx.data` undefined and the deferred section
67
+ * renders blank. The shim wraps the `{ data }` envelope.
68
+ */
69
+ describe("scaffolded page routes wire the deferred-loader shim, not the raw server fn", () => {
70
+ for (const platform of ["vtex", "custom"] as const) {
71
+ describe(`platform: ${platform}`, () => {
72
+ const files = generateRoutes(makeCtx(platform));
73
+
74
+ for (const file of ["src/routes/index.tsx", "src/routes/$.tsx"] as const) {
75
+ it(`${file} passes deferredSectionLoader from the sdk subpath`, () => {
76
+ const content = files[file];
77
+ expect(content, `${file} must be emitted`).toBeTypeOf("string");
78
+
79
+ expect(content).toContain(
80
+ 'import { deferredSectionLoader } from "@decocms/tanstack/sdk/deferredSectionLoader"',
81
+ );
82
+ expect(content).toContain("loadDeferredSectionFn={deferredSectionLoader}");
83
+
84
+ // The broken form: the raw server fn passed directly.
85
+ expect(content).not.toContain("loadDeferredSectionFn={loadDeferredSection}");
86
+ });
87
+ }
88
+ });
89
+ }
90
+ });
@@ -115,7 +115,8 @@ function RootLayout() {
115
115
 
116
116
  function generateIndex(ctx: MigrationContext, siteTitle: string): string {
117
117
  return `import { createFileRoute } from "@tanstack/react-router";
118
- import { cmsHomeRouteConfig, DecoPageRenderer, loadDeferredSection } from "@decocms/tanstack";
118
+ import { cmsHomeRouteConfig, DecoPageRenderer } from "@decocms/tanstack";
119
+ import { deferredSectionLoader } from "@decocms/tanstack/sdk/deferredSectionLoader";
119
120
 
120
121
  // MIGRATION TODO: customize defaultTitle / defaultDescription / fallback
121
122
  // copy below for ${siteTitle}. CMS \`Site.seo\` overrides these once block
@@ -151,7 +152,7 @@ function HomePage() {
151
152
  deferredPromises={data.deferredPromises}
152
153
  pagePath={data.pagePath}
153
154
  pageUrl={data.pageUrl}
154
- loadDeferredSectionFn={loadDeferredSection}
155
+ loadDeferredSectionFn={deferredSectionLoader}
155
156
  />
156
157
  );
157
158
  }
@@ -160,7 +161,8 @@ function HomePage() {
160
161
 
161
162
  function generateCatchAll(ctx: MigrationContext, siteTitle: string): string {
162
163
  return `import { createFileRoute } from "@tanstack/react-router";
163
- import { cmsRouteConfig, DecoPageRenderer, loadDeferredSection } from "@decocms/tanstack";
164
+ import { cmsRouteConfig, DecoPageRenderer } from "@decocms/tanstack";
165
+ import { deferredSectionLoader } from "@decocms/tanstack/sdk/deferredSectionLoader";
164
166
 
165
167
  // MIGRATION TODO: customize defaultTitle / defaultDescription for ${siteTitle}
166
168
  // (CMS \`Site.seo\` overrides these per-page once block resolution kicks in).
@@ -188,7 +190,7 @@ function CmsPage() {
188
190
  deferredPromises={data.deferredPromises}
189
191
  pagePath={data.pagePath}
190
192
  pageUrl={data.pageUrl}
191
- loadDeferredSectionFn={loadDeferredSection}
193
+ loadDeferredSectionFn={deferredSectionLoader}
192
194
  />
193
195
  );
194
196
  }