@decocms/blocks-cli 7.4.0 → 7.5.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.4.0",
3
+ "version": "7.5.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.4.0",
37
+ "@decocms/blocks": "7.5.0",
38
38
  "ts-morph": "^27.0.0",
39
39
  "tsx": "^4.19.0"
40
40
  },
@@ -14,7 +14,14 @@
14
14
  *
15
15
  * Env / CLI:
16
16
  * --blocks-dir override input (default: .deco/blocks)
17
- * --out-file override output (default: src/server/cms/blocks.gen.ts)
17
+ * --out-file override output (default: .deco/blocks.gen.ts, with a sibling
18
+ * .deco/blocks.gen.json — the .json path is always derived from
19
+ * --out-file by swapping its extension, so passing --out-file
20
+ * moves both artifacts together)
21
+ *
22
+ * If no `--out-file` is passed and the OLD default (src/server/cms/blocks.gen.ts)
23
+ * still exists on disk, a one-line legacy warning is printed to stderr and the
24
+ * NEW default is written anyway — see lib/legacyArtifact.ts.
18
25
  *
19
26
  * Programmatic:
20
27
  * import { generateBlocks } from "@decocms/blocks-cli/generate-blocks";
@@ -33,6 +40,7 @@ import {
33
40
  mergeCandidates,
34
41
  singleDecodeBlockName,
35
42
  } from "./lib/blocks-dedupe";
43
+ import { warnLegacyArtifact } from "./lib/legacyArtifact";
36
44
 
37
45
  const TS_STUB = [
38
46
  "// Auto-generated — thin wrapper around blocks.gen.json.",
@@ -291,7 +299,13 @@ if (isMainModule()) {
291
299
  };
292
300
 
293
301
  const blocksDir = path.resolve(process.cwd(), arg("blocks-dir", ".deco/blocks"));
294
- const outFile = path.resolve(process.cwd(), arg("out-file", "src/server/cms/blocks.gen.ts"));
302
+ const OUT_FILE_EXPLICIT = args.includes("--out-file");
303
+ const NEW_DEFAULT_OUT_FILE = ".deco/blocks.gen.ts";
304
+ const OLD_DEFAULT_OUT_FILE = "src/server/cms/blocks.gen.ts";
305
+ const outFile = path.resolve(process.cwd(), arg("out-file", NEW_DEFAULT_OUT_FILE));
306
+ if (!OUT_FILE_EXPLICIT && fs.existsSync(path.resolve(process.cwd(), OLD_DEFAULT_OUT_FILE))) {
307
+ warnLegacyArtifact(OLD_DEFAULT_OUT_FILE, NEW_DEFAULT_OUT_FILE);
308
+ }
295
309
 
296
310
  generateBlocks({ blocksDir, outFile }).catch((err) => {
297
311
  console.error(err);
@@ -11,6 +11,16 @@
11
11
  * This script generates an equivalent file where each server function is a
12
12
  * top-level const, which the compiler can correctly transform into RPC stubs.
13
13
  *
14
+ * Unlike the other four generators (generate-blocks, generate-loaders,
15
+ * generate-sections, generate-schema), this one's default output stays under
16
+ * `src/`, not `.deco/`. The other four emit inert framework artifacts (data
17
+ * snapshots / metadata) that the framework loads at runtime — those belong
18
+ * next to the rest of the framework's own generated state. This file emits
19
+ * actual app server-function code (`createServerFn().handler()` calls) that
20
+ * TanStack Start's own build-time compiler transforms into RPC stubs; it's
21
+ * compiled and shipped as part of the site's application code, so it lives
22
+ * in `src/` alongside the code it's compiled with, not in `.deco/`.
23
+ *
14
24
  * Usage (from site root):
15
25
  * npx tsx node_modules/@decocms/blocks-cli/scripts/generate-invoke.ts
16
26
  *
@@ -34,13 +34,18 @@
34
34
  * CLI:
35
35
  * --loaders-dir override loaders input (default: src/loaders)
36
36
  * --actions-dir override actions input (default: src/actions)
37
- * --out-file override output (default: src/server/cms/loaders.gen.ts)
37
+ * --out-file override output (default: .deco/loaders.gen.ts)
38
38
  * --exclude comma-separated list of loader keys to skip (they have custom wiring)
39
39
  * --prune-by-decofile only emit entries whose key appears as `__resolveType` in any JSON
40
40
  * --decofile-dir @deprecated alias for --prune-by-decofile
41
+ *
42
+ * If no `--out-file` is passed and the OLD default (src/server/cms/loaders.gen.ts)
43
+ * still exists on disk, a one-line legacy warning is printed to stderr and the
44
+ * NEW default is written anyway — see lib/legacyArtifact.ts.
41
45
  */
42
46
  import fs from "node:fs";
43
47
  import path from "node:path";
48
+ import { warnLegacyArtifact } from "./lib/legacyArtifact";
44
49
 
45
50
  const args = process.argv.slice(2);
46
51
  function arg(name: string, fallback: string): string {
@@ -50,7 +55,13 @@ function arg(name: string, fallback: string): string {
50
55
 
51
56
  const loadersDir = path.resolve(process.cwd(), arg("loaders-dir", "src/loaders"));
52
57
  const actionsDir = path.resolve(process.cwd(), arg("actions-dir", "src/actions"));
53
- const outFile = path.resolve(process.cwd(), arg("out-file", "src/server/cms/loaders.gen.ts"));
58
+ const OUT_FILE_EXPLICIT = args.includes("--out-file");
59
+ const NEW_DEFAULT_OUT_FILE = ".deco/loaders.gen.ts";
60
+ const OLD_DEFAULT_OUT_FILE = "src/server/cms/loaders.gen.ts";
61
+ const outFile = path.resolve(process.cwd(), arg("out-file", NEW_DEFAULT_OUT_FILE));
62
+ if (!OUT_FILE_EXPLICIT && fs.existsSync(path.resolve(process.cwd(), OLD_DEFAULT_OUT_FILE))) {
63
+ warnLegacyArtifact(OLD_DEFAULT_OUT_FILE, NEW_DEFAULT_OUT_FILE);
64
+ }
54
65
  const excludeRaw = arg("exclude", "");
55
66
  const excludeSet = new Set(excludeRaw.split(",").map((s) => s.trim()).filter(Boolean));
56
67
 
@@ -1,5 +1,9 @@
1
+ import * as cp from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
1
5
  import { Project } from "ts-morph";
2
- import { describe, expect, it } from "vitest";
6
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
3
7
  import {
4
8
  WIDGET_TYPE_FORMATS,
5
9
  applyWidgetFormat,
@@ -95,3 +99,95 @@ describe("typeToJsonSchema with an unresolvable widget alias import", () => {
95
99
  });
96
100
  });
97
101
  });
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Default output path (.deco/) + legacy warning — subprocess, mirrors the
105
+ // pattern in generate-sections.test.ts. generate-schema.ts IS guarded by
106
+ // isMainModule(), but it's still driven as a subprocess here so the CLI's
107
+ // argv-parsed OUT_REL/legacy-check top-level code runs against a real cwd.
108
+ // ---------------------------------------------------------------------------
109
+
110
+ const SCRIPT = path.resolve(__dirname, "generate-schema.ts");
111
+
112
+ function runGenerator(
113
+ args: string[],
114
+ opts: { cwd?: string } = {},
115
+ ): { stdout: string; stderr: string; code: number } {
116
+ const r = cp.spawnSync("npx", ["tsx", SCRIPT, ...args], { encoding: "utf8", cwd: opts.cwd });
117
+ return { stdout: r.stdout || "", stderr: r.stderr || "", code: r.status ?? 0 };
118
+ }
119
+
120
+ describe("generate-schema default output path (.deco/)", () => {
121
+ let tmpDir: string;
122
+
123
+ beforeEach(() => {
124
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "generate-schema-defaults-"));
125
+ fs.writeFileSync(
126
+ path.join(tmpDir, "tsconfig.json"),
127
+ JSON.stringify({
128
+ compilerOptions: {
129
+ target: "ES2020",
130
+ module: "ESNext",
131
+ moduleResolution: "Bundler",
132
+ jsx: "react-jsx",
133
+ skipLibCheck: true,
134
+ strict: true,
135
+ },
136
+ }),
137
+ );
138
+ const sectionsDir = path.join(tmpDir, "src", "sections");
139
+ fs.mkdirSync(sectionsDir, { recursive: true });
140
+ fs.writeFileSync(
141
+ path.join(sectionsDir, "Hero.tsx"),
142
+ [
143
+ "export interface Props {",
144
+ " title: string;",
145
+ "}",
146
+ "export default function Hero(props: Props) { return null; }",
147
+ ].join("\n"),
148
+ );
149
+ });
150
+
151
+ afterEach(() => {
152
+ fs.rmSync(tmpDir, { recursive: true, force: true });
153
+ });
154
+
155
+ it("writes to .deco/meta.gen.json when no --out flag is passed", () => {
156
+ const { code } = runGenerator(["--skip-apps"], { cwd: tmpDir });
157
+ expect(code).toBe(0);
158
+
159
+ const newDefault = path.join(tmpDir, ".deco", "meta.gen.json");
160
+ expect(fs.existsSync(newDefault)).toBe(true);
161
+ const meta = JSON.parse(fs.readFileSync(newDefault, "utf-8"));
162
+ expect(meta.manifest.blocks.sections).toHaveProperty("site/sections/Hero.tsx");
163
+ });
164
+
165
+ it("warns once to stderr naming both paths when the OLD default file exists and no --out is passed, but still writes the NEW default", () => {
166
+ const oldDefaultDir = path.join(tmpDir, "src", "server", "admin");
167
+ fs.mkdirSync(oldDefaultDir, { recursive: true });
168
+ fs.writeFileSync(path.join(oldDefaultDir, "meta.gen.json"), "{}");
169
+
170
+ const { code, stderr } = runGenerator(["--skip-apps"], { cwd: tmpDir });
171
+ expect(code).toBe(0);
172
+
173
+ expect(stderr).toContain("src/server/admin/meta.gen.json");
174
+ expect(stderr).toContain(".deco/meta.gen.json");
175
+ expect(stderr).toContain("Move the file and update its importers");
176
+
177
+ const newDefault = path.join(tmpDir, ".deco", "meta.gen.json");
178
+ expect(fs.existsSync(newDefault)).toBe(true);
179
+ });
180
+
181
+ it("does not warn when an explicit --out is passed, even if the OLD default file exists", () => {
182
+ const oldDefaultDir = path.join(tmpDir, "src", "server", "admin");
183
+ fs.mkdirSync(oldDefaultDir, { recursive: true });
184
+ fs.writeFileSync(path.join(oldDefaultDir, "meta.gen.json"), "{}");
185
+
186
+ const explicitOut = path.join(tmpDir, "custom", "meta.gen.json");
187
+ const { code, stderr } = runGenerator(["--skip-apps", "--out", explicitOut], { cwd: tmpDir });
188
+ expect(code).toBe(0);
189
+
190
+ expect(stderr).not.toContain("Generator default output moved");
191
+ expect(fs.existsSync(explicitOut)).toBe(true);
192
+ });
193
+ });
@@ -20,8 +20,12 @@ import { fileURLToPath } from "node:url";
20
20
  * --loaders Loaders directory (default: "src/loaders")
21
21
  * --apps Apps directory (default: "src/apps")
22
22
  * --skip-apps Skip app schema generation
23
- * --out Output file (default: "src/server/admin/meta.gen.json")
23
+ * --out Output file (default: ".deco/meta.gen.json")
24
24
  * --platform Platform name (default: "cloudflare")
25
+ *
26
+ * If no `--out` is passed and the OLD default (src/server/admin/meta.gen.json)
27
+ * still exists on disk, a one-line legacy warning is printed to stderr and the
28
+ * NEW default is written anyway — see lib/legacyArtifact.ts.
25
29
  */
26
30
  import {
27
31
  type Symbol as MorphSymbol,
@@ -32,6 +36,7 @@ import {
32
36
  type Type,
33
37
  } from "ts-morph";
34
38
  import { isExcludedCodegenFile } from "./lib/codegenExclusions";
39
+ import { warnLegacyArtifact } from "./lib/legacyArtifact";
35
40
 
36
41
  // ---------------------------------------------------------------------------
37
42
  // CLI arg parsing
@@ -49,8 +54,14 @@ const SECTIONS_REL = arg("sections", "src/sections");
49
54
  const LOADERS_REL = arg("loaders", "src/loaders");
50
55
  const APPS_REL = arg("apps", "src/apps");
51
56
  const SKIP_APPS = argv.includes("--skip-apps");
52
- const OUT_REL = arg("out", "src/server/admin/meta.gen.json");
57
+ const OUT_FILE_EXPLICIT = argv.includes("--out");
58
+ const NEW_DEFAULT_OUT_REL = ".deco/meta.gen.json";
59
+ const OLD_DEFAULT_OUT_REL = "src/server/admin/meta.gen.json";
60
+ const OUT_REL = arg("out", NEW_DEFAULT_OUT_REL);
53
61
  const PLATFORM = arg("platform", "cloudflare");
62
+ if (!OUT_FILE_EXPLICIT && fs.existsSync(path.resolve(process.cwd(), OLD_DEFAULT_OUT_REL))) {
63
+ warnLegacyArtifact(OLD_DEFAULT_OUT_REL, NEW_DEFAULT_OUT_REL);
64
+ }
54
65
 
55
66
  // ---------------------------------------------------------------------------
56
67
  // Interfaces
@@ -21,8 +21,11 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
21
21
 
22
22
  const SCRIPT = path.resolve(__dirname, "generate-sections.ts");
23
23
 
24
- function runGenerator(args: string[]): { stdout: string; stderr: string; code: number } {
25
- const r = cp.spawnSync("npx", ["tsx", SCRIPT, ...args], { encoding: "utf8" });
24
+ function runGenerator(
25
+ args: string[],
26
+ opts: { cwd?: string } = {},
27
+ ): { stdout: string; stderr: string; code: number } {
28
+ const r = cp.spawnSync("npx", ["tsx", SCRIPT, ...args], { encoding: "utf8", cwd: opts.cwd });
26
29
  return { stdout: r.stdout || "", stderr: r.stderr || "", code: r.status ?? 0 };
27
30
  }
28
31
 
@@ -71,6 +74,66 @@ describe("generate-sections walkDir exclusions", () => {
71
74
  });
72
75
  });
73
76
 
77
+ describe("generate-sections default output path (.deco/)", () => {
78
+ let tmpDir: string;
79
+ let sectionsDir: string;
80
+
81
+ beforeEach(() => {
82
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "generate-sections-defaults-"));
83
+ sectionsDir = path.join(tmpDir, "src", "sections");
84
+ fs.mkdirSync(sectionsDir, { recursive: true });
85
+ fs.writeFileSync(
86
+ path.join(sectionsDir, "Hero.tsx"),
87
+ "export const eager = true;\nexport default function Hero() { return null; }\n",
88
+ );
89
+ });
90
+
91
+ afterEach(() => {
92
+ fs.rmSync(tmpDir, { recursive: true, force: true });
93
+ });
94
+
95
+ it("writes to .deco/sections.gen.ts when no --out-file flag is passed", () => {
96
+ const { code, stderr } = runGenerator([], { cwd: tmpDir });
97
+ expect(code).toBe(0);
98
+
99
+ const newDefault = path.join(tmpDir, ".deco", "sections.gen.ts");
100
+ expect(fs.existsSync(newDefault)).toBe(true);
101
+ expect(fs.readFileSync(newDefault, "utf-8")).toContain("site/sections/Hero.tsx");
102
+ // No legacy file present, so no warning is expected.
103
+ expect(stderr).not.toContain("Generator default output moved");
104
+ });
105
+
106
+ it("warns once to stderr naming both paths when the OLD default file exists and no --out-file is passed, but still writes the NEW default", () => {
107
+ const oldDefaultDir = path.join(tmpDir, "src", "server", "cms");
108
+ fs.mkdirSync(oldDefaultDir, { recursive: true });
109
+ fs.writeFileSync(path.join(oldDefaultDir, "sections.gen.ts"), "// stale\n");
110
+
111
+ const { code, stderr } = runGenerator([], { cwd: tmpDir });
112
+ expect(code).toBe(0);
113
+
114
+ expect(stderr).toContain("src/server/cms/sections.gen.ts");
115
+ expect(stderr).toContain(".deco/sections.gen.ts");
116
+ expect(stderr).toContain("Move the file and update its importers");
117
+
118
+ const newDefault = path.join(tmpDir, ".deco", "sections.gen.ts");
119
+ expect(fs.existsSync(newDefault)).toBe(true);
120
+ expect(fs.readFileSync(newDefault, "utf-8")).toContain("site/sections/Hero.tsx");
121
+ });
122
+
123
+ it("does not warn when an explicit --out-file is passed, even if the OLD default file exists", () => {
124
+ const oldDefaultDir = path.join(tmpDir, "src", "server", "cms");
125
+ fs.mkdirSync(oldDefaultDir, { recursive: true });
126
+ fs.writeFileSync(path.join(oldDefaultDir, "sections.gen.ts"), "// stale\n");
127
+
128
+ const explicitOut = path.join(tmpDir, "custom", "sections.gen.ts");
129
+ const { code, stderr } = runGenerator(["--out-file", explicitOut], { cwd: tmpDir });
130
+ expect(code).toBe(0);
131
+
132
+ expect(stderr).not.toContain("Generator default output moved");
133
+ expect(fs.existsSync(explicitOut)).toBe(true);
134
+ });
135
+ });
136
+
74
137
  describe("generate-sections --registry", () => {
75
138
  let tmpDir: string;
76
139
  let sectionsDir: string;
@@ -16,17 +16,22 @@
16
16
  *
17
17
  * CLI:
18
18
  * --sections-dir override input (default: src/sections)
19
- * --out-file override output (default: src/server/cms/sections.gen.ts)
19
+ * --out-file override output (default: .deco/sections.gen.ts)
20
20
  * --registry also emit `sectionImports` — a lazy section-import map
21
21
  * keyed glob-style (`./sections/...`), the Next.js/webpack
22
22
  * equivalent of Vite's `import.meta.glob("./sections/**\/*.tsx")`.
23
23
  * Built from every scanned section file, not just the ones
24
24
  * carrying convention exports. Off by default so existing
25
25
  * Vite sites regenerating sections.gen.ts in CI see zero diff.
26
+ *
27
+ * If no `--out-file` is passed and the OLD default (src/server/cms/sections.gen.ts)
28
+ * still exists on disk, a one-line legacy warning is printed to stderr and the
29
+ * NEW default is written anyway — see lib/legacyArtifact.ts.
26
30
  */
27
31
  import fs from "node:fs";
28
32
  import path from "node:path";
29
33
  import { isExcludedCodegenFile } from "./lib/codegenExclusions";
34
+ import { warnLegacyArtifact } from "./lib/legacyArtifact";
30
35
 
31
36
  const args = process.argv.slice(2);
32
37
  function arg(name: string, fallback: string): string {
@@ -35,7 +40,13 @@ function arg(name: string, fallback: string): string {
35
40
  }
36
41
 
37
42
  const sectionsDir = path.resolve(process.cwd(), arg("sections-dir", "src/sections"));
38
- const outFile = path.resolve(process.cwd(), arg("out-file", "src/server/cms/sections.gen.ts"));
43
+ const OUT_FILE_EXPLICIT = args.includes("--out-file");
44
+ const NEW_DEFAULT_OUT_FILE = ".deco/sections.gen.ts";
45
+ const OLD_DEFAULT_OUT_FILE = "src/server/cms/sections.gen.ts";
46
+ const outFile = path.resolve(process.cwd(), arg("out-file", NEW_DEFAULT_OUT_FILE));
47
+ if (!OUT_FILE_EXPLICIT && fs.existsSync(path.resolve(process.cwd(), OLD_DEFAULT_OUT_FILE))) {
48
+ warnLegacyArtifact(OLD_DEFAULT_OUT_FILE, NEW_DEFAULT_OUT_FILE);
49
+ }
39
50
  const EMIT_REGISTRY = args.includes("--registry");
40
51
 
41
52
  interface SectionMeta {
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Generator default output paths flipped from `src/server/{cms,admin}/` to
3
+ * `.deco/` (framework artifacts live in the framework's folder, not mixed
4
+ * into app source). Sites that never pass an explicit `--out`/`--out-file`
5
+ * flag pick up the new default silently — except when the OLD default file
6
+ * is still sitting on disk, which almost always means something (an
7
+ * importer, a `.gitignore` entry, a stale CI cache check) still points at
8
+ * it. In that case we warn once, to stderr, and then write to the NEW
9
+ * default anyway: the artifact is regenerated code, so there's no reason to
10
+ * block the run — the warning is just a nudge to go clean up the stale file
11
+ * and its importers.
12
+ *
13
+ * An explicit flag means the caller made a deliberate choice about where
14
+ * output goes; it gets no warning and no guard.
15
+ */
16
+ export function warnLegacyArtifact(oldPath: string, newPath: string): void {
17
+ console.warn(
18
+ `[deco] Generator default output moved: ${oldPath} -> ${newPath}. Move the file and update its importers.`,
19
+ );
20
+ }
@@ -64,7 +64,7 @@ export function generateCommerceLoaders(ctx: MigrationContext): string {
64
64
  }
65
65
 
66
66
  // Always import siteLoaders — the generate:loaders script creates this file
67
- lines.push(`import { siteLoaders } from "../server/cms/loaders.gen";`);
67
+ lines.push(`import { siteLoaders } from "../../.deco/loaders.gen";`);
68
68
  lines.push(``);
69
69
 
70
70
  lines.push(`const DOMAIN_RE = /;\\s*domain=[^;]*/gi;`);
@@ -12,7 +12,6 @@ const config: KnipConfig = {
12
12
  project: ["src/**/*.{ts,tsx}"],
13
13
  ignore: [
14
14
  "src/server/invoke.gen.ts",
15
- "src/server/cms/blocks.gen.ts",
16
15
  "src/routeTree.gen.ts",
17
16
  ],
18
17
  ignoreDependencies: [
@@ -99,8 +99,8 @@ import { setInvokeLoaders } from "@decocms/start/admin";${isVtex ? `
99
99
  import { createInstrumentedFetch } from "@decocms/start/sdk/instrumentedFetch";
100
100
  import { initVtexFromBlocks, setVtexFetch } from "@decocms/apps/vtex";` : ""}${hasLocationMatcher ? `
101
101
  import { registerLocationMatcher } from "./matchers/location";` : ""}
102
- import { blocks as generatedBlocks } from "./server/cms/blocks.gen";
103
- import { sectionMeta, syncComponents, loadingFallbacks } from "./server/cms/sections.gen";
102
+ import { blocks as generatedBlocks } from "../.deco/blocks.gen";
103
+ import { sectionMeta, syncComponents, loadingFallbacks } from "../.deco/sections.gen";
104
104
  import { PreviewProviders } from "@decocms/start/hooks";
105
105
  // @ts-ignore Vite ?url import
106
106
  import appCss from "./styles/app.css?url";
@@ -112,7 +112,7 @@ import "./setup/section-loaders";
112
112
  createSiteSetup({
113
113
  sections: import.meta.glob("./sections/**/*.tsx") as Record<string, () => Promise<any>>,
114
114
  blocks: generatedBlocks,
115
- meta: () => import("./server/admin/meta.gen.json").then((m) => m.default),
115
+ meta: () => import("../.deco/meta.gen.json").then((m) => m.default),
116
116
  css: appCss,
117
117
  fonts: [${fontEntries}],
118
118
  productionOrigins: [