@decocms/start 2.6.0 → 2.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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: deco-migrate-script
3
- description: Automated migration script that converts Deco storefronts from Fresh/Preact/Deno to TanStack Start/React/Cloudflare Workers. Runs 7 phases (analyze, scaffold, transform, cleanup, report, verify, bootstrap). Use when running the migration script, debugging its output, extending it with new transforms, or understanding what it does. Located at scripts/migrate.ts in @decocms/start.
3
+ description: Automated migration script that converts Deco storefronts from Fresh/Preact/Deno to TanStack Start/React/Cloudflare Workers. Runs 8 phases (analyze, scaffold, transform, cleanup, report, verify, bootstrap, compile). Use when running the migration script, debugging its output, extending it with new transforms, or understanding what it does. Located at scripts/migrate.ts in @decocms/start.
4
4
  globs:
5
5
  - "scripts/migrate.ts"
6
6
  - "scripts/migrate/**/*"
@@ -27,8 +27,13 @@ npx tsx node_modules/@decocms/start/scripts/migrate.ts --source /path/to/old-sit
27
27
  | `--source <dir>` | Source site directory (default: `.`) |
28
28
  | `--dry-run` | Preview changes without writing files |
29
29
  | `--verbose` | Show detailed per-file output |
30
+ | `--strict` | Promote post-bootstrap typecheck/build failures from warnings to errors (exit 2) |
31
+ | `--with-build` | After typecheck, also run `npx vite build` for full runtime validation (slower) |
32
+ | `--no-compile` | Skip the post-bootstrap compile phase entirely |
30
33
  | `--help` | Show help |
31
34
 
35
+ **CI usage:** pair `--strict` with `--with-build` to catch both type and runtime regressions before merge.
36
+
32
37
  ## Architecture
33
38
 
34
39
  ```
@@ -42,6 +47,7 @@ scripts/migrate/
42
47
  ├── phase-cleanup.ts ← Phase 4: delete old artifacts
43
48
  ├── phase-report.ts ← Phase 5: generate MIGRATION_REPORT.md
44
49
  ├── phase-verify.ts ← Phase 6: smoke tests
50
+ ├── phase-compile.ts ← Phase 8: post-bootstrap tsc/vite-build
45
51
  ├── transforms/ ← Transform modules (applied in order)
46
52
  │ ├── imports.ts ← 70+ import rewriting rules
47
53
  │ ├── jsx.ts ← JSX attribute fixes
@@ -264,6 +270,26 @@ Runs automatically after all phases (skipped in `--dry-run`):
264
270
  2. `npx tsx node_modules/@decocms/start/scripts/generate-blocks.ts`
265
271
  3. `npx tsr generate`
266
272
 
273
+ ### Phase 8: Compile
274
+
275
+ Runs automatically after bootstrap (skipped in `--dry-run` or with `--no-compile`):
276
+
277
+ 1. `npx tsc --noEmit` — surfaces any typecheck regression introduced by the
278
+ transform pipeline. Output is captured and printed (truncated to ~50 lines)
279
+ so users can see the actual TypeScript diagnostics.
280
+ 2. `npx vite build` — only when `--with-build` is passed. Catches
281
+ runtime-only issues that escape typecheck (missing exports, broken
282
+ barrel files, server/client boundary violations).
283
+
284
+ Failures are warnings by default — the migration completes and tells the
285
+ user to inspect the diagnostics. With `--strict`, failures abort with
286
+ exit code `2` so CI can fail the pipeline.
287
+
288
+ Skipped automatically when `node_modules/` is missing (e.g. bootstrap
289
+ failed before install). This is the gate that catches regressions like
290
+ `#105 TS5097` (rewriter leaving `.ts` extensions in imports) or dead
291
+ shim references that escape the static `phase-verify` checks.
292
+
267
293
  ## Key Design Decisions
268
294
 
269
295
  ### MigrationContext (types.ts)
@@ -136,7 +136,7 @@ See: `references/deco-framework/README.md`
136
136
 
137
137
  **Actions**:
138
138
  1. `from "apps/commerce/types.ts"` → `from "@decocms/apps/commerce/types"`
139
- 2. `from "apps/admin/widgets.ts"` → `from "~/types/widgets"` (create local file)
139
+ 2. `from "apps/admin/widgets.ts"` → `from "@decocms/start/types/widgets"` (framework owns the string aliases — do **not** create a local `src/types/widgets.ts`)
140
140
  3. `from "apps/website/components/Image.tsx"` → `from "~/components/ui/Image"` (create local)
141
141
  4. SDK utilities: `~/sdk/useOffer` → `@decocms/apps/commerce/sdk/useOffer`, etc.
142
142
 
@@ -135,7 +135,38 @@ utility). Your runtime behavior gets MUCH better — segment cookies, IS
135
135
  cookies, VTEX session auth all start working again instead of being
136
136
  silently stubbed to `{}` / `null`.
137
137
 
138
- ## 6. Search for orphan `TODO: move into framework` comments
138
+ ## 6. Drop `src/types/widgets.ts` framework owns it
139
+
140
+ Older migrations scaffold a local `src/types/widgets.ts` containing 8
141
+ string-aliased widget types (`ImageWidget`, `HTMLWidget`, …). The
142
+ framework now exports the same set (plus `TextArea`) at
143
+ `@decocms/start/types/widgets`, and the schema generator detects the
144
+ widgets via type-text matching, so the local file is purely
145
+ duplicated boilerplate.
146
+
147
+ ```bash
148
+ # Quick check
149
+ rg -n "from ['\"]~/types/widgets['\"]" src/ | wc -l # >0 → cleanup applies
150
+ ```
151
+
152
+ Replace all imports in one pass:
153
+
154
+ ```bash
155
+ # macOS / BSD sed: drop the empty quotes after -i
156
+ rg -l "from ['\"]~/types/widgets['\"]" src/ \
157
+ | xargs sed -i '' "s|from ['\"]~/types/widgets['\"]|from \"@decocms/start/types/widgets\"|g"
158
+ ```
159
+
160
+ Then delete the now-orphan local file:
161
+
162
+ ```bash
163
+ rm src/types/widgets.ts
164
+ ```
165
+
166
+ Confirm `tsc --noEmit` is still clean — the framework version is a
167
+ strict superset of what the migration script generated.
168
+
169
+ ## 7. Search for orphan `TODO: move into framework` comments
139
170
 
140
171
  Real sites accumulate `TODO` comments like `// TODO: move into decoVitePlugin
141
172
  in next @decocms/start release`. These are roadmap items the framework
@@ -152,7 +183,7 @@ For each hit, decide:
152
183
 
153
184
  ## Verification checklist
154
185
 
155
- After completing 1-6:
186
+ After completing 1-7:
156
187
 
157
188
  - [ ] `npm run typecheck` baseline matches pre-cleanup count (no new errors)
158
189
  - [ ] `npm run dev` starts and `/`, `/some-pdp/p`, `/s?q=foo` all render
@@ -218,7 +218,7 @@ See: `references/deco-framework/README.md`
218
218
 
219
219
  **Actions**:
220
220
  1. `from "apps/commerce/types.ts"` → `from "@decocms/apps/commerce/types"`
221
- 2. `from "apps/admin/widgets.ts"` → `from "~/types/widgets"` (create local file with string aliases)
221
+ 2. `from "apps/admin/widgets.ts"` → `from "@decocms/start/types/widgets"` (framework owns the string aliases — `ImageWidget`, `HTMLWidget`, etc.; do **not** create a local `src/types/widgets.ts`)
222
222
  3. `from "apps/website/components/Image.tsx"` → `from "~/components/ui/Image"` (create local components)
223
223
  4. SDK utilities: `~/sdk/useOffer` → `@decocms/apps/commerce/sdk/useOffer`, `~/sdk/format` → `@decocms/apps/commerce/sdk/formatPrice`, etc.
224
224
 
@@ -135,7 +135,38 @@ utility). Your runtime behavior gets MUCH better — segment cookies, IS
135
135
  cookies, VTEX session auth all start working again instead of being
136
136
  silently stubbed to `{}` / `null`.
137
137
 
138
- ## 6. Search for orphan `TODO: move into framework` comments
138
+ ## 6. Drop `src/types/widgets.ts` framework owns it
139
+
140
+ Older migrations scaffold a local `src/types/widgets.ts` containing 8
141
+ string-aliased widget types (`ImageWidget`, `HTMLWidget`, …). The
142
+ framework now exports the same set (plus `TextArea`) at
143
+ `@decocms/start/types/widgets`, and the schema generator detects the
144
+ widgets via type-text matching, so the local file is purely
145
+ duplicated boilerplate.
146
+
147
+ ```bash
148
+ # Quick check
149
+ rg -n "from ['\"]~/types/widgets['\"]" src/ | wc -l # >0 → cleanup applies
150
+ ```
151
+
152
+ Replace all imports in one pass:
153
+
154
+ ```bash
155
+ # macOS / BSD sed: drop the empty quotes after -i
156
+ rg -l "from ['\"]~/types/widgets['\"]" src/ \
157
+ | xargs sed -i '' "s|from ['\"]~/types/widgets['\"]|from \"@decocms/start/types/widgets\"|g"
158
+ ```
159
+
160
+ Then delete the now-orphan local file:
161
+
162
+ ```bash
163
+ rm src/types/widgets.ts
164
+ ```
165
+
166
+ Confirm `tsc --noEmit` is still clean — the framework version is a
167
+ strict superset of what the migration script generated.
168
+
169
+ ## 7. Search for orphan `TODO: move into framework` comments
139
170
 
140
171
  Real sites accumulate `TODO` comments like `// TODO: move into decoVitePlugin
141
172
  in next @decocms/start release`. These are roadmap items the framework
@@ -152,7 +183,7 @@ For each hit, decide:
152
183
 
153
184
  ## Verification checklist
154
185
 
155
- After completing 1-6:
186
+ After completing 1-7:
156
187
 
157
188
  - [ ] `npm run typecheck` baseline matches pre-cleanup count (no new errors)
158
189
  - [ ] `npm run dev` starts and `/`, `/some-pdp/p`, `/s?q=foo` all render
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/start",
3
- "version": "2.6.0",
3
+ "version": "2.8.0",
4
4
  "type": "module",
5
5
  "description": "Deco framework for TanStack Start - CMS bridge, admin protocol, hooks, schema generation",
6
6
  "main": "./src/index.ts",
@@ -0,0 +1,193 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { _truncateForTesting, compile } from "./phase-compile";
6
+ import type { CompileRunResult } from "./phase-compile";
7
+ import type { MigrationContext } from "./types";
8
+
9
+ function makeCtx(sourceDir: string, dryRun = false): MigrationContext {
10
+ return {
11
+ sourceDir,
12
+ siteName: "test-site",
13
+ platform: "vtex",
14
+ vtexAccount: null,
15
+ gtmId: null,
16
+ importMap: {},
17
+ discoveredNpmDeps: {},
18
+ themeColors: {},
19
+ fontFamily: null,
20
+ files: [],
21
+ sectionMetas: [],
22
+ islandClassifications: [],
23
+ islandWrapperTargets: new Map(),
24
+ loaderInventory: [],
25
+ scaffoldedFiles: [],
26
+ transformedFiles: [],
27
+ deletedFiles: [],
28
+ movedFiles: [],
29
+ manualReviewItems: [],
30
+ frameworkFindings: [],
31
+ dryRun,
32
+ verbose: false,
33
+ };
34
+ }
35
+
36
+ function fakeRunner(
37
+ responses: Record<string, CompileRunResult>,
38
+ ): {
39
+ calls: string[];
40
+ runner: (cmd: string, cwd: string) => CompileRunResult;
41
+ } {
42
+ const calls: string[] = [];
43
+ const runner = (cmd: string, _cwd: string): CompileRunResult => {
44
+ calls.push(cmd);
45
+ return responses[cmd] ?? { ok: true };
46
+ };
47
+ return { calls, runner };
48
+ }
49
+
50
+ describe("compile (phase 8)", () => {
51
+ let tmpDir: string;
52
+ let logSpy: ReturnType<typeof vi.spyOn>;
53
+
54
+ beforeEach(() => {
55
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "compile-test-"));
56
+ logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
57
+ });
58
+
59
+ afterEach(() => {
60
+ fs.rmSync(tmpDir, { recursive: true, force: true });
61
+ logSpy.mockRestore();
62
+ });
63
+
64
+ it("is a no-op in dry-run mode", () => {
65
+ const ctx = makeCtx(tmpDir, true);
66
+ const { runner, calls } = fakeRunner({});
67
+ const result = compile(ctx, { runner });
68
+ expect(result.skipped).toBe(true);
69
+ expect(result.typecheck.ran).toBe(false);
70
+ expect(result.shouldFail).toBe(false);
71
+ expect(calls).toEqual([]);
72
+ });
73
+
74
+ it("skips when node_modules/ is missing", () => {
75
+ const ctx = makeCtx(tmpDir);
76
+ const { runner, calls } = fakeRunner({});
77
+ const result = compile(ctx, { runner });
78
+ expect(result.skipped).toBe(true);
79
+ expect(result.typecheck.ran).toBe(false);
80
+ expect(calls).toEqual([]);
81
+ });
82
+
83
+ it("runs typecheck when node_modules/ exists and reports success", () => {
84
+ fs.mkdirSync(path.join(tmpDir, "node_modules"), { recursive: true });
85
+ const ctx = makeCtx(tmpDir);
86
+ const { runner, calls } = fakeRunner({
87
+ "npx tsc --noEmit": { ok: true },
88
+ });
89
+ const result = compile(ctx, { runner });
90
+ expect(result.skipped).toBe(false);
91
+ expect(result.typecheck.ran).toBe(true);
92
+ expect(result.typecheck.passed).toBe(true);
93
+ expect(result.shouldFail).toBe(false);
94
+ expect(calls).toEqual(["npx tsc --noEmit"]);
95
+ });
96
+
97
+ it("captures tsc failure output and emits a warning by default", () => {
98
+ fs.mkdirSync(path.join(tmpDir, "node_modules"), { recursive: true });
99
+ const ctx = makeCtx(tmpDir);
100
+ const tscOutput = "src/foo.ts(10,5): error TS2322: Type mismatch.";
101
+ const { runner } = fakeRunner({
102
+ "npx tsc --noEmit": { ok: false, output: tscOutput },
103
+ });
104
+ const result = compile(ctx, { runner });
105
+ expect(result.typecheck.passed).toBe(false);
106
+ expect(result.typecheck.output).toBe(tscOutput);
107
+ // In default (non-strict) mode, failure does not abort the migration.
108
+ expect(result.shouldFail).toBe(false);
109
+ });
110
+
111
+ it("promotes tsc failure to abort in --strict mode", () => {
112
+ fs.mkdirSync(path.join(tmpDir, "node_modules"), { recursive: true });
113
+ const ctx = makeCtx(tmpDir);
114
+ const { runner } = fakeRunner({
115
+ "npx tsc --noEmit": { ok: false, output: "TS error" },
116
+ });
117
+ const result = compile(ctx, { runner, strict: true });
118
+ expect(result.typecheck.passed).toBe(false);
119
+ expect(result.shouldFail).toBe(true);
120
+ });
121
+
122
+ it("runs vite build only when --with-build is passed", () => {
123
+ fs.mkdirSync(path.join(tmpDir, "node_modules"), { recursive: true });
124
+ const ctx = makeCtx(tmpDir);
125
+ const { runner, calls } = fakeRunner({
126
+ "npx tsc --noEmit": { ok: true },
127
+ "npx vite build": { ok: true },
128
+ });
129
+ const result = compile(ctx, { runner, withBuild: true });
130
+ expect(calls).toEqual(["npx tsc --noEmit", "npx vite build"]);
131
+ expect(result.build.ran).toBe(true);
132
+ expect(result.build.passed).toBe(true);
133
+ });
134
+
135
+ it("does not run vite build by default", () => {
136
+ fs.mkdirSync(path.join(tmpDir, "node_modules"), { recursive: true });
137
+ const ctx = makeCtx(tmpDir);
138
+ const { runner, calls } = fakeRunner({
139
+ "npx tsc --noEmit": { ok: true },
140
+ });
141
+ const result = compile(ctx, { runner });
142
+ expect(calls).toEqual(["npx tsc --noEmit"]);
143
+ expect(result.build.ran).toBe(false);
144
+ });
145
+
146
+ it("aborts in strict mode when build fails even if typecheck passes", () => {
147
+ fs.mkdirSync(path.join(tmpDir, "node_modules"), { recursive: true });
148
+ const ctx = makeCtx(tmpDir);
149
+ const { runner } = fakeRunner({
150
+ "npx tsc --noEmit": { ok: true },
151
+ "npx vite build": { ok: false, output: "Vite build error" },
152
+ });
153
+ const result = compile(ctx, {
154
+ runner,
155
+ withBuild: true,
156
+ strict: true,
157
+ });
158
+ expect(result.typecheck.passed).toBe(true);
159
+ expect(result.build.passed).toBe(false);
160
+ expect(result.shouldFail).toBe(true);
161
+ });
162
+
163
+ it("still runs build after a tsc failure (strict mode aborts at the end)", () => {
164
+ fs.mkdirSync(path.join(tmpDir, "node_modules"), { recursive: true });
165
+ const ctx = makeCtx(tmpDir);
166
+ const { runner, calls } = fakeRunner({
167
+ "npx tsc --noEmit": { ok: false, output: "TS error" },
168
+ "npx vite build": { ok: true },
169
+ });
170
+ const result = compile(ctx, { runner, withBuild: true });
171
+ expect(calls).toEqual(["npx tsc --noEmit", "npx vite build"]);
172
+ expect(result.typecheck.passed).toBe(false);
173
+ expect(result.build.passed).toBe(true);
174
+ });
175
+ });
176
+
177
+ describe("truncate helper", () => {
178
+ it("returns input unchanged when within line limit", () => {
179
+ const input = "line1\nline2\nline3";
180
+ expect(_truncateForTesting(input, 10)).toBe(input);
181
+ });
182
+
183
+ it("truncates output exceeding the line limit", () => {
184
+ const lines = Array.from({ length: 100 }, (_, i) => `line${i}`);
185
+ const out = _truncateForTesting(lines.join("\n"), 50);
186
+ const outLines = out.split("\n");
187
+ // 50 kept lines + 1 truncation marker
188
+ expect(outLines).toHaveLength(51);
189
+ expect(outLines[0]).toBe("line0");
190
+ expect(outLines[49]).toBe("line49");
191
+ expect(outLines[50]).toContain("50 more lines truncated");
192
+ });
193
+ });
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Phase 8 — Compile.
3
+ *
4
+ * After scaffolding, transforming, and bootstrapping the migrated site,
5
+ * actually run the TypeScript compiler (and optionally a full Vite build) to
6
+ * catch regressions that escaped the static `phase-verify` checks.
7
+ *
8
+ * This is the gate that would have caught past migration regressions like:
9
+ * - #105 TS5097 (rewriter leaving `.ts` extensions in imports)
10
+ * - the dead `src/lib/*` shims that broke `phase-cleanup`
11
+ * - import-rewrite gaps where `from "apps/foo"` silently became `from ""`
12
+ *
13
+ * Behavior:
14
+ * - Default: typecheck only (`tsc --noEmit`). Failures are surfaced as
15
+ * warnings — the migration completes, but the user sees the diagnostics.
16
+ * - `--strict`: failures abort the migration with a non-zero exit code so
17
+ * CI can fail.
18
+ * - `--with-build`: also runs `npx vite build` after typecheck. Slower
19
+ * (~30-90s) but catches runtime-only issues like missing exports.
20
+ *
21
+ * No-ops in dry-run mode.
22
+ */
23
+
24
+ import { execSync } from "node:child_process";
25
+ import * as fs from "node:fs";
26
+ import * as path from "node:path";
27
+ import { gray, green, red, yellow } from "./colors";
28
+ import type { MigrationContext } from "./types";
29
+ import { logPhase } from "./types";
30
+
31
+ export type CompileRunResult =
32
+ | { ok: true }
33
+ | { ok: false; output: string };
34
+
35
+ export interface CompileOptions {
36
+ /** Promote typecheck/build failures from warnings to errors (exit code 2). */
37
+ strict?: boolean;
38
+ /** Also run `npx vite build` after typecheck. */
39
+ withBuild?: boolean;
40
+ /**
41
+ * Internal: override the command runner. Tests inject a stub here; the
42
+ * default uses `child_process.execSync`.
43
+ */
44
+ runner?: (cmd: string, cwd: string) => CompileRunResult;
45
+ }
46
+
47
+ export interface CompileResult {
48
+ skipped: boolean;
49
+ typecheck: { ran: boolean; passed: boolean; output?: string };
50
+ build: { ran: boolean; passed: boolean; output?: string };
51
+ /** True iff compile decided the migration should fail (only in strict mode). */
52
+ shouldFail: boolean;
53
+ }
54
+
55
+ const MAX_OUTPUT_LINES = 50;
56
+
57
+ function truncate(output: string, maxLines: number): string {
58
+ const lines = output.split("\n");
59
+ if (lines.length <= maxLines) return output;
60
+ return [
61
+ ...lines.slice(0, maxLines),
62
+ gray(` … (${lines.length - maxLines} more lines truncated)`),
63
+ ].join("\n");
64
+ }
65
+
66
+ /** Default command runner — uses execSync. Overridable for tests. */
67
+ function defaultRunner(cmd: string, cwd: string): CompileRunResult {
68
+ try {
69
+ execSync(cmd, { cwd, stdio: "pipe" });
70
+ return { ok: true };
71
+ } catch (e: unknown) {
72
+ const err = e as { stdout?: Buffer; stderr?: Buffer; message?: string };
73
+ const stdout = err.stdout?.toString() ?? "";
74
+ const stderr = err.stderr?.toString() ?? "";
75
+ const output = (stdout + stderr).trim() || err.message || "Unknown error";
76
+ return { ok: false, output };
77
+ }
78
+ }
79
+
80
+ /** Exported for testing. */
81
+ export { truncate as _truncateForTesting };
82
+
83
+ /**
84
+ * Run the post-bootstrap compile checks.
85
+ *
86
+ * Returns `{ shouldFail }` so the caller (migrate.ts) can decide whether to
87
+ * exit non-zero. We don't `process.exit()` ourselves so this stays unit-testable.
88
+ */
89
+ export function compile(
90
+ ctx: MigrationContext,
91
+ opts: CompileOptions = {},
92
+ ): CompileResult {
93
+ logPhase("Compile (TypeScript + optional build)");
94
+
95
+ const result: CompileResult = {
96
+ skipped: false,
97
+ typecheck: { ran: false, passed: false },
98
+ build: { ran: false, passed: false },
99
+ shouldFail: false,
100
+ };
101
+
102
+ if (ctx.dryRun) {
103
+ console.log(" Skipping compile in dry-run mode\n");
104
+ result.skipped = true;
105
+ return result;
106
+ }
107
+
108
+ // Compile relies on `node_modules/`. If bootstrap was skipped or failed
109
+ // before the install step we have nothing to typecheck — bail with a
110
+ // clear message rather than exploding.
111
+ const nodeModules = path.join(ctx.sourceDir, "node_modules");
112
+ if (!fs.existsSync(nodeModules)) {
113
+ console.log(
114
+ ` ${yellow("⚠")} Skipping compile — node_modules/ missing (bootstrap likely failed)\n`,
115
+ );
116
+ result.skipped = true;
117
+ return result;
118
+ }
119
+
120
+ const runner = opts.runner ?? defaultRunner;
121
+
122
+ // Typecheck.
123
+ console.log(" Running: TypeScript typecheck (npx tsc --noEmit)...");
124
+ const tsc = runner("npx tsc --noEmit", ctx.sourceDir);
125
+ result.typecheck.ran = true;
126
+ result.typecheck.passed = tsc.ok;
127
+
128
+ if (tsc.ok) {
129
+ console.log(` ${green("✓")} Typecheck passed`);
130
+ } else {
131
+ const output = truncate(tsc.output, MAX_OUTPUT_LINES);
132
+ result.typecheck.output = tsc.output;
133
+ const icon = opts.strict ? red("✗") : yellow("⚠");
134
+ console.log(` ${icon} Typecheck failed:\n`);
135
+ console.log(output);
136
+ console.log("");
137
+ }
138
+
139
+ // Optional Vite build (heavier, opt-in).
140
+ if (opts.withBuild) {
141
+ console.log(" Running: Vite build (npx vite build)...");
142
+ const vite = runner("npx vite build", ctx.sourceDir);
143
+ result.build.ran = true;
144
+ result.build.passed = vite.ok;
145
+
146
+ if (vite.ok) {
147
+ console.log(` ${green("✓")} Build passed`);
148
+ } else {
149
+ const output = truncate(vite.output, MAX_OUTPUT_LINES);
150
+ result.build.output = vite.output;
151
+ const icon = opts.strict ? red("✗") : yellow("⚠");
152
+ console.log(` ${icon} Build failed:\n`);
153
+ console.log(output);
154
+ console.log("");
155
+ }
156
+ }
157
+
158
+ // Decide whether to fail the migration.
159
+ const anyFailed =
160
+ (result.typecheck.ran && !result.typecheck.passed) ||
161
+ (result.build.ran && !result.build.passed);
162
+
163
+ if (anyFailed && opts.strict) {
164
+ result.shouldFail = true;
165
+ console.log(
166
+ ` ${red("Compile failed in strict mode — migration aborted.")}\n`,
167
+ );
168
+ } else if (anyFailed) {
169
+ console.log(
170
+ ` ${yellow("Compile completed with errors.")} Re-run with ${gray("--strict")} in CI to fail the build.\n`,
171
+ );
172
+ } else {
173
+ console.log(` ${green("Compile passed.")}\n`);
174
+ }
175
+
176
+ return result;
177
+ }
@@ -30,7 +30,9 @@ const REQUIRED_FILES = [
30
30
  "src/hooks/useCart.ts",
31
31
  "src/hooks/useUser.ts",
32
32
  "src/hooks/useWishlist.ts",
33
- "src/types/widgets.ts",
33
+ // src/types/widgets.ts intentionally omitted — provided by the
34
+ // framework at `@decocms/start/types/widgets`; sites no longer
35
+ // shadow the file locally.
34
36
  "src/types/deco.ts",
35
37
  "src/types/commerce-app.ts",
36
38
  "src/components/ui/Image.tsx",
@@ -442,7 +444,8 @@ const checks: Check[] = [
442
444
  severity: "warning",
443
445
  fn: (ctx) => {
444
446
  const typeFiles = [
445
- "src/types/widgets.ts",
447
+ // widgets.ts is provided by @decocms/start/types/widgets, not
448
+ // scaffolded locally.
446
449
  "src/types/deco.ts",
447
450
  "src/types/commerce-app.ts",
448
451
  ];
@@ -3,15 +3,12 @@ import type { MigrationContext } from "../types";
3
3
  export function generateTypeFiles(ctx: MigrationContext): Record<string, string> {
4
4
  const files: Record<string, string> = {};
5
5
 
6
- files["src/types/widgets.ts"] = `export type ImageWidget = string;
7
- export type HTMLWidget = string;
8
- export type VideoWidget = string;
9
- export type TextWidget = string;
10
- export type RichText = string;
11
- export type Secret = string;
12
- export type Color = string;
13
- export type ButtonWidget = string;
14
- `;
6
+ // src/types/widgets.ts is no longer generated the framework owns these
7
+ // string aliases (`ImageWidget`, `HTMLWidget`, …) at
8
+ // `@decocms/start/types/widgets`, and `transforms/imports.ts` rewrites
9
+ // `apps/admin/widgets.ts` directly to that path. Schema generation
10
+ // works the same way: the generator matches by type *text*, not module
11
+ // identity (see scripts/generate-schema.ts:WIDGET_TYPE_FORMATS).
15
12
 
16
13
  files["src/types/deco.ts"] = `export type SectionProps<T extends (...args: any[]) => any> = Awaited<
17
14
  ReturnType<T>
@@ -29,7 +29,12 @@ const IMPORT_RULES: Array<[RegExp, string | null]> = [
29
29
  [/^"@deco\/deco"$/, `"~/types/deco"`],
30
30
 
31
31
  // Apps — widgets & components
32
- [/^"apps\/admin\/widgets\.ts"$/, `"~/types/widgets"`],
32
+ // Widget aliases (ImageWidget, HTMLWidget, ...) are framework-owned —
33
+ // every site has the same type set, and the schema generator detects
34
+ // them via type-text matching, not module identity. Re-export from
35
+ // @decocms/start/types/widgets so we don't keep a duplicated 8-line
36
+ // file in every site.
37
+ [/^"apps\/admin\/widgets\.ts"$/, `"@decocms/start/types/widgets"`],
33
38
  [/^"apps\/website\/components\/Image\.tsx"$/, `"~/components/ui/Image"`],
34
39
  [/^"apps\/website\/components\/Picture\.tsx"$/, `"~/components/ui/Picture"`],
35
40
  [/^"apps\/website\/components\/Video\.tsx"$/, `"~/components/ui/Video"`],
@@ -32,6 +32,7 @@ import { transform } from "./migrate/phase-transform";
32
32
  import { cleanup } from "./migrate/phase-cleanup";
33
33
  import { report } from "./migrate/phase-report";
34
34
  import { verify } from "./migrate/phase-verify";
35
+ import { compile } from "./migrate/phase-compile";
35
36
  import { banner, stat, red, green, yellow } from "./migrate/colors";
36
37
 
37
38
  function parseArgs(args: string[]): {
@@ -39,11 +40,17 @@ function parseArgs(args: string[]): {
39
40
  dryRun: boolean;
40
41
  verbose: boolean;
41
42
  help: boolean;
43
+ strict: boolean;
44
+ withBuild: boolean;
45
+ noCompile: boolean;
42
46
  } {
43
47
  let source = ".";
44
48
  let dryRun = false;
45
49
  let verbose = false;
46
50
  let help = false;
51
+ let strict = false;
52
+ let withBuild = false;
53
+ let noCompile = false;
47
54
 
48
55
  for (let i = 0; i < args.length; i++) {
49
56
  switch (args[i]) {
@@ -56,6 +63,15 @@ function parseArgs(args: string[]): {
56
63
  case "--verbose":
57
64
  verbose = true;
58
65
  break;
66
+ case "--strict":
67
+ strict = true;
68
+ break;
69
+ case "--with-build":
70
+ withBuild = true;
71
+ break;
72
+ case "--no-compile":
73
+ noCompile = true;
74
+ break;
59
75
  case "--help":
60
76
  case "-h":
61
77
  help = true;
@@ -63,7 +79,7 @@ function parseArgs(args: string[]): {
63
79
  }
64
80
  }
65
81
 
66
- return { source, dryRun, verbose, help };
82
+ return { source, dryRun, verbose, help, strict, withBuild, noCompile };
67
83
  }
68
84
 
69
85
  function showHelp() {
@@ -77,11 +93,15 @@ function showHelp() {
77
93
  --source <dir> Source directory (default: .)
78
94
  --dry-run Preview changes without writing files
79
95
  --verbose Show detailed output for every file
96
+ --strict Fail (exit 2) when typecheck/build report errors
97
+ --with-build Also run \`vite build\` after typecheck (slower)
98
+ --no-compile Skip the post-bootstrap compile phase entirely
80
99
  --help, -h Show this help message
81
100
 
82
101
  Examples:
83
102
  npx -p @decocms/start deco-migrate --dry-run --verbose
84
103
  npx -p @decocms/start deco-migrate --source ./my-site
104
+ npx -p @decocms/start deco-migrate --strict --with-build
85
105
  npx -p @decocms/start deco-migrate
86
106
  `);
87
107
  }
@@ -132,6 +152,19 @@ async function main() {
132
152
  if (!ctx.dryRun) {
133
153
  bootstrap(ctx);
134
154
  }
155
+
156
+ // Phase 8: Compile (typecheck + optional build)
157
+ // Skipped in dry-run, when --no-compile is passed, or when bootstrap
158
+ // didn't install dependencies (handled inside `compile`).
159
+ if (!opts.noCompile) {
160
+ const compileResult = compile(ctx, {
161
+ strict: opts.strict,
162
+ withBuild: opts.withBuild,
163
+ });
164
+ if (compileResult.shouldFail) {
165
+ process.exit(2);
166
+ }
167
+ }
135
168
  } catch (error) {
136
169
  console.error(`\n ${red("Migration failed:")}`, error);
137
170
  process.exit(1);