@decocms/blocks-cli 7.0.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 +44 -0
- package/scripts/analyze-traces.mjs +1117 -0
- package/scripts/audit-observability-config.test.ts +446 -0
- package/scripts/audit-observability-config.ts +511 -0
- package/scripts/deco-migrate-cli.ts +444 -0
- package/scripts/fast-deploy-kv.test.ts +131 -0
- package/scripts/generate-blocks.test.ts +94 -0
- package/scripts/generate-blocks.ts +274 -0
- package/scripts/generate-invoke.test.ts +195 -0
- package/scripts/generate-invoke.ts +469 -0
- package/scripts/generate-loaders.ts +217 -0
- package/scripts/generate-schema.ts +1287 -0
- package/scripts/generate-sections.ts +237 -0
- package/scripts/htmx-analyze.ts +226 -0
- package/scripts/lib/blocks-dedupe.test.ts +179 -0
- package/scripts/lib/blocks-dedupe.ts +142 -0
- package/scripts/lib/cf-kv-rest.ts +78 -0
- package/scripts/lib/jsonc.ts +122 -0
- package/scripts/lib/kv-snapshot.ts +51 -0
- package/scripts/lib/read-decofile.ts +70 -0
- package/scripts/lib/sync-helpers.ts +44 -0
- package/scripts/migrate/analyzers/htmx-analyze.test.ts +372 -0
- package/scripts/migrate/analyzers/htmx-analyze.ts +425 -0
- package/scripts/migrate/analyzers/island-classifier.ts +96 -0
- package/scripts/migrate/analyzers/loader-inventory.ts +63 -0
- package/scripts/migrate/analyzers/section-metadata.ts +147 -0
- package/scripts/migrate/analyzers/theme-extractor.ts +122 -0
- package/scripts/migrate/colors.ts +46 -0
- package/scripts/migrate/config.test.ts +202 -0
- package/scripts/migrate/config.ts +186 -0
- package/scripts/migrate/phase-analyze.test.ts +63 -0
- package/scripts/migrate/phase-analyze.ts +782 -0
- package/scripts/migrate/phase-cleanup-audit.test.ts +137 -0
- package/scripts/migrate/phase-cleanup-audit.ts +105 -0
- package/scripts/migrate/phase-cleanup.test.ts +141 -0
- package/scripts/migrate/phase-cleanup.ts +1588 -0
- package/scripts/migrate/phase-compile.test.ts +193 -0
- package/scripts/migrate/phase-compile.ts +177 -0
- package/scripts/migrate/phase-report.ts +243 -0
- package/scripts/migrate/phase-scaffold.ts +593 -0
- package/scripts/migrate/phase-transform.ts +310 -0
- package/scripts/migrate/phase-verify.test.ts +127 -0
- package/scripts/migrate/phase-verify.ts +572 -0
- package/scripts/migrate/post-cleanup/rules.ts +1708 -0
- package/scripts/migrate/post-cleanup/runner.test.ts +1771 -0
- package/scripts/migrate/post-cleanup/runner.ts +137 -0
- package/scripts/migrate/post-cleanup/shim-classify.test.ts +352 -0
- package/scripts/migrate/post-cleanup/shim-classify.ts +246 -0
- package/scripts/migrate/post-cleanup/types.ts +106 -0
- package/scripts/migrate/source-layout.test.ts +111 -0
- package/scripts/migrate/source-layout.ts +103 -0
- package/scripts/migrate/templates/app-css.ts +366 -0
- package/scripts/migrate/templates/cache-config.ts +26 -0
- package/scripts/migrate/templates/commerce-loaders.ts +230 -0
- package/scripts/migrate/templates/cursor-rules.test.ts +59 -0
- package/scripts/migrate/templates/cursor-rules.ts +70 -0
- package/scripts/migrate/templates/hooks.test.ts +141 -0
- package/scripts/migrate/templates/hooks.ts +152 -0
- package/scripts/migrate/templates/knip-config.ts +27 -0
- package/scripts/migrate/templates/lib-utils.test.ts +139 -0
- package/scripts/migrate/templates/lib-utils.ts +326 -0
- package/scripts/migrate/templates/lockfile-check-yml.test.ts +26 -0
- package/scripts/migrate/templates/lockfile-check-yml.ts +66 -0
- package/scripts/migrate/templates/package-json.ts +177 -0
- package/scripts/migrate/templates/routes.ts +237 -0
- package/scripts/migrate/templates/sdk-gen.ts +59 -0
- package/scripts/migrate/templates/section-loaders.ts +456 -0
- package/scripts/migrate/templates/server-entry.ts +561 -0
- package/scripts/migrate/templates/setup.ts +148 -0
- package/scripts/migrate/templates/tsconfig.ts +21 -0
- package/scripts/migrate/templates/types-gen.ts +174 -0
- package/scripts/migrate/templates/ui-components.ts +144 -0
- package/scripts/migrate/templates/vite-config.ts +101 -0
- package/scripts/migrate/transforms/dead-code.ts +455 -0
- package/scripts/migrate/transforms/deno-isms.ts +85 -0
- package/scripts/migrate/transforms/fresh-apis.ts +223 -0
- package/scripts/migrate/transforms/htmx-on-events.test.ts +305 -0
- package/scripts/migrate/transforms/htmx-on-events.ts +193 -0
- package/scripts/migrate/transforms/imports.ts +385 -0
- package/scripts/migrate/transforms/jsx.ts +317 -0
- package/scripts/migrate/transforms/section-conventions.ts +210 -0
- package/scripts/migrate/transforms/tailwind.ts +739 -0
- package/scripts/migrate/types.ts +244 -0
- package/scripts/migrate-blocks-to-kv.ts +104 -0
- package/scripts/migrate-post-cleanup.ts +191 -0
- package/scripts/migrate-to-cf-observability.test.ts +215 -0
- package/scripts/migrate-to-cf-observability.ts +699 -0
- package/scripts/migrate.ts +282 -0
- package/scripts/smoke-otlp-errorlog.ts +46 -0
- package/scripts/smoke-otlp-meter.ts +48 -0
- package/scripts/sync-blocks-to-kv.ts +153 -0
- package/scripts/tailwind-lint.ts +518 -0
- package/tsconfig.json +7 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { MigrationContext } from "./types";
|
|
4
|
+
import { logPhase } from "./types";
|
|
5
|
+
|
|
6
|
+
const FRAMEWORK_FINDINGS = [
|
|
7
|
+
"Session/analytics SDK is boilerplate duplicated across all sites — should be a single framework function",
|
|
8
|
+
"GTM event system (useGTMEvent, data-gtm-* listeners) is universal pattern — should be in @decocms/start",
|
|
9
|
+
"Route files (__root.tsx, index.tsx, $.tsx, deco/*) are near-identical across sites — should be generated by framework",
|
|
10
|
+
"server.ts, worker-entry.ts, router.tsx are pure boilerplate — should be a single createSite() call",
|
|
11
|
+
"setup.ts section registration via import.meta.glob is 100% boilerplate — framework should handle this",
|
|
12
|
+
"runtime.ts invoke proxy is identical across sites — already in @decocms/start but sites still have local copies",
|
|
13
|
+
"apps/site.ts is mostly empty after migration — platform config should be in a config file, not code",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export function report(ctx: MigrationContext): void {
|
|
17
|
+
logPhase("Report");
|
|
18
|
+
|
|
19
|
+
ctx.frameworkFindings = FRAMEWORK_FINDINGS;
|
|
20
|
+
|
|
21
|
+
const lines: string[] = [];
|
|
22
|
+
|
|
23
|
+
lines.push("# Migration Report");
|
|
24
|
+
lines.push("");
|
|
25
|
+
lines.push(`**Site:** ${ctx.siteName}`);
|
|
26
|
+
lines.push(`**Platform:** ${ctx.platform}`);
|
|
27
|
+
lines.push(`**GTM ID:** ${ctx.gtmId || "none"}`);
|
|
28
|
+
lines.push(`**Date:** ${new Date().toISOString().split("T")[0]}`);
|
|
29
|
+
lines.push(`**Mode:** ${ctx.dryRun ? "DRY RUN" : "EXECUTED"}`);
|
|
30
|
+
lines.push("");
|
|
31
|
+
|
|
32
|
+
// Summary
|
|
33
|
+
lines.push("## Summary");
|
|
34
|
+
lines.push("");
|
|
35
|
+
lines.push(`| Metric | Count |`);
|
|
36
|
+
lines.push(`|--------|-------|`);
|
|
37
|
+
lines.push(`| Files analyzed | ${ctx.files.length} |`);
|
|
38
|
+
lines.push(`| Files scaffolded | ${ctx.scaffoldedFiles.length} |`);
|
|
39
|
+
lines.push(`| Files transformed | ${ctx.transformedFiles.length} |`);
|
|
40
|
+
lines.push(`| Files deleted | ${ctx.deletedFiles.length} |`);
|
|
41
|
+
lines.push(`| Files moved | ${ctx.movedFiles.length} |`);
|
|
42
|
+
lines.push(
|
|
43
|
+
`| Manual review items | ${ctx.manualReviewItems.length} |`,
|
|
44
|
+
);
|
|
45
|
+
lines.push("");
|
|
46
|
+
|
|
47
|
+
// Scaffolded files
|
|
48
|
+
lines.push("## Scaffolded Files (new)");
|
|
49
|
+
lines.push("");
|
|
50
|
+
for (const f of ctx.scaffoldedFiles) {
|
|
51
|
+
lines.push(`- \`${f}\``);
|
|
52
|
+
}
|
|
53
|
+
lines.push("");
|
|
54
|
+
|
|
55
|
+
// Transformed files
|
|
56
|
+
lines.push("## Transformed Files");
|
|
57
|
+
lines.push("");
|
|
58
|
+
for (const f of ctx.transformedFiles) {
|
|
59
|
+
lines.push(`- \`${f}\``);
|
|
60
|
+
}
|
|
61
|
+
lines.push("");
|
|
62
|
+
|
|
63
|
+
// Deleted files
|
|
64
|
+
lines.push("## Deleted Files");
|
|
65
|
+
lines.push("");
|
|
66
|
+
for (const f of ctx.deletedFiles) {
|
|
67
|
+
lines.push(`- \`${f}\``);
|
|
68
|
+
}
|
|
69
|
+
lines.push("");
|
|
70
|
+
|
|
71
|
+
// Moved files
|
|
72
|
+
if (ctx.movedFiles.length > 0) {
|
|
73
|
+
lines.push("## Moved Files");
|
|
74
|
+
lines.push("");
|
|
75
|
+
for (const { from, to } of ctx.movedFiles) {
|
|
76
|
+
lines.push(`- \`${from}\` → \`${to}\``);
|
|
77
|
+
}
|
|
78
|
+
lines.push("");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Manual review
|
|
82
|
+
if (ctx.manualReviewItems.length > 0) {
|
|
83
|
+
lines.push("## Manual Review Required");
|
|
84
|
+
lines.push("");
|
|
85
|
+
for (const item of ctx.manualReviewItems) {
|
|
86
|
+
const icon = item.severity === "error"
|
|
87
|
+
? "🔴"
|
|
88
|
+
: item.severity === "warning"
|
|
89
|
+
? "🟡"
|
|
90
|
+
: "🔵";
|
|
91
|
+
lines.push(`${icon} **\`${item.file}\`**: ${item.reason}`);
|
|
92
|
+
}
|
|
93
|
+
lines.push("");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Analyzer summaries
|
|
97
|
+
if (ctx.sectionMetas.length > 0) {
|
|
98
|
+
lines.push("## Section Analysis");
|
|
99
|
+
lines.push("");
|
|
100
|
+
const withLoader = ctx.sectionMetas.filter((m) => m.hasLoader).length;
|
|
101
|
+
const layouts = ctx.sectionMetas.filter((m) => m.isHeader || m.isFooter || m.isTheme).length;
|
|
102
|
+
const listings = ctx.sectionMetas.filter((m) => m.isListing).length;
|
|
103
|
+
lines.push(`- **${ctx.sectionMetas.length}** sections analyzed`);
|
|
104
|
+
lines.push(`- **${withLoader}** have loaders (extracted to \`setup/section-loaders.ts\`)`);
|
|
105
|
+
lines.push(`- **${layouts}** are layout sections (eager + sync + layout)`);
|
|
106
|
+
lines.push(`- **${listings}** are listing sections (cache = "listing")`);
|
|
107
|
+
lines.push("");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (ctx.islandClassifications.length > 0) {
|
|
111
|
+
const wrappers = ctx.islandClassifications.filter((c) => c.type === "wrapper").length;
|
|
112
|
+
const standalone = ctx.islandClassifications.filter((c) => c.type === "standalone").length;
|
|
113
|
+
lines.push("## Island Elimination");
|
|
114
|
+
lines.push("");
|
|
115
|
+
lines.push(`- **${ctx.islandClassifications.length}** islands classified`);
|
|
116
|
+
lines.push(`- **${wrappers}** wrappers (deleted, imports repointed)`);
|
|
117
|
+
lines.push(`- **${standalone}** standalone (moved to \`src/components/\`)`);
|
|
118
|
+
lines.push("");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (ctx.loaderInventory.length > 0) {
|
|
122
|
+
const custom = ctx.loaderInventory.filter((l) => l.isCustom).length;
|
|
123
|
+
const mapped = ctx.loaderInventory.filter((l) => l.appsEquivalent).length;
|
|
124
|
+
lines.push("## Loader Inventory");
|
|
125
|
+
lines.push("");
|
|
126
|
+
lines.push(`- **${ctx.loaderInventory.length}** loaders inventoried`);
|
|
127
|
+
lines.push(`- **${mapped}** mapped to \`@decocms/apps\` equivalents`);
|
|
128
|
+
lines.push(`- **${custom}** custom (included in \`setup/commerce-loaders.ts\`)`);
|
|
129
|
+
lines.push("");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Always-present manual review items
|
|
133
|
+
lines.push("## Always Check (site-specific)");
|
|
134
|
+
lines.push("");
|
|
135
|
+
lines.push("- [ ] `src/setup/commerce-loaders.ts` — verify each loader mapping is correct");
|
|
136
|
+
lines.push("- [ ] `src/setup/section-loaders.ts` — verify extracted loaders work correctly");
|
|
137
|
+
lines.push("- [ ] `src/hooks/useCart.ts` — wire to actual server functions for your platform");
|
|
138
|
+
lines.push("- [ ] `src/worker-entry.ts` — verify CSP, proxy, and segment builder");
|
|
139
|
+
lines.push("- [ ] DaisyUI v4 to v5 class name changes");
|
|
140
|
+
lines.push("- [ ] Tailwind v3 to v4: verify all utility classes still work");
|
|
141
|
+
lines.push("- [ ] Check `src/styles/app.css` theme colors match the original design");
|
|
142
|
+
lines.push("- [ ] Run `npm run generate:blocks` and `npm run generate:schema` after migration");
|
|
143
|
+
lines.push("");
|
|
144
|
+
|
|
145
|
+
// Known Issues
|
|
146
|
+
lines.push("## Known Issues (Tailwind v3 → v4 + React)");
|
|
147
|
+
lines.push("");
|
|
148
|
+
lines.push("### Negative z-index on background images");
|
|
149
|
+
lines.push("");
|
|
150
|
+
lines.push("The migration script automatically converts `-z-{n}` to `z-0` on `<img>` and `<Image>` elements.");
|
|
151
|
+
lines.push("However, if you have **non-image elements** with negative z-index (e.g. `-z-10` on a `<div>` used as a background layer), they may become invisible.");
|
|
152
|
+
lines.push("");
|
|
153
|
+
lines.push("**Why it breaks:** In TanStack Start/React, section wrappers (`<section>`) or parent elements can create");
|
|
154
|
+
lines.push("CSS stacking contexts (via `animation`, `transform`, `will-change`, `filter`, `isolation`, etc.).");
|
|
155
|
+
lines.push("A child with negative z-index gets trapped inside that stacking context and renders behind the parent's background — making it invisible.");
|
|
156
|
+
lines.push("");
|
|
157
|
+
lines.push("**How to fix:**");
|
|
158
|
+
lines.push("1. Replace `-z-{n}` with `z-0` on the background element");
|
|
159
|
+
lines.push("2. Content siblings render on top naturally via DOM order (they come after in the HTML)");
|
|
160
|
+
lines.push("3. If needed, add `relative z-10` to content siblings to ensure they stay above");
|
|
161
|
+
lines.push("");
|
|
162
|
+
lines.push("**How to detect:** Search for remaining negative z-index: `grep -rn '\\-z-' src/ --include='*.tsx'`");
|
|
163
|
+
lines.push("");
|
|
164
|
+
lines.push("### Opacity utility classes");
|
|
165
|
+
lines.push("");
|
|
166
|
+
lines.push("Tailwind v4 removed `bg-opacity-{n}`, `text-opacity-{n}`, etc. The script converts them to");
|
|
167
|
+
lines.push("the modifier syntax (e.g. `bg-black bg-opacity-20` → `bg-black/20`). If a color and its opacity");
|
|
168
|
+
lines.push("are not in the same className string (e.g. set via different conditional branches), the script");
|
|
169
|
+
lines.push("flags them for manual review.");
|
|
170
|
+
lines.push("");
|
|
171
|
+
lines.push("**How to detect:** `grep -rn 'opacity-' src/ --include='*.tsx'`");
|
|
172
|
+
lines.push("");
|
|
173
|
+
|
|
174
|
+
// Framework findings
|
|
175
|
+
lines.push("## Framework Findings");
|
|
176
|
+
lines.push("");
|
|
177
|
+
lines.push(
|
|
178
|
+
"> These are patterns found during migration that should eventually be handled by `@decocms/start` instead of being duplicated in every site.",
|
|
179
|
+
);
|
|
180
|
+
lines.push("");
|
|
181
|
+
for (const finding of ctx.frameworkFindings) {
|
|
182
|
+
lines.push(`- ${finding}`);
|
|
183
|
+
}
|
|
184
|
+
lines.push("");
|
|
185
|
+
|
|
186
|
+
// Next steps
|
|
187
|
+
lines.push("## Next Steps");
|
|
188
|
+
lines.push("");
|
|
189
|
+
lines.push("```bash");
|
|
190
|
+
lines.push("# 1. Install dependencies (bun is the canonical PM)");
|
|
191
|
+
lines.push("bun install");
|
|
192
|
+
lines.push("");
|
|
193
|
+
lines.push("# 2. Generate CMS blocks and schema");
|
|
194
|
+
lines.push("bun run generate:blocks");
|
|
195
|
+
lines.push("bun run generate:schema");
|
|
196
|
+
lines.push("");
|
|
197
|
+
lines.push("# 3. Generate routes");
|
|
198
|
+
lines.push("bunx tsr generate");
|
|
199
|
+
lines.push("");
|
|
200
|
+
lines.push("# 4. Type check");
|
|
201
|
+
lines.push("bunx tsc --noEmit");
|
|
202
|
+
lines.push("");
|
|
203
|
+
lines.push("# 5. Find unused code");
|
|
204
|
+
lines.push("bun run knip");
|
|
205
|
+
lines.push("");
|
|
206
|
+
lines.push("# 6. Run dev server");
|
|
207
|
+
lines.push("npm run dev");
|
|
208
|
+
lines.push("");
|
|
209
|
+
lines.push("# 7. (Once wrangler.jsonc is wired) enable CF-native observability");
|
|
210
|
+
lines.push("# Adds the canonical `observability` block to wrangler.jsonc so the");
|
|
211
|
+
lines.push("# Cloudflare runtime captures console.* logs and auto-instrumented");
|
|
212
|
+
lines.push("# traces directly into the CF dashboard (Workers & Pages -> <site>");
|
|
213
|
+
lines.push("# -> Observability). No external destination required — the CF");
|
|
214
|
+
lines.push("# dashboard is the destination.");
|
|
215
|
+
lines.push("#");
|
|
216
|
+
lines.push("# Forwarding to a future ClickHouse-backed OTel collector is on the");
|
|
217
|
+
lines.push("# roadmap (placeholder lives at");
|
|
218
|
+
lines.push("# @decocms/start/sdk/otelAdapters/clickhouseCollector); when it");
|
|
219
|
+
lines.push("# ships, the codemod gains a `--destination-logs` /");
|
|
220
|
+
lines.push("# `--destination-traces` flag to point at it.");
|
|
221
|
+
lines.push("npx -p @decocms/start deco-cf-observability # dry-run");
|
|
222
|
+
lines.push("npx -p @decocms/start deco-cf-observability --write # apply");
|
|
223
|
+
lines.push("```");
|
|
224
|
+
lines.push("");
|
|
225
|
+
|
|
226
|
+
const content = lines.join("\n");
|
|
227
|
+
|
|
228
|
+
if (ctx.dryRun) {
|
|
229
|
+
console.log("\n" + content);
|
|
230
|
+
} else {
|
|
231
|
+
const reportPath = path.join(ctx.sourceDir, "MIGRATION_REPORT.md");
|
|
232
|
+
fs.writeFileSync(reportPath, content, "utf-8");
|
|
233
|
+
console.log(` Report written to MIGRATION_REPORT.md`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Print summary to console
|
|
237
|
+
console.log(`\n === Migration ${ctx.dryRun ? "(DRY RUN)" : "COMPLETE"} ===`);
|
|
238
|
+
console.log(` Scaffolded: ${ctx.scaffoldedFiles.length}`);
|
|
239
|
+
console.log(` Transformed: ${ctx.transformedFiles.length}`);
|
|
240
|
+
console.log(` Deleted: ${ctx.deletedFiles.length}`);
|
|
241
|
+
console.log(` Moved: ${ctx.movedFiles.length}`);
|
|
242
|
+
console.log(` Manual review: ${ctx.manualReviewItems.length}`);
|
|
243
|
+
}
|