@decocms/blocks-cli 7.31.7 → 7.32.1

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.31.7",
3
+ "version": "7.32.1",
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.31.7",
34
+ "@decocms/blocks": "7.32.1",
35
35
  "ts-morph": "^27.0.0",
36
36
  "tsx": "^4.22.5"
37
37
  },
@@ -1,9 +1,22 @@
1
+ import * as cp from "node:child_process";
2
+ import * as fs from "node:fs";
1
3
  import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import * as os from "node:os";
2
5
  import { tmpdir } from "node:os";
3
6
  import path from "node:path";
4
7
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
8
  import { readBlockDelta } from "./generate-blocks";
6
9
 
10
+ const SCRIPT = path.resolve(__dirname, "generate-blocks.ts");
11
+
12
+ function runGenerator(
13
+ args: string[],
14
+ opts: { cwd?: string } = {},
15
+ ): { stdout: string; stderr: string; code: number } {
16
+ const r = cp.spawnSync("npx", ["tsx", SCRIPT, ...args], { encoding: "utf8", cwd: opts.cwd });
17
+ return { stdout: r.stdout || "", stderr: r.stderr || "", code: r.status ?? 0 };
18
+ }
19
+
7
20
  describe("readBlockDelta", () => {
8
21
  let dir: string;
9
22
 
@@ -92,3 +105,66 @@ describe("readBlockDelta", () => {
92
105
  expect(delta).toEqual({});
93
106
  });
94
107
  });
108
+
109
+ describe("generate-blocks legacy artifact sync", () => {
110
+ let tmpDir: string;
111
+
112
+ beforeEach(() => {
113
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "generate-blocks-legacy-"));
114
+ // Minimal .deco/blocks/ dir with one block so output is non-empty.
115
+ fs.mkdirSync(path.join(tmpDir, ".deco", "blocks"), { recursive: true });
116
+ fs.writeFileSync(
117
+ path.join(tmpDir, ".deco", "blocks", "pages-Home.json"),
118
+ JSON.stringify({ path: "/" }),
119
+ );
120
+ });
121
+
122
+ afterEach(() => {
123
+ fs.rmSync(tmpDir, { recursive: true, force: true });
124
+ });
125
+
126
+ it("syncs .ts and .json to the old path when legacy file is detected", () => {
127
+ const oldDir = path.join(tmpDir, "src", "server", "cms");
128
+ fs.mkdirSync(oldDir, { recursive: true });
129
+ fs.writeFileSync(path.join(oldDir, "blocks.gen.ts"), "// stale\n");
130
+
131
+ const { code, stderr } = runGenerator([], { cwd: tmpDir });
132
+ expect(code).toBe(0);
133
+
134
+ expect(stderr).toContain("src/server/cms/blocks.gen.ts");
135
+ expect(stderr).toContain(".deco/blocks.gen.ts");
136
+ expect(stderr).toContain("Update importers to use the new path");
137
+
138
+ // New path must exist.
139
+ expect(fs.existsSync(path.join(tmpDir, ".deco", "blocks.gen.ts"))).toBe(true);
140
+ expect(fs.existsSync(path.join(tmpDir, ".deco", "blocks.gen.json"))).toBe(true);
141
+
142
+ // Old .ts must be synced (stub content, not the stale placeholder).
143
+ const oldTs = fs.readFileSync(path.join(oldDir, "blocks.gen.ts"), "utf-8");
144
+ expect(oldTs).not.toBe("// stale\n");
145
+
146
+ // Old .json sibling must be synced — the Vite plugin reads it by suffix.
147
+ const oldJson = path.join(oldDir, "blocks.gen.json");
148
+ expect(fs.existsSync(oldJson)).toBe(true);
149
+ const oldContent = JSON.parse(fs.readFileSync(oldJson, "utf-8"));
150
+ const newContent = JSON.parse(
151
+ fs.readFileSync(path.join(tmpDir, ".deco", "blocks.gen.json"), "utf-8"),
152
+ );
153
+ expect(oldContent).toEqual(newContent);
154
+ }, 30_000);
155
+
156
+ it("does not warn and does not sync when an explicit --out-file is passed", () => {
157
+ const oldDir = path.join(tmpDir, "src", "server", "cms");
158
+ fs.mkdirSync(oldDir, { recursive: true });
159
+ fs.writeFileSync(path.join(oldDir, "blocks.gen.ts"), "// stale\n");
160
+
161
+ const explicitOut = path.join(tmpDir, "custom", "blocks.gen.ts");
162
+ const { code, stderr } = runGenerator(["--out-file", explicitOut], { cwd: tmpDir });
163
+ expect(code).toBe(0);
164
+
165
+ expect(stderr).not.toContain("Generator default output moved");
166
+ expect(fs.existsSync(explicitOut)).toBe(true);
167
+ // Old file must be untouched.
168
+ expect(fs.readFileSync(path.join(oldDir, "blocks.gen.ts"), "utf-8")).toBe("// stale\n");
169
+ }, 30_000);
170
+ });
@@ -49,7 +49,7 @@ import {
49
49
  singleDecodeBlockName,
50
50
  } from "./lib/blocks-dedupe";
51
51
  import { buildCsvRedirectBlocks } from "./lib/csv-redirects";
52
- import { warnLegacyArtifact } from "./lib/legacyArtifact";
52
+ import { syncLegacyArtifact, warnLegacyArtifact } from "./lib/legacyArtifact";
53
53
 
54
54
  const TS_STUB = [
55
55
  "// Auto-generated — thin wrapper around blocks.gen.json.",
@@ -325,11 +325,20 @@ if (isMainModule()) {
325
325
  const NEW_DEFAULT_OUT_FILE = ".deco/blocks.gen.ts";
326
326
  const OLD_DEFAULT_OUT_FILE = "src/server/cms/blocks.gen.ts";
327
327
  const outFile = path.resolve(process.cwd(), arg("out-file", NEW_DEFAULT_OUT_FILE));
328
- if (!OUT_FILE_EXPLICIT && fs.existsSync(path.resolve(process.cwd(), OLD_DEFAULT_OUT_FILE))) {
328
+ const oldFilePath = path.resolve(process.cwd(), OLD_DEFAULT_OUT_FILE);
329
+ const hasLegacy = !OUT_FILE_EXPLICIT && fs.existsSync(oldFilePath);
330
+ if (hasLegacy) {
329
331
  warnLegacyArtifact(OLD_DEFAULT_OUT_FILE, NEW_DEFAULT_OUT_FILE);
330
332
  }
331
333
 
332
- generateBlocks({ blocksDir, outFile }).catch((err) => {
334
+ generateBlocks({ blocksDir, outFile }).then((result) => {
335
+ if (hasLegacy) {
336
+ syncLegacyArtifact(oldFilePath, result.outFile);
337
+ // The .ts is a Vite stub — the plugin reads the .json sibling by suffix.
338
+ // Sync the JSON too so the Vite interceptor at the old path finds it.
339
+ syncLegacyArtifact(oldFilePath.replace(/\.ts$/, ".json"), result.jsonFile);
340
+ }
341
+ }).catch((err) => {
333
342
  console.error(err);
334
343
  process.exit(1);
335
344
  });
@@ -14,8 +14,8 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest";
14
14
 
15
15
  const SCRIPT = path.resolve(__dirname, "generate-loaders.ts");
16
16
 
17
- function run(cwd: string): { stdout: string; stderr: string; code: number } {
18
- const r = cp.spawnSync("npx", ["tsx", SCRIPT], { encoding: "utf8", cwd });
17
+ function run(cwd: string, args: string[] = []): { stdout: string; stderr: string; code: number } {
18
+ const r = cp.spawnSync("npx", ["tsx", SCRIPT, ...args], { encoding: "utf8", cwd });
19
19
  return { stdout: r.stdout || "", stderr: r.stderr || "", code: r.status ?? -1 };
20
20
  }
21
21
 
@@ -62,3 +62,56 @@ describe("generate-loaders — loader vs action emit", () => {
62
62
  expect(out).not.toContain('createLoaderEntry("site/actions/addToCart"');
63
63
  });
64
64
  });
65
+
66
+ describe("generate-loaders legacy artifact sync", () => {
67
+ let tmpDir: string;
68
+
69
+ beforeEach(() => {
70
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "generate-loaders-legacy-"));
71
+ const write = (rel: string, content: string) => {
72
+ const abs = path.join(tmpDir, rel);
73
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
74
+ fs.writeFileSync(abs, content);
75
+ };
76
+ write("src/loaders/related.ts", "export default async () => [];\n");
77
+ });
78
+
79
+ afterEach(() => {
80
+ fs.rmSync(tmpDir, { recursive: true, force: true });
81
+ });
82
+
83
+ it("syncs to the old path when legacy file is detected", () => {
84
+ const oldDir = path.join(tmpDir, "src", "server", "cms");
85
+ fs.mkdirSync(oldDir, { recursive: true });
86
+ fs.writeFileSync(path.join(oldDir, "loaders.gen.ts"), "// stale\n");
87
+
88
+ const { code, stderr } = run(tmpDir);
89
+ expect(code).toBe(0);
90
+
91
+ expect(stderr).toContain("src/server/cms/loaders.gen.ts");
92
+ expect(stderr).toContain(".deco/loaders.gen.ts");
93
+ expect(stderr).toContain("Update importers to use the new path");
94
+
95
+ // New path must exist.
96
+ expect(fs.existsSync(path.join(tmpDir, ".deco", "loaders.gen.ts"))).toBe(true);
97
+
98
+ // Old file must be synced (not stale placeholder).
99
+ const oldContent = fs.readFileSync(path.join(oldDir, "loaders.gen.ts"), "utf-8");
100
+ expect(oldContent).not.toBe("// stale\n");
101
+ expect(oldContent).toContain("site/loaders/related");
102
+ }, 30_000);
103
+
104
+ it("does not warn and does not sync when an explicit --out-file is passed", () => {
105
+ const oldDir = path.join(tmpDir, "src", "server", "cms");
106
+ fs.mkdirSync(oldDir, { recursive: true });
107
+ fs.writeFileSync(path.join(oldDir, "loaders.gen.ts"), "// stale\n");
108
+
109
+ const explicitOut = path.join(tmpDir, "custom", "loaders.gen.ts");
110
+ const { code, stderr } = run(tmpDir, ["--out-file", explicitOut]);
111
+ expect(code).toBe(0);
112
+
113
+ expect(stderr).not.toContain("Generator default output moved");
114
+ expect(fs.existsSync(explicitOut)).toBe(true);
115
+ expect(fs.readFileSync(path.join(oldDir, "loaders.gen.ts"), "utf-8")).toBe("// stale\n");
116
+ }, 30_000);
117
+ });
@@ -50,7 +50,7 @@
50
50
  */
51
51
  import fs from "node:fs";
52
52
  import path from "node:path";
53
- import { warnLegacyArtifact } from "./lib/legacyArtifact";
53
+ import { syncLegacyArtifact, warnLegacyArtifact } from "./lib/legacyArtifact";
54
54
 
55
55
  const args = process.argv.slice(2);
56
56
  function arg(name: string, fallback: string): string {
@@ -64,7 +64,9 @@ const OUT_FILE_EXPLICIT = args.includes("--out-file");
64
64
  const NEW_DEFAULT_OUT_FILE = ".deco/loaders.gen.ts";
65
65
  const OLD_DEFAULT_OUT_FILE = "src/server/cms/loaders.gen.ts";
66
66
  const outFile = path.resolve(process.cwd(), arg("out-file", NEW_DEFAULT_OUT_FILE));
67
- if (!OUT_FILE_EXPLICIT && fs.existsSync(path.resolve(process.cwd(), OLD_DEFAULT_OUT_FILE))) {
67
+ const oldLoadersPath = path.resolve(process.cwd(), OLD_DEFAULT_OUT_FILE);
68
+ const hasLegacy = !OUT_FILE_EXPLICIT && fs.existsSync(oldLoadersPath);
69
+ if (hasLegacy) {
68
70
  warnLegacyArtifact(OLD_DEFAULT_OUT_FILE, NEW_DEFAULT_OUT_FILE);
69
71
  }
70
72
  const excludeRaw = arg("exclude", "");
@@ -254,6 +256,7 @@ lines.push("");
254
256
 
255
257
  fs.mkdirSync(path.dirname(outFile), { recursive: true });
256
258
  fs.writeFileSync(outFile, lines.join("\n"));
259
+ if (hasLegacy) syncLegacyArtifact(oldLoadersPath, outFile);
257
260
 
258
261
  const filterNote = cmsReferences
259
262
  ? ` (filtered against ${cmsReferences.size} CMS __resolveType references; pruned ${prunedCount} dead entries)`
@@ -323,10 +323,17 @@ describe("generate-schema default output path (.deco/)", () => {
323
323
 
324
324
  expect(stderr).toContain("src/server/admin/meta.gen.json");
325
325
  expect(stderr).toContain(".deco/meta.gen.json");
326
- expect(stderr).toContain("Move the file and update its importers");
326
+ expect(stderr).toContain("Update importers to use the new path");
327
327
 
328
328
  const newDefault = path.join(tmpDir, ".deco", "meta.gen.json");
329
329
  expect(fs.existsSync(newDefault)).toBe(true);
330
+
331
+ // Old file must be kept in sync so mid-migration imports still work.
332
+ const oldDefault = path.join(tmpDir, "src", "server", "admin", "meta.gen.json");
333
+ expect(fs.existsSync(oldDefault)).toBe(true);
334
+ const oldContent = JSON.parse(fs.readFileSync(oldDefault, "utf-8"));
335
+ const newContent = JSON.parse(fs.readFileSync(newDefault, "utf-8"));
336
+ expect(oldContent).toEqual(newContent);
330
337
  }, 30_000);
331
338
 
332
339
  it("does not warn when an explicit --out is passed, even if the OLD default file exists", () => {
@@ -47,7 +47,7 @@ import {
47
47
  type Type,
48
48
  } from "ts-morph";
49
49
  import { isExcludedCodegenFile } from "./lib/codegenExclusions";
50
- import { warnLegacyArtifact } from "./lib/legacyArtifact";
50
+ import { syncLegacyArtifact, warnLegacyArtifact } from "./lib/legacyArtifact";
51
51
 
52
52
  // ---------------------------------------------------------------------------
53
53
  // CLI arg parsing
@@ -90,6 +90,8 @@ let COMPOSE = false;
90
90
  // Value written to the composed meta's `framework` field (only used with
91
91
  // --compose). Defaults to composeMeta's historical "tanstack-start".
92
92
  let FRAMEWORK = "tanstack-start";
93
+ let _hasLegacySchema = false;
94
+ let _oldSchemaPath = "";
93
95
 
94
96
  if (isMainModule()) {
95
97
  SITE_NAMESPACE = arg("namespace", SITE_NAMESPACE);
@@ -104,7 +106,9 @@ if (isMainModule()) {
104
106
  PLATFORM = arg("platform", PLATFORM);
105
107
  COMPOSE = argv.includes("--compose");
106
108
  FRAMEWORK = arg("framework", FRAMEWORK);
107
- if (!outFileExplicit && fs.existsSync(path.resolve(process.cwd(), OLD_DEFAULT_OUT_REL))) {
109
+ _oldSchemaPath = path.resolve(process.cwd(), OLD_DEFAULT_OUT_REL);
110
+ _hasLegacySchema = !outFileExplicit && fs.existsSync(_oldSchemaPath);
111
+ if (_hasLegacySchema) {
108
112
  warnLegacyArtifact(OLD_DEFAULT_OUT_REL, NEW_DEFAULT_OUT_REL);
109
113
  }
110
114
  }
@@ -1537,6 +1541,7 @@ if (isMainModule()) {
1537
1541
  const outPath = path.resolve(process.cwd(), OUT_REL);
1538
1542
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
1539
1543
  fs.writeFileSync(outPath, JSON.stringify(meta, null, 2));
1544
+ if (_hasLegacySchema) syncLegacyArtifact(_oldSchemaPath, outPath);
1540
1545
 
1541
1546
  const defCount = Object.keys(meta.schema.definitions).length;
1542
1547
  const secCount = Object.keys(meta.manifest.blocks.sections || {}).length;
@@ -113,11 +113,15 @@ describe("generate-sections default output path (.deco/)", () => {
113
113
 
114
114
  expect(stderr).toContain("src/server/cms/sections.gen.ts");
115
115
  expect(stderr).toContain(".deco/sections.gen.ts");
116
- expect(stderr).toContain("Move the file and update its importers");
116
+ expect(stderr).toContain("Update importers to use the new path");
117
117
 
118
118
  const newDefault = path.join(tmpDir, ".deco", "sections.gen.ts");
119
119
  expect(fs.existsSync(newDefault)).toBe(true);
120
120
  expect(fs.readFileSync(newDefault, "utf-8")).toContain("site/sections/Hero.tsx");
121
+
122
+ // Old file must be kept in sync so mid-migration imports still work.
123
+ const oldDefault = path.join(tmpDir, "src", "server", "cms", "sections.gen.ts");
124
+ expect(fs.readFileSync(oldDefault, "utf-8")).toContain("site/sections/Hero.tsx");
121
125
  }, 30_000);
122
126
 
123
127
  it("does not warn when an explicit --out-file is passed, even if the OLD default file exists", () => {
@@ -36,7 +36,7 @@
36
36
  import fs from "node:fs";
37
37
  import path from "node:path";
38
38
  import { isExcludedCodegenFile } from "./lib/codegenExclusions";
39
- import { warnLegacyArtifact } from "./lib/legacyArtifact";
39
+ import { syncLegacyArtifact, warnLegacyArtifact } from "./lib/legacyArtifact";
40
40
 
41
41
  const args = process.argv.slice(2);
42
42
  function arg(name: string, fallback: string): string {
@@ -49,7 +49,9 @@ const OUT_FILE_EXPLICIT = args.includes("--out-file");
49
49
  const NEW_DEFAULT_OUT_FILE = ".deco/sections.gen.ts";
50
50
  const OLD_DEFAULT_OUT_FILE = "src/server/cms/sections.gen.ts";
51
51
  const outFile = path.resolve(process.cwd(), arg("out-file", NEW_DEFAULT_OUT_FILE));
52
- if (!OUT_FILE_EXPLICIT && fs.existsSync(path.resolve(process.cwd(), OLD_DEFAULT_OUT_FILE))) {
52
+ const oldSectionsPath = path.resolve(process.cwd(), OLD_DEFAULT_OUT_FILE);
53
+ const hasLegacy = !OUT_FILE_EXPLICIT && fs.existsSync(oldSectionsPath);
54
+ if (hasLegacy) {
53
55
  warnLegacyArtifact(OLD_DEFAULT_OUT_FILE, NEW_DEFAULT_OUT_FILE);
54
56
  }
55
57
  const EMIT_REGISTRY = args.includes("--registry");
@@ -291,6 +293,7 @@ fs.writeFileSync(
291
293
  outFile,
292
294
  lines.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n*$/, "\n"),
293
295
  );
296
+ if (hasLegacy) syncLegacyArtifact(oldSectionsPath, outFile);
294
297
 
295
298
  console.log(
296
299
  `Generated section metadata for ${entries.length} sections → ${path.relative(process.cwd(), outFile)}`,
@@ -1,3 +1,6 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
1
4
  /**
2
5
  * Generator default output paths flipped from `src/server/{cms,admin}/` to
3
6
  * `.deco/` (framework artifacts live in the framework's folder, not mixed
@@ -15,6 +18,17 @@
15
18
  */
16
19
  export function warnLegacyArtifact(oldPath: string, newPath: string): void {
17
20
  console.warn(
18
- `[deco] Generator default output moved: ${oldPath} -> ${newPath}. Move the file and update its importers.`,
21
+ `[deco] Generator default output moved: ${oldPath} -> ${newPath}. Update importers to use the new path, then delete the old file.`,
19
22
  );
20
23
  }
24
+
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.
30
+ */
31
+ export function syncLegacyArtifact(oldPath: string, newPath: string): void {
32
+ fs.mkdirSync(path.dirname(oldPath), { recursive: true });
33
+ fs.copyFileSync(newPath, oldPath);
34
+ }