@decocms/start 2.7.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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/start",
3
- "version": "2.7.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
+ }
@@ -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);