@decocms/start 2.9.0 → 2.11.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/.agents/skills/deco-migrate-script/SKILL.md +27 -0
- package/.agents/skills/deco-to-tanstack-migration/references/post-migration-cleanup.md +23 -0
- package/package.json +4 -2
- package/scripts/migrate/config.test.ts +202 -0
- package/scripts/migrate/config.ts +186 -0
- package/scripts/migrate/phase-transform.ts +21 -2
- package/scripts/migrate/post-cleanup/rules.ts +293 -0
- package/scripts/migrate/post-cleanup/runner.test.ts +288 -0
- package/scripts/migrate/post-cleanup/runner.ts +97 -0
- package/scripts/migrate/post-cleanup/types.ts +66 -0
- package/scripts/migrate/transforms/section-conventions.ts +175 -150
- package/scripts/migrate/types.ts +14 -1
- package/scripts/migrate-post-cleanup.ts +156 -0
- package/scripts/migrate.ts +11 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-migration cleanup audit — shared types.
|
|
3
|
+
*
|
|
4
|
+
* The audit runner is a thin orchestrator: it loads the site, runs each
|
|
5
|
+
* rule, and prints the findings. The interesting bits live in `rules.ts`.
|
|
6
|
+
*
|
|
7
|
+
* Rules are pure(ish) functions over an injected `FsAdapter`, which means
|
|
8
|
+
* they can be unit-tested with an in-memory file system and never touch
|
|
9
|
+
* the real disk in CI.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type Severity = "info" | "warning";
|
|
13
|
+
|
|
14
|
+
export interface Finding {
|
|
15
|
+
/** Stable rule identifier (e.g. "dead-lib-shims"). */
|
|
16
|
+
rule: string;
|
|
17
|
+
severity: Severity;
|
|
18
|
+
/** Site-relative path of the file the finding refers to. */
|
|
19
|
+
file: string;
|
|
20
|
+
/** One-line message — shown in default text output. */
|
|
21
|
+
message: string;
|
|
22
|
+
/** Suggested human action, if any. */
|
|
23
|
+
fix?: string;
|
|
24
|
+
/** Free-form structured payload for JSON consumers. */
|
|
25
|
+
meta?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RuleSummary {
|
|
29
|
+
/** Stable rule identifier. */
|
|
30
|
+
rule: string;
|
|
31
|
+
/** Human-readable section title. */
|
|
32
|
+
title: string;
|
|
33
|
+
findings: Finding[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AuditReport {
|
|
37
|
+
site: string;
|
|
38
|
+
rules: RuleSummary[];
|
|
39
|
+
totalFindings: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Minimal file-system adapter — read + glob. Keeping the surface tiny
|
|
44
|
+
* is what lets us pass an in-memory map in unit tests.
|
|
45
|
+
*/
|
|
46
|
+
export interface FsAdapter {
|
|
47
|
+
exists(absPath: string): boolean;
|
|
48
|
+
readText(absPath: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* Return absolute paths matching the glob, ordered by path. Globs are
|
|
51
|
+
* relative to `siteDir`. Implementations must respect `excludeDirs` and
|
|
52
|
+
* skip them entirely.
|
|
53
|
+
*/
|
|
54
|
+
glob(siteDir: string, pattern: string, excludeDirs?: string[]): string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface RuleContext {
|
|
58
|
+
siteDir: string;
|
|
59
|
+
fs: FsAdapter;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface Rule {
|
|
63
|
+
id: string;
|
|
64
|
+
title: string;
|
|
65
|
+
run(ctx: RuleContext): Finding[];
|
|
66
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
SectionConventionSets,
|
|
3
|
+
} from "../config";
|
|
4
|
+
import { resolveSectionConventions } from "../config";
|
|
5
|
+
import type { SectionMeta, TransformResult } from "../types";
|
|
2
6
|
|
|
3
7
|
/**
|
|
4
8
|
* Adds section convention exports (sync, eager, layout, cache)
|
|
@@ -6,158 +10,179 @@ import type { TransformResult, SectionMeta } from "../types";
|
|
|
6
10
|
*
|
|
7
11
|
* These exports are read by generate-sections.ts in @decocms/start
|
|
8
12
|
* to build the sections.gen.ts registry.
|
|
13
|
+
*
|
|
14
|
+
* The set of section *names* that get hints applied is configurable
|
|
15
|
+
* via `.deco-migrate.config.json` (see `migrate/config.ts`). The exported
|
|
16
|
+
* `transformSectionConventions` keeps a back-compat signature using the
|
|
17
|
+
* baked-in defaults; new callers should prefer
|
|
18
|
+
* `createSectionConventionsTransform(sets)` so config can drive the lists.
|
|
9
19
|
*/
|
|
10
20
|
|
|
11
|
-
const EAGER_SYNC_SECTIONS = new Set([
|
|
12
|
-
"UtilLinks",
|
|
13
|
-
"DepartamentList",
|
|
14
|
-
"ImageGallery",
|
|
15
|
-
"BannersGrid",
|
|
16
|
-
"Carousel",
|
|
17
|
-
"Tipbar",
|
|
18
|
-
"Live",
|
|
19
|
-
]);
|
|
20
|
-
|
|
21
|
-
const SYNC_SECTIONS = new Set([
|
|
22
|
-
"ProductShelf",
|
|
23
|
-
"ProductShelfTabbed",
|
|
24
|
-
"ProductShelfGroup",
|
|
25
|
-
"ProductShelfTopSort",
|
|
26
|
-
"CouponList",
|
|
27
|
-
"NotFoundChallenge",
|
|
28
|
-
"MountedPDP",
|
|
29
|
-
"BackgroundWrapper",
|
|
30
|
-
"SearchResult",
|
|
31
|
-
"LpCartao",
|
|
32
|
-
]);
|
|
33
|
-
|
|
34
|
-
const LISTING_CACHE_SECTIONS = new Set([
|
|
35
|
-
"ProductShelf",
|
|
36
|
-
"ProductShelfTabbed",
|
|
37
|
-
"ProductShelfGroup",
|
|
38
|
-
"ProductShelfTimedOffers",
|
|
39
|
-
]);
|
|
40
|
-
|
|
41
|
-
const STATIC_CACHE_SECTIONS = new Set([
|
|
42
|
-
"InstagramPosts",
|
|
43
|
-
"Faq",
|
|
44
|
-
]);
|
|
45
|
-
|
|
46
21
|
function getSectionBasename(filePath: string): string {
|
|
47
|
-
|
|
22
|
+
return filePath.split("/").pop()?.replace(/\.\w+$/, "") || "";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Build a `transformSectionConventions` closure bound to the given
|
|
27
|
+
* resolved sets. This is the preferred entry point — the caller (usually
|
|
28
|
+
* `phase-transform`) loads config once and passes the sets in.
|
|
29
|
+
*/
|
|
30
|
+
export function createSectionConventionsTransform(
|
|
31
|
+
sets: SectionConventionSets,
|
|
32
|
+
) {
|
|
33
|
+
return (
|
|
34
|
+
content: string,
|
|
35
|
+
sectionMeta: SectionMeta | undefined,
|
|
36
|
+
): TransformResult => transformWithSets(content, sectionMeta, sets);
|
|
48
37
|
}
|
|
49
38
|
|
|
39
|
+
/** Default-configured transform. Uses the baked-in defaults from `config.ts`. */
|
|
40
|
+
const defaultSets = resolveSectionConventions(null);
|
|
41
|
+
|
|
50
42
|
export function transformSectionConventions(
|
|
51
|
-
|
|
52
|
-
|
|
43
|
+
content: string,
|
|
44
|
+
sectionMeta: SectionMeta | undefined,
|
|
45
|
+
): TransformResult {
|
|
46
|
+
return transformWithSets(content, sectionMeta, defaultSets);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function transformWithSets(
|
|
50
|
+
content: string,
|
|
51
|
+
sectionMeta: SectionMeta | undefined,
|
|
52
|
+
sets: SectionConventionSets,
|
|
53
53
|
): TransformResult {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
54
|
+
if (!sectionMeta) {
|
|
55
|
+
return { content, changed: false, notes: [] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const notes: string[] = [];
|
|
59
|
+
let result = content;
|
|
60
|
+
let changed = false;
|
|
61
|
+
const basename = getSectionBasename(sectionMeta.path);
|
|
62
|
+
|
|
63
|
+
// Header, footer, theme → eager + sync + layout
|
|
64
|
+
if (sectionMeta.isHeader || sectionMeta.isFooter || sectionMeta.isTheme) {
|
|
65
|
+
if (!result.includes("export const eager")) {
|
|
66
|
+
result += "\nexport const eager = true;\n";
|
|
67
|
+
notes.push("Added: export const eager = true");
|
|
68
|
+
changed = true;
|
|
69
|
+
}
|
|
70
|
+
if (!result.includes("export const sync")) {
|
|
71
|
+
result += "export const sync = true;\n";
|
|
72
|
+
notes.push("Added: export const sync = true");
|
|
73
|
+
changed = true;
|
|
74
|
+
}
|
|
75
|
+
// Header in golden does NOT have layout=true; only footer+theme do
|
|
76
|
+
if (
|
|
77
|
+
(sectionMeta.isFooter || sectionMeta.isTheme) &&
|
|
78
|
+
!result.includes("export const layout")
|
|
79
|
+
) {
|
|
80
|
+
result += "export const layout = true;\n";
|
|
81
|
+
notes.push("Added: export const layout = true");
|
|
82
|
+
changed = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Known eager+sync sections (non-layout)
|
|
87
|
+
if (sets.eagerSync.has(basename)) {
|
|
88
|
+
if (!result.includes("export const eager")) {
|
|
89
|
+
result += "\nexport const eager = true;\n";
|
|
90
|
+
notes.push(`Added: export const eager = true (${basename})`);
|
|
91
|
+
changed = true;
|
|
92
|
+
}
|
|
93
|
+
if (!result.includes("export const sync")) {
|
|
94
|
+
result += "export const sync = true;\n";
|
|
95
|
+
notes.push(`Added: export const sync = true (${basename})`);
|
|
96
|
+
changed = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Known sync-only sections
|
|
101
|
+
if (sets.sync.has(basename) && !result.includes("export const sync")) {
|
|
102
|
+
result += "\nexport const sync = true;\n";
|
|
103
|
+
notes.push(`Added: export const sync = true (${basename})`);
|
|
104
|
+
changed = true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Listing cache sections
|
|
108
|
+
if (
|
|
109
|
+
sets.listingCache.has(basename) &&
|
|
110
|
+
!result.includes("export const cache")
|
|
111
|
+
) {
|
|
112
|
+
result += '\nexport const cache = "listing";\n';
|
|
113
|
+
notes.push(`Added: export const cache = "listing" (${basename})`);
|
|
114
|
+
changed = true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Static cache sections
|
|
118
|
+
if (
|
|
119
|
+
sets.staticCache.has(basename) &&
|
|
120
|
+
!result.includes("export const cache")
|
|
121
|
+
) {
|
|
122
|
+
result += '\nexport const cache = "static";\n';
|
|
123
|
+
notes.push(`Added: export const cache = "static" (${basename})`);
|
|
124
|
+
changed = true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Generic: listing sections not already matched above
|
|
128
|
+
if (sectionMeta.isListing && !result.includes("export const cache")) {
|
|
129
|
+
result += '\nexport const cache = "listing";\n';
|
|
130
|
+
notes.push('Added: export const cache = "listing"');
|
|
131
|
+
changed = true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Sections with loaders that use device → add sync (needs SSR device detection)
|
|
135
|
+
if (
|
|
136
|
+
sectionMeta.hasLoader &&
|
|
137
|
+
sectionMeta.loaderUsesDevice &&
|
|
138
|
+
!result.includes("export const sync")
|
|
139
|
+
) {
|
|
140
|
+
result += "\nexport const sync = true;\n";
|
|
141
|
+
notes.push("Added: export const sync = true (loader uses device)");
|
|
142
|
+
changed = true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Sections that render nested Section children need sync so they're in
|
|
146
|
+
// the syncComponents registry (SectionRenderer resolves the string key).
|
|
147
|
+
const hasNestedSections =
|
|
148
|
+
/children:\s*Section\b/.test(result) ||
|
|
149
|
+
/fallback:\s*Section\b/.test(result);
|
|
150
|
+
if (hasNestedSections && !result.includes("export const sync")) {
|
|
151
|
+
result += "\nexport const sync = true;\n";
|
|
152
|
+
notes.push(
|
|
153
|
+
"Added: export const sync = true (renders nested Section children)",
|
|
154
|
+
);
|
|
155
|
+
changed = true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Re-export sections that wrap PDP/nested content need sync too.
|
|
159
|
+
// Detect: file is a re-export AND the target component renders nested Sections
|
|
160
|
+
const isReExport = /^export\s+\{[^}]*default[^}]*\}\s+from\s+/.test(
|
|
161
|
+
result.trim(),
|
|
162
|
+
);
|
|
163
|
+
if (
|
|
164
|
+
isReExport &&
|
|
165
|
+
(basename === "MountedPDP" || basename === "NotFoundChallenge")
|
|
166
|
+
) {
|
|
167
|
+
if (!result.includes("export const sync")) {
|
|
168
|
+
result += "\nexport const sync = true;\n";
|
|
169
|
+
notes.push(`Added: export const sync = true (re-export: ${basename})`);
|
|
170
|
+
changed = true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Don't add LoadingFallback re-exports to thin section files —
|
|
175
|
+
// we can't guarantee the target component exports it.
|
|
176
|
+
// Instead, if it's a listing section, a generic skeleton will be added below.
|
|
177
|
+
|
|
178
|
+
// Generate a basic LoadingFallback if the section doesn't have one
|
|
179
|
+
// and it's a listing section (visible skeleton improvement)
|
|
180
|
+
if (
|
|
181
|
+
sectionMeta.isListing &&
|
|
182
|
+
!sectionMeta.hasLoadingFallback &&
|
|
183
|
+
!result.includes("LoadingFallback")
|
|
184
|
+
) {
|
|
185
|
+
result += `
|
|
161
186
|
export function LoadingFallback() {
|
|
162
187
|
return (
|
|
163
188
|
<div className="w-full py-8">
|
|
@@ -177,9 +202,9 @@ export function LoadingFallback() {
|
|
|
177
202
|
);
|
|
178
203
|
}
|
|
179
204
|
`;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
205
|
+
notes.push("Added: LoadingFallback skeleton for listing section");
|
|
206
|
+
changed = true;
|
|
207
|
+
}
|
|
183
208
|
|
|
184
|
-
|
|
209
|
+
return { content: result, changed, notes };
|
|
185
210
|
}
|
package/scripts/migrate/types.ts
CHANGED
|
@@ -137,6 +137,14 @@ export interface MigrationContext {
|
|
|
137
137
|
vtexAccount: string | null;
|
|
138
138
|
gtmId: string | null;
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Per-site config loaded from `.deco-migrate.config.json`. `null` means
|
|
142
|
+
* no config file was present — defaults apply throughout. Imported
|
|
143
|
+
* lazily as `MigrateConfig` to avoid a circular dependency between
|
|
144
|
+
* `types.ts` and `config.ts`.
|
|
145
|
+
*/
|
|
146
|
+
config?: import("./config").MigrateConfig | null;
|
|
147
|
+
|
|
140
148
|
/** deno.json import map entries */
|
|
141
149
|
importMap: Record<string, string>;
|
|
142
150
|
|
|
@@ -191,7 +199,11 @@ export interface TransformResult {
|
|
|
191
199
|
|
|
192
200
|
export function createContext(
|
|
193
201
|
sourceDir: string,
|
|
194
|
-
opts: {
|
|
202
|
+
opts: {
|
|
203
|
+
dryRun?: boolean;
|
|
204
|
+
verbose?: boolean;
|
|
205
|
+
config?: import("./config").MigrateConfig | null;
|
|
206
|
+
} = {},
|
|
195
207
|
): MigrationContext {
|
|
196
208
|
return {
|
|
197
209
|
sourceDir,
|
|
@@ -199,6 +211,7 @@ export function createContext(
|
|
|
199
211
|
platform: "custom",
|
|
200
212
|
vtexAccount: null,
|
|
201
213
|
gtmId: null,
|
|
214
|
+
config: opts.config ?? null,
|
|
202
215
|
importMap: {},
|
|
203
216
|
discoveredNpmDeps: {},
|
|
204
217
|
themeColors: {},
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Post-Migration Cleanup Audit
|
|
4
|
+
*
|
|
5
|
+
* Read-only audit that scans a migrated site for dead code and obsolete
|
|
6
|
+
* boilerplate that the framework now owns. Mirrors the human checklist at
|
|
7
|
+
* `.agents/skills/deco-to-tanstack-migration/references/post-migration-cleanup.md`
|
|
8
|
+
* but turns it into something CI can actually run.
|
|
9
|
+
*
|
|
10
|
+
* Usage (from a migrated site directory):
|
|
11
|
+
* npx -p @decocms/start deco-post-cleanup
|
|
12
|
+
* npx -p @decocms/start deco-post-cleanup --json
|
|
13
|
+
*
|
|
14
|
+
* Options:
|
|
15
|
+
* --source <dir> Site directory to audit (default: current directory)
|
|
16
|
+
* --json Emit machine-readable JSON instead of pretty text
|
|
17
|
+
* --strict Exit code 2 if any warning-severity findings exist
|
|
18
|
+
* --help, -h Show this help
|
|
19
|
+
*
|
|
20
|
+
* This script is intentionally read-only. Auto-fix support (`--fix`) is
|
|
21
|
+
* a planned follow-up — see the SKILL doc.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import * as path from "node:path";
|
|
25
|
+
import { banner, bold, gray, green, red, yellow } from "./migrate/colors";
|
|
26
|
+
import { realFsAdapter, runAudit } from "./migrate/post-cleanup/runner";
|
|
27
|
+
import type { AuditReport, Severity } from "./migrate/post-cleanup/types";
|
|
28
|
+
|
|
29
|
+
interface CliOpts {
|
|
30
|
+
source: string;
|
|
31
|
+
json: boolean;
|
|
32
|
+
strict: boolean;
|
|
33
|
+
help: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseArgs(args: string[]): CliOpts {
|
|
37
|
+
let source = ".";
|
|
38
|
+
let json = false;
|
|
39
|
+
let strict = false;
|
|
40
|
+
let help = false;
|
|
41
|
+
for (let i = 0; i < args.length; i++) {
|
|
42
|
+
switch (args[i]) {
|
|
43
|
+
case "--source":
|
|
44
|
+
source = args[++i];
|
|
45
|
+
break;
|
|
46
|
+
case "--json":
|
|
47
|
+
json = true;
|
|
48
|
+
break;
|
|
49
|
+
case "--strict":
|
|
50
|
+
strict = true;
|
|
51
|
+
break;
|
|
52
|
+
case "--help":
|
|
53
|
+
case "-h":
|
|
54
|
+
help = true;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { source, json, strict, help };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function showHelp() {
|
|
62
|
+
console.log(`
|
|
63
|
+
@decocms/start — Post-Migration Cleanup Audit
|
|
64
|
+
|
|
65
|
+
Scans a migrated site for dead code and obsolete boilerplate that the
|
|
66
|
+
framework now owns. Read-only — prints findings, does not modify files.
|
|
67
|
+
|
|
68
|
+
Usage:
|
|
69
|
+
npx -p @decocms/start deco-post-cleanup [options]
|
|
70
|
+
|
|
71
|
+
Options:
|
|
72
|
+
--source <dir> Site directory to audit (default: .)
|
|
73
|
+
--json Emit machine-readable JSON instead of pretty text
|
|
74
|
+
--strict Exit code 2 if any warning-severity findings exist
|
|
75
|
+
--help, -h Show this help
|
|
76
|
+
|
|
77
|
+
Examples:
|
|
78
|
+
npx -p @decocms/start deco-post-cleanup
|
|
79
|
+
npx -p @decocms/start deco-post-cleanup --source ./my-site --json
|
|
80
|
+
|
|
81
|
+
See: .agents/skills/deco-to-tanstack-migration/references/post-migration-cleanup.md
|
|
82
|
+
`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function severityColor(sev: Severity, text: string): string {
|
|
86
|
+
if (sev === "warning") return yellow(text);
|
|
87
|
+
return gray(text);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function printText(report: AuditReport): void {
|
|
91
|
+
banner("Post-Migration Cleanup Audit");
|
|
92
|
+
console.log(` ${gray("Site:")} ${bold(report.site)}`);
|
|
93
|
+
console.log(` ${gray("Findings:")} ${bold(String(report.totalFindings))}`);
|
|
94
|
+
console.log("");
|
|
95
|
+
|
|
96
|
+
let idx = 0;
|
|
97
|
+
for (const summary of report.rules) {
|
|
98
|
+
idx++;
|
|
99
|
+
const count = summary.findings.length;
|
|
100
|
+
const headColor = count === 0 ? green : yellow;
|
|
101
|
+
console.log(`${headColor(`[${idx}] ${summary.title}`)} ${gray(`(${count} found)`)}`);
|
|
102
|
+
for (const f of summary.findings) {
|
|
103
|
+
const tag = severityColor(f.severity, `[${f.severity.toUpperCase()}]`);
|
|
104
|
+
console.log(` ${tag} ${bold(f.file)} — ${f.message}`);
|
|
105
|
+
if (f.fix) console.log(` ${gray("fix:")} ${f.fix}`);
|
|
106
|
+
}
|
|
107
|
+
if (count === 0) console.log(` ${gray("(nothing to clean up)")}`);
|
|
108
|
+
console.log("");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const warnings = report.rules
|
|
112
|
+
.flatMap((r) => r.findings)
|
|
113
|
+
.filter((f) => f.severity === "warning").length;
|
|
114
|
+
const infos = report.totalFindings - warnings;
|
|
115
|
+
console.log(
|
|
116
|
+
`${bold("Summary:")} ${report.totalFindings} finding(s) — ${yellow(`${warnings} warning(s)`)}, ${gray(`${infos} info`)}`,
|
|
117
|
+
);
|
|
118
|
+
if (report.totalFindings > 0) {
|
|
119
|
+
console.log(gray(" See post-migration-cleanup.md for the canonical fix steps per rule."));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function printJson(report: AuditReport): void {
|
|
124
|
+
console.log(JSON.stringify(report, null, 2));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function shouldFail(report: AuditReport, strict: boolean): boolean {
|
|
128
|
+
if (!strict) return false;
|
|
129
|
+
return report.rules.some((r) => r.findings.some((f) => f.severity === "warning"));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function main() {
|
|
133
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
134
|
+
if (opts.help) {
|
|
135
|
+
showHelp();
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const siteDir = path.resolve(opts.source);
|
|
140
|
+
const report = runAudit(siteDir, realFsAdapter);
|
|
141
|
+
|
|
142
|
+
if (opts.json) {
|
|
143
|
+
printJson(report);
|
|
144
|
+
} else {
|
|
145
|
+
printText(report);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (shouldFail(report, opts.strict)) {
|
|
149
|
+
process.exit(2);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
main().catch((err) => {
|
|
154
|
+
console.error(red(`Audit failed: ${(err as Error).message}`));
|
|
155
|
+
process.exit(1);
|
|
156
|
+
});
|
package/scripts/migrate.ts
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
import * as path from "node:path";
|
|
27
27
|
import { execSync } from "node:child_process";
|
|
28
|
+
import { loadConfig, validateConfig } from "./migrate/config";
|
|
28
29
|
import { createContext, logPhase } from "./migrate/types";
|
|
29
30
|
import { analyze } from "./migrate/phase-analyze";
|
|
30
31
|
import { scaffold } from "./migrate/phase-scaffold";
|
|
@@ -121,9 +122,19 @@ async function main() {
|
|
|
121
122
|
stat("Mode", opts.dryRun ? yellow("DRY RUN") : green("EXECUTE"));
|
|
122
123
|
stat("Verbose", opts.verbose ? "yes" : "no");
|
|
123
124
|
|
|
125
|
+
// Load optional per-site config from `.deco-migrate.config.json`. Drives
|
|
126
|
+
// section-conventions hardcoded lists today; future fields will tune
|
|
127
|
+
// import rewrites, scaffolding, etc.
|
|
128
|
+
const siteConfig = loadConfig(sourceDir);
|
|
129
|
+
if (siteConfig) {
|
|
130
|
+
validateConfig(siteConfig);
|
|
131
|
+
stat("Config", green(".deco-migrate.config.json (loaded)"));
|
|
132
|
+
}
|
|
133
|
+
|
|
124
134
|
const ctx = createContext(sourceDir, {
|
|
125
135
|
dryRun: opts.dryRun,
|
|
126
136
|
verbose: opts.verbose,
|
|
137
|
+
config: siteConfig,
|
|
127
138
|
});
|
|
128
139
|
|
|
129
140
|
try {
|