@decocms/start 2.12.0 → 2.13.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/start",
3
- "version": "2.12.0",
3
+ "version": "2.13.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,137 @@
1
+ /**
2
+ * Tests for Phase 9 (cleanup audit integration into migrate.ts).
3
+ *
4
+ * These exercise the wrapper logic — what it prints, when it fails,
5
+ * how it interacts with --strict and dry-run. The underlying audit
6
+ * rules are tested separately in post-cleanup/runner.test.ts; we
7
+ * stub the disk minimally here just to drive findings counts.
8
+ */
9
+
10
+ import * as fs from "node:fs";
11
+ import * as os from "node:os";
12
+ import * as path from "node:path";
13
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
14
+ import { cleanupAudit } from "./phase-cleanup-audit";
15
+ import { createContext } from "./types";
16
+
17
+ let tmpDir: string;
18
+
19
+ beforeEach(() => {
20
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "deco-migrate-audit-"));
21
+ });
22
+
23
+ afterEach(() => {
24
+ fs.rmSync(tmpDir, { recursive: true, force: true });
25
+ });
26
+
27
+ function makeCtx(overrides?: { dryRun?: boolean }) {
28
+ return createContext(tmpDir, {
29
+ dryRun: overrides?.dryRun ?? false,
30
+ verbose: false,
31
+ });
32
+ }
33
+
34
+ describe("cleanupAudit — dry-run", () => {
35
+ it("is a no-op in dry-run mode (returns false, no console output)", () => {
36
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
37
+ const ctx = makeCtx({ dryRun: true });
38
+ const failed = cleanupAudit(ctx);
39
+ expect(failed).toBe(false);
40
+ expect(spy).not.toHaveBeenCalled();
41
+ spy.mockRestore();
42
+ });
43
+ });
44
+
45
+ describe("cleanupAudit — empty site", () => {
46
+ it("prints success and returns false when there are no findings", () => {
47
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
48
+ const ctx = makeCtx();
49
+ const failed = cleanupAudit(ctx);
50
+ expect(failed).toBe(false);
51
+ const out = spy.mock.calls.map((c) => c.join(" ")).join("\n");
52
+ expect(out).toMatch(/No findings/);
53
+ spy.mockRestore();
54
+ });
55
+ });
56
+
57
+ describe("cleanupAudit — info-only findings (e.g. local widgets.ts)", () => {
58
+ beforeEach(() => {
59
+ // Set up a finding for rule 6 (local-widgets-types):
60
+ // need src/types/widgets.ts + at least one importer.
61
+ fs.mkdirSync(path.join(tmpDir, "src", "types"), { recursive: true });
62
+ fs.writeFileSync(
63
+ path.join(tmpDir, "src", "types", "widgets.ts"),
64
+ "export type ImageWidget = string;\n",
65
+ );
66
+ fs.mkdirSync(path.join(tmpDir, "src", "sections"), { recursive: true });
67
+ fs.writeFileSync(
68
+ path.join(tmpDir, "src", "sections", "Foo.tsx"),
69
+ 'import type { ImageWidget } from "~/types/widgets";\nexport const x: ImageWidget = "y";\n',
70
+ );
71
+ });
72
+
73
+ it("prints the finding and returns false (info doesn't fail strict)", () => {
74
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
75
+ const ctx = makeCtx();
76
+ const failed = cleanupAudit(ctx, { strict: true });
77
+ expect(failed).toBe(false);
78
+ const out = spy.mock.calls.map((c) => c.join(" ")).join("\n");
79
+ expect(out).toMatch(/Local src\/types\/widgets\.ts/);
80
+ expect(out).toMatch(/widgets\.ts/);
81
+ expect(out).toMatch(/deco-post-cleanup --fix/);
82
+ spy.mockRestore();
83
+ });
84
+ });
85
+
86
+ describe("cleanupAudit — warning findings (vtex-shim-regression)", () => {
87
+ beforeEach(() => {
88
+ // Trigger rule 5 (vtex-shim-regression, warning severity):
89
+ // any file outside src/lib/ that imports from ~/lib/vtex-*.
90
+ fs.mkdirSync(path.join(tmpDir, "src", "loaders"), { recursive: true });
91
+ fs.writeFileSync(
92
+ path.join(tmpDir, "src", "loaders", "Product.ts"),
93
+ 'import { fetchSafe } from "~/lib/vtex-fetch";\nexport default fetchSafe;\n',
94
+ );
95
+ });
96
+
97
+ it("returns false in non-strict mode even with warning findings", () => {
98
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
99
+ const ctx = makeCtx();
100
+ const failed = cleanupAudit(ctx, { strict: false });
101
+ expect(failed).toBe(false);
102
+ spy.mockRestore();
103
+ });
104
+
105
+ it("returns true in --strict mode when warning findings exist", () => {
106
+ const spyLog = vi.spyOn(console, "log").mockImplementation(() => {});
107
+ const ctx = makeCtx();
108
+ const failed = cleanupAudit(ctx, { strict: true });
109
+ expect(failed).toBe(true);
110
+ const out = spyLog.mock.calls.map((c) => c.join(" ")).join("\n");
111
+ expect(out).toMatch(/--strict/);
112
+ expect(out).toMatch(/failed the audit/);
113
+ spyLog.mockRestore();
114
+ });
115
+ });
116
+
117
+ describe("cleanupAudit — output truncation", () => {
118
+ beforeEach(() => {
119
+ // Fabricate >5 vtex-shim-regression findings to test the cap.
120
+ fs.mkdirSync(path.join(tmpDir, "src", "loaders"), { recursive: true });
121
+ for (let i = 0; i < 8; i++) {
122
+ fs.writeFileSync(
123
+ path.join(tmpDir, "src", "loaders", `Loader${i}.ts`),
124
+ 'import { fetchSafe } from "~/lib/vtex-fetch";\n',
125
+ );
126
+ }
127
+ });
128
+
129
+ it("caps per-rule output at 5 with a 'and N more' suffix", () => {
130
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
131
+ const ctx = makeCtx();
132
+ cleanupAudit(ctx);
133
+ const out = spy.mock.calls.map((c) => c.join(" ")).join("\n");
134
+ expect(out).toMatch(/and 3 more/);
135
+ spy.mockRestore();
136
+ });
137
+ });
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Phase 9: Post-Migration Cleanup Audit
3
+ *
4
+ * Runs the same `deco-post-cleanup` audit logic as the standalone CLI,
5
+ * but inline at the tail of `deco-migrate`. The goal is to surface any
6
+ * residual debt the migration script can't fix on its own (e.g.
7
+ * silent vtex shim regressions, orphan TODOs, manual-review items)
8
+ * the moment the migration completes — without making the user
9
+ * remember a separate command.
10
+ *
11
+ * Behaviour:
12
+ * - Always READ-ONLY. Auto-fix is opt-in via the standalone CLI's
13
+ * `--fix` flag — never invoked from inside the migration script
14
+ * to keep the migration's mutation surface predictable.
15
+ * - Skipped in dry-run (no migrated output to scan).
16
+ * - Skipped via `--no-cleanup-audit` when integrated runs are noisy.
17
+ * - In `--strict` mode, returns true (caller exits 2) if any
18
+ * warning-severity findings exist. Info findings never fail the
19
+ * build — they're just hints.
20
+ */
21
+
22
+ import { banner, bold, gray, green, red, yellow } from "./colors";
23
+ import { realFsAdapter, runAudit } from "./post-cleanup/runner";
24
+ import type { AuditReport, Severity } from "./post-cleanup/types";
25
+ import type { MigrationContext } from "./types";
26
+
27
+ export interface CleanupAuditOptions {
28
+ /** Promote warning findings to fatal (exit 2 from main). Default: false. */
29
+ strict?: boolean;
30
+ }
31
+
32
+ /**
33
+ * Returns `true` when the caller should exit with a non-zero code.
34
+ * Always false in normal mode — audit is informational by default.
35
+ */
36
+ export function cleanupAudit(ctx: MigrationContext, opts: CleanupAuditOptions = {}): boolean {
37
+ if (ctx.dryRun) {
38
+ return false;
39
+ }
40
+
41
+ banner("Phase 9: Post-Migration Cleanup Audit");
42
+
43
+ const report = runAudit(ctx.sourceDir, realFsAdapter);
44
+
45
+ if (report.totalFindings === 0) {
46
+ console.log(` ${green("✓")} No findings — migration output is clean.`);
47
+ return false;
48
+ }
49
+
50
+ printSummary(report);
51
+
52
+ const warnings = countSeverity(report, "warning");
53
+ const infos = countSeverity(report, "info");
54
+ const willFail = (opts.strict ?? false) && warnings > 0;
55
+
56
+ console.log("");
57
+ console.log(
58
+ ` ${bold("Audit:")} ${report.totalFindings} finding(s) — ${yellow(`${warnings} warning(s)`)}, ${gray(`${infos} info`)}`,
59
+ );
60
+ console.log(
61
+ ` ${gray("Run")} ${bold("deco-post-cleanup --fix")} ${gray("from this directory to auto-correct the safe rules,")}`,
62
+ );
63
+ console.log(` ${gray("or see post-migration-cleanup.md for the full per-rule playbook.")}`);
64
+
65
+ if (willFail) {
66
+ console.log(
67
+ `\n ${red("--strict:")} ${warnings} warning-severity finding(s) failed the audit.`,
68
+ );
69
+ return true;
70
+ }
71
+
72
+ return false;
73
+ }
74
+
75
+ function severityTag(sev: Severity, text: string): string {
76
+ if (sev === "warning") return yellow(text);
77
+ return gray(text);
78
+ }
79
+
80
+ function printSummary(report: AuditReport): void {
81
+ let idx = 0;
82
+ for (const summary of report.rules) {
83
+ idx++;
84
+ if (summary.findings.length === 0) continue;
85
+ const headColor = yellow;
86
+ console.log(
87
+ ` ${headColor(`[${idx}] ${summary.title}`)} ${gray(`(${summary.findings.length} found)`)}`,
88
+ );
89
+ // Cap the per-rule output so a noisy site doesn't drown the
90
+ // migration's own report. Standalone CLI shows everything.
91
+ const visible = summary.findings.slice(0, 5);
92
+ for (const f of visible) {
93
+ const tag = severityTag(f.severity, `[${f.severity.toUpperCase()}]`);
94
+ console.log(` ${tag} ${bold(f.file)} — ${f.message}`);
95
+ }
96
+ const hidden = summary.findings.length - visible.length;
97
+ if (hidden > 0) {
98
+ console.log(` ${gray(`...and ${hidden} more (run deco-post-cleanup for full list)`)}`);
99
+ }
100
+ }
101
+ }
102
+
103
+ function countSeverity(report: AuditReport, sev: Severity): number {
104
+ return report.rules.flatMap((r) => r.findings).filter((f) => f.severity === sev).length;
105
+ }
@@ -30,6 +30,7 @@ import { banner, green, red, stat, yellow } from "./migrate/colors";
30
30
  import { loadConfig, validateConfig } from "./migrate/config";
31
31
  import { analyze } from "./migrate/phase-analyze";
32
32
  import { cleanup } from "./migrate/phase-cleanup";
33
+ import { cleanupAudit } from "./migrate/phase-cleanup-audit";
33
34
  import { compile } from "./migrate/phase-compile";
34
35
  import { report } from "./migrate/phase-report";
35
36
  import { scaffold } from "./migrate/phase-scaffold";
@@ -46,6 +47,7 @@ function parseArgs(args: string[]): {
46
47
  strict: boolean;
47
48
  withBuild: boolean;
48
49
  noCompile: boolean;
50
+ noCleanupAudit: boolean;
49
51
  } {
50
52
  let source = ".";
51
53
  let dryRun = false;
@@ -54,6 +56,7 @@ function parseArgs(args: string[]): {
54
56
  let strict = false;
55
57
  let withBuild = false;
56
58
  let noCompile = false;
59
+ let noCleanupAudit = false;
57
60
 
58
61
  for (let i = 0; i < args.length; i++) {
59
62
  switch (args[i]) {
@@ -75,6 +78,9 @@ function parseArgs(args: string[]): {
75
78
  case "--no-compile":
76
79
  noCompile = true;
77
80
  break;
81
+ case "--no-cleanup-audit":
82
+ noCleanupAudit = true;
83
+ break;
78
84
  case "--help":
79
85
  case "-h":
80
86
  help = true;
@@ -82,7 +88,16 @@ function parseArgs(args: string[]): {
82
88
  }
83
89
  }
84
90
 
85
- return { source, dryRun, verbose, help, strict, withBuild, noCompile };
91
+ return {
92
+ source,
93
+ dryRun,
94
+ verbose,
95
+ help,
96
+ strict,
97
+ withBuild,
98
+ noCompile,
99
+ noCleanupAudit,
100
+ };
86
101
  }
87
102
 
88
103
  function showHelp() {
@@ -93,13 +108,15 @@ function showHelp() {
93
108
  npx -p @decocms/start deco-migrate [options]
94
109
 
95
110
  Options:
96
- --source <dir> Source directory (default: .)
97
- --dry-run Preview changes without writing files
98
- --verbose Show detailed output for every file
99
- --strict Fail (exit 2) when typecheck/build report errors
100
- --with-build Also run \`vite build\` after typecheck (slower)
101
- --no-compile Skip the post-bootstrap compile phase entirely
102
- --help, -h Show this help message
111
+ --source <dir> Source directory (default: .)
112
+ --dry-run Preview changes without writing files
113
+ --verbose Show detailed output for every file
114
+ --strict Fail (exit 2) when typecheck/build report errors
115
+ --with-build Also run \`vite build\` after typecheck (slower)
116
+ --no-compile Skip the post-bootstrap compile phase entirely
117
+ --no-cleanup-audit Skip the post-migration cleanup audit (run separately
118
+ via \`deco-post-cleanup\` if needed)
119
+ --help, -h Show this help message
103
120
 
104
121
  Examples:
105
122
  npx -p @decocms/start deco-migrate --dry-run --verbose
@@ -191,6 +208,17 @@ async function main() {
191
208
  process.exit(2);
192
209
  }
193
210
  }
211
+
212
+ // Phase 9: Post-migration cleanup audit
213
+ // Read-only scan that catches residual debt the migration script
214
+ // can't (or won't) fix. Always informational unless --strict is on,
215
+ // in which case warning-severity findings exit 2.
216
+ if (!opts.noCleanupAudit) {
217
+ const auditFailed = cleanupAudit(ctx, { strict: opts.strict });
218
+ if (auditFailed) {
219
+ process.exit(2);
220
+ }
221
+ }
194
222
  } catch (error) {
195
223
  console.error(`\n ${red("Migration failed:")}`, error);
196
224
  process.exit(1);