@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
|
@@ -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
|
+
}
|
package/scripts/migrate.ts
CHANGED
|
@@ -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 {
|
|
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>
|
|
97
|
-
--dry-run
|
|
98
|
-
--verbose
|
|
99
|
-
--strict
|
|
100
|
-
--with-build
|
|
101
|
-
--no-compile
|
|
102
|
-
--
|
|
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);
|