@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
|
@@ -34,6 +34,33 @@ npx tsx node_modules/@decocms/start/scripts/migrate.ts --source /path/to/old-sit
|
|
|
34
34
|
|
|
35
35
|
**CI usage:** pair `--strict` with `--with-build` to catch both type and runtime regressions before merge.
|
|
36
36
|
|
|
37
|
+
### Per-site config: `.deco-migrate.config.json`
|
|
38
|
+
|
|
39
|
+
Optional JSON file at the source root that customises the migration for sites whose section names don't match the casaevideo-derived defaults baked into the script.
|
|
40
|
+
|
|
41
|
+
```jsonc
|
|
42
|
+
{
|
|
43
|
+
"sectionConventions": {
|
|
44
|
+
// Add to defaults — preferred for sites that share most defaults.
|
|
45
|
+
"extend": {
|
|
46
|
+
"sync": ["MyCustomShelf"],
|
|
47
|
+
"listingCache": ["MyCustomShelf"],
|
|
48
|
+
"staticCache": ["AboutUs", "PrivacyPolicy"]
|
|
49
|
+
}
|
|
50
|
+
// Or replace defaults entirely (rare):
|
|
51
|
+
// "replace": { "sync": [...], "eagerSync": [...], ... }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Categories:**
|
|
57
|
+
- `eagerSync` — section files registered as both eager and sync (rendered above-the-fold, no client-defer).
|
|
58
|
+
- `sync` — registered as sync only (server-side default applies for loading).
|
|
59
|
+
- `listingCache` — emit `export const cache = "listing"` (medium TTL).
|
|
60
|
+
- `staticCache` — emit `export const cache = "static"` (long TTL).
|
|
61
|
+
|
|
62
|
+
When the file is absent the baked-in casaevideo defaults apply, so existing migrations are unaffected.
|
|
63
|
+
|
|
37
64
|
## Architecture
|
|
38
65
|
|
|
39
66
|
```
|
|
@@ -5,6 +5,29 @@ recurring set of dead-code and boilerplate cleanup that every migrated
|
|
|
5
5
|
site benefits from. Run this checklist before the first PR review, not
|
|
6
6
|
after the site has been shipping for weeks.
|
|
7
7
|
|
|
8
|
+
## Run the audit first
|
|
9
|
+
|
|
10
|
+
This whole checklist is now automated by the **`deco-post-cleanup`**
|
|
11
|
+
audit script (added in `@decocms/start >= 2.11.0`). Run it from the
|
|
12
|
+
site repo to get a structured report of which sections below actually
|
|
13
|
+
apply to your codebase:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Pretty text output, exits 0 unless --strict is passed
|
|
17
|
+
npx -p @decocms/start deco-post-cleanup
|
|
18
|
+
|
|
19
|
+
# Machine-readable JSON for CI dashboards
|
|
20
|
+
npx -p @decocms/start deco-post-cleanup --json
|
|
21
|
+
|
|
22
|
+
# Fail the run (exit 2) if any warning-severity findings exist
|
|
23
|
+
npx -p @decocms/start deco-post-cleanup --strict
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The audit covers all 7 rules below and prints the exact file path +
|
|
27
|
+
suggested fix for each finding. It is **read-only** — auto-fix support
|
|
28
|
+
is a planned follow-up. Use the audit to scope this checklist to the
|
|
29
|
+
real, current findings instead of triaging the full document by hand.
|
|
30
|
+
|
|
8
31
|
## 1. Delete unused `src/lib/*` shims
|
|
9
32
|
|
|
10
33
|
The migration script's `templates/lib-utils.ts` generates 11 shim files
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/start",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.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",
|
|
7
7
|
"bin": {
|
|
8
|
-
"deco-migrate": "./scripts/migrate.ts"
|
|
8
|
+
"deco-migrate": "./scripts/migrate.ts",
|
|
9
|
+
"deco-post-cleanup": "./scripts/migrate-post-cleanup.ts"
|
|
9
10
|
},
|
|
10
11
|
"exports": {
|
|
11
12
|
".": "./src/index.ts",
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
"./scripts/generate-schema": "./scripts/generate-schema.ts",
|
|
60
61
|
"./scripts/generate-invoke": "./scripts/generate-invoke.ts",
|
|
61
62
|
"./scripts/migrate": "./scripts/migrate.ts",
|
|
63
|
+
"./scripts/migrate-post-cleanup": "./scripts/migrate-post-cleanup.ts",
|
|
62
64
|
"./scripts/tailwind-lint": "./scripts/tailwind-lint.ts",
|
|
63
65
|
"./vite": "./src/vite/plugin.js",
|
|
64
66
|
"./daemon": "./src/daemon/index.ts"
|
|
@@ -0,0 +1,202 @@
|
|
|
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 } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_SECTION_CONVENTIONS,
|
|
7
|
+
loadConfig,
|
|
8
|
+
resolveSectionConventions,
|
|
9
|
+
validateConfig,
|
|
10
|
+
} from "./config";
|
|
11
|
+
|
|
12
|
+
describe("loadConfig", () => {
|
|
13
|
+
let tmpDir: string;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "config-test-"));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("returns null when the config file is missing", () => {
|
|
24
|
+
expect(loadConfig(tmpDir)).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("loads valid JSON", () => {
|
|
28
|
+
const content = JSON.stringify({
|
|
29
|
+
sectionConventions: { extend: { sync: ["MySection"] } },
|
|
30
|
+
});
|
|
31
|
+
fs.writeFileSync(
|
|
32
|
+
path.join(tmpDir, ".deco-migrate.config.json"),
|
|
33
|
+
content,
|
|
34
|
+
"utf-8",
|
|
35
|
+
);
|
|
36
|
+
const config = loadConfig(tmpDir);
|
|
37
|
+
expect(config).toEqual({
|
|
38
|
+
sectionConventions: { extend: { sync: ["MySection"] } },
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("throws with a helpful error on malformed JSON", () => {
|
|
43
|
+
fs.writeFileSync(
|
|
44
|
+
path.join(tmpDir, ".deco-migrate.config.json"),
|
|
45
|
+
"not valid json{",
|
|
46
|
+
"utf-8",
|
|
47
|
+
);
|
|
48
|
+
expect(() => loadConfig(tmpDir)).toThrow(
|
|
49
|
+
/Failed to parse.*Expected valid JSON/s,
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("resolveSectionConventions", () => {
|
|
55
|
+
it("returns the defaults when config is null", () => {
|
|
56
|
+
const sets = resolveSectionConventions(null);
|
|
57
|
+
expect(sets.eagerSync.has("UtilLinks")).toBe(true);
|
|
58
|
+
expect(sets.sync.has("ProductShelf")).toBe(true);
|
|
59
|
+
expect(sets.listingCache.has("ProductShelf")).toBe(true);
|
|
60
|
+
expect(sets.staticCache.has("Faq")).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("preserves all default eagerSync entries", () => {
|
|
64
|
+
const sets = resolveSectionConventions(null);
|
|
65
|
+
for (const name of DEFAULT_SECTION_CONVENTIONS.eagerSync ?? []) {
|
|
66
|
+
expect(sets.eagerSync.has(name)).toBe(true);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("extend mode adds to defaults", () => {
|
|
71
|
+
const sets = resolveSectionConventions({
|
|
72
|
+
sectionConventions: {
|
|
73
|
+
extend: { sync: ["MySection"], staticCache: ["MyStatic"] },
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
// Default still present
|
|
77
|
+
expect(sets.sync.has("ProductShelf")).toBe(true);
|
|
78
|
+
// Extension added
|
|
79
|
+
expect(sets.sync.has("MySection")).toBe(true);
|
|
80
|
+
// Default static still present
|
|
81
|
+
expect(sets.staticCache.has("Faq")).toBe(true);
|
|
82
|
+
// Extension added
|
|
83
|
+
expect(sets.staticCache.has("MyStatic")).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("extend mode handles partial extensions (only some categories)", () => {
|
|
87
|
+
const sets = resolveSectionConventions({
|
|
88
|
+
sectionConventions: { extend: { eagerSync: ["FrontFacing"] } },
|
|
89
|
+
});
|
|
90
|
+
// All defaults still present in untouched categories
|
|
91
|
+
expect(sets.sync.has("ProductShelf")).toBe(true);
|
|
92
|
+
expect(sets.staticCache.has("Faq")).toBe(true);
|
|
93
|
+
// Extension added
|
|
94
|
+
expect(sets.eagerSync.has("FrontFacing")).toBe(true);
|
|
95
|
+
// Defaults still present in extended category
|
|
96
|
+
expect(sets.eagerSync.has("UtilLinks")).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("replace mode discards defaults", () => {
|
|
100
|
+
const sets = resolveSectionConventions({
|
|
101
|
+
sectionConventions: {
|
|
102
|
+
replace: { sync: ["OnlyThis"] },
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
// Default removed
|
|
106
|
+
expect(sets.sync.has("ProductShelf")).toBe(false);
|
|
107
|
+
// Replacement present
|
|
108
|
+
expect(sets.sync.has("OnlyThis")).toBe(true);
|
|
109
|
+
// Untouched categories empty
|
|
110
|
+
expect(sets.eagerSync.size).toBe(0);
|
|
111
|
+
expect(sets.listingCache.size).toBe(0);
|
|
112
|
+
expect(sets.staticCache.size).toBe(0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("replace mode is full replacement, not partial overlay", () => {
|
|
116
|
+
const sets = resolveSectionConventions({
|
|
117
|
+
sectionConventions: {
|
|
118
|
+
replace: {
|
|
119
|
+
eagerSync: ["X"],
|
|
120
|
+
sync: ["Y"],
|
|
121
|
+
listingCache: ["Z"],
|
|
122
|
+
staticCache: ["W"],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
expect(Array.from(sets.eagerSync)).toEqual(["X"]);
|
|
127
|
+
expect(Array.from(sets.sync)).toEqual(["Y"]);
|
|
128
|
+
expect(Array.from(sets.listingCache)).toEqual(["Z"]);
|
|
129
|
+
expect(Array.from(sets.staticCache)).toEqual(["W"]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("returns empty sets when given empty arrays in replace", () => {
|
|
133
|
+
const sets = resolveSectionConventions({
|
|
134
|
+
sectionConventions: { replace: {} },
|
|
135
|
+
});
|
|
136
|
+
expect(sets.eagerSync.size).toBe(0);
|
|
137
|
+
expect(sets.sync.size).toBe(0);
|
|
138
|
+
expect(sets.listingCache.size).toBe(0);
|
|
139
|
+
expect(sets.staticCache.size).toBe(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("returns defaults when sectionConventions is undefined", () => {
|
|
143
|
+
const sets = resolveSectionConventions({});
|
|
144
|
+
expect(sets.sync.has("ProductShelf")).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("validateConfig", () => {
|
|
149
|
+
it("accepts an empty config", () => {
|
|
150
|
+
expect(() => validateConfig({})).not.toThrow();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("accepts a config with extend", () => {
|
|
154
|
+
expect(() =>
|
|
155
|
+
validateConfig({
|
|
156
|
+
sectionConventions: { extend: { sync: ["Foo"] } },
|
|
157
|
+
}),
|
|
158
|
+
).not.toThrow();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("accepts a config with replace", () => {
|
|
162
|
+
expect(() =>
|
|
163
|
+
validateConfig({
|
|
164
|
+
sectionConventions: { replace: { sync: ["Foo"] } },
|
|
165
|
+
}),
|
|
166
|
+
).not.toThrow();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("rejects non-object root", () => {
|
|
170
|
+
expect(() => validateConfig("bad")).toThrow(
|
|
171
|
+
/must be a JSON object/,
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("rejects non-object sectionConventions", () => {
|
|
176
|
+
expect(() => validateConfig({ sectionConventions: "bad" })).toThrow(
|
|
177
|
+
/sectionConventions must be an object/,
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("rejects non-array values in convention lists", () => {
|
|
182
|
+
expect(() =>
|
|
183
|
+
validateConfig({
|
|
184
|
+
sectionConventions: { extend: { sync: "not-an-array" } },
|
|
185
|
+
}),
|
|
186
|
+
).toThrow(/must be string\[\]/);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("rejects non-string entries in convention lists", () => {
|
|
190
|
+
expect(() =>
|
|
191
|
+
validateConfig({
|
|
192
|
+
sectionConventions: { extend: { sync: [1, 2, 3] } },
|
|
193
|
+
}),
|
|
194
|
+
).toThrow(/must be string\[\]/);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("ignores unknown top-level fields gracefully", () => {
|
|
198
|
+
expect(() =>
|
|
199
|
+
validateConfig({ unknownField: 123, sectionConventions: {} }),
|
|
200
|
+
).not.toThrow();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-site migration configuration.
|
|
3
|
+
*
|
|
4
|
+
* Looks for `.deco-migrate.config.json` next to the source root. The file
|
|
5
|
+
* is optional — without it the script falls back to a baked-in default set
|
|
6
|
+
* of section-convention names that work for `casaevideo` and most other
|
|
7
|
+
* Deco/VTEX sites that derived from the same template.
|
|
8
|
+
*
|
|
9
|
+
* The defaults are kept here (not in `transforms/section-conventions.ts`)
|
|
10
|
+
* so that:
|
|
11
|
+
* 1. They live alongside the schema that consumes them.
|
|
12
|
+
* 2. They can be overridden per site without forking the transform.
|
|
13
|
+
* 3. Other phases (analyze, report) can read them too.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Names of section files that get framework hints applied during the
|
|
21
|
+
* `transformSectionConventions` step.
|
|
22
|
+
*
|
|
23
|
+
* - `eagerSync`: render server-side eagerly *and* register as sync (no
|
|
24
|
+
* client-side defer).
|
|
25
|
+
* - `sync`: register as sync (sectionLoaders.sync), but server-side
|
|
26
|
+
* loading remains the default.
|
|
27
|
+
* - `listingCache`: `export const cache = "listing"` (medium TTL).
|
|
28
|
+
* - `staticCache`: `export const cache = "static"` (long TTL).
|
|
29
|
+
*/
|
|
30
|
+
export interface SectionConventionConfig {
|
|
31
|
+
eagerSync?: string[];
|
|
32
|
+
sync?: string[];
|
|
33
|
+
listingCache?: string[];
|
|
34
|
+
staticCache?: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface MigrateConfig {
|
|
38
|
+
sectionConventions?: {
|
|
39
|
+
/**
|
|
40
|
+
* Replace the built-in defaults entirely. Use only when porting a
|
|
41
|
+
* site whose section names don't overlap the defaults at all.
|
|
42
|
+
*/
|
|
43
|
+
replace?: SectionConventionConfig;
|
|
44
|
+
/**
|
|
45
|
+
* Add to the built-in defaults. Recommended path for sites that
|
|
46
|
+
* share most defaults but have a few extra section names.
|
|
47
|
+
*/
|
|
48
|
+
extend?: SectionConventionConfig;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolved sets used by `transformSectionConventions`. Always provided —
|
|
54
|
+
* either from defaults, from config replace, or defaults+config.extend.
|
|
55
|
+
*/
|
|
56
|
+
export interface SectionConventionSets {
|
|
57
|
+
eagerSync: Set<string>;
|
|
58
|
+
sync: Set<string>;
|
|
59
|
+
listingCache: Set<string>;
|
|
60
|
+
staticCache: Set<string>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Built-in defaults. Originally extracted from `casaevideo` migration —
|
|
65
|
+
* these names are common across Deco/VTEX storefronts that share the
|
|
66
|
+
* lineage. Sites that don't have these sections are unaffected (the
|
|
67
|
+
* matcher just never fires).
|
|
68
|
+
*/
|
|
69
|
+
export const DEFAULT_SECTION_CONVENTIONS: SectionConventionConfig = {
|
|
70
|
+
eagerSync: [
|
|
71
|
+
"UtilLinks",
|
|
72
|
+
"DepartamentList",
|
|
73
|
+
"ImageGallery",
|
|
74
|
+
"BannersGrid",
|
|
75
|
+
"Carousel",
|
|
76
|
+
"Tipbar",
|
|
77
|
+
"Live",
|
|
78
|
+
],
|
|
79
|
+
sync: [
|
|
80
|
+
"ProductShelf",
|
|
81
|
+
"ProductShelfTabbed",
|
|
82
|
+
"ProductShelfGroup",
|
|
83
|
+
"ProductShelfTopSort",
|
|
84
|
+
"CouponList",
|
|
85
|
+
"NotFoundChallenge",
|
|
86
|
+
"MountedPDP",
|
|
87
|
+
"BackgroundWrapper",
|
|
88
|
+
"SearchResult",
|
|
89
|
+
"LpCartao",
|
|
90
|
+
],
|
|
91
|
+
listingCache: [
|
|
92
|
+
"ProductShelf",
|
|
93
|
+
"ProductShelfTabbed",
|
|
94
|
+
"ProductShelfGroup",
|
|
95
|
+
"ProductShelfTimedOffers",
|
|
96
|
+
],
|
|
97
|
+
staticCache: ["InstagramPosts", "Faq"],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** Load `.deco-migrate.config.json` from the source dir, if present. */
|
|
101
|
+
export function loadConfig(sourceDir: string): MigrateConfig | null {
|
|
102
|
+
const configPath = path.join(sourceDir, ".deco-migrate.config.json");
|
|
103
|
+
if (!fs.existsSync(configPath)) return null;
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(fs.readFileSync(configPath, "utf-8")) as MigrateConfig;
|
|
106
|
+
} catch (e) {
|
|
107
|
+
const msg = (e as Error).message;
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Failed to parse ${configPath}: ${msg}. Expected valid JSON.`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Resolve config + defaults into the four sets the transform consumes. */
|
|
115
|
+
export function resolveSectionConventions(
|
|
116
|
+
config: MigrateConfig | null,
|
|
117
|
+
): SectionConventionSets {
|
|
118
|
+
const sc = config?.sectionConventions;
|
|
119
|
+
|
|
120
|
+
// Replace mode: use only what the user provided. No defaults mixed in.
|
|
121
|
+
if (sc?.replace) {
|
|
122
|
+
return toSets(sc.replace);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Default + extend: start from defaults, union in extend lists.
|
|
126
|
+
const merged: SectionConventionConfig = {
|
|
127
|
+
eagerSync: [
|
|
128
|
+
...(DEFAULT_SECTION_CONVENTIONS.eagerSync ?? []),
|
|
129
|
+
...(sc?.extend?.eagerSync ?? []),
|
|
130
|
+
],
|
|
131
|
+
sync: [
|
|
132
|
+
...(DEFAULT_SECTION_CONVENTIONS.sync ?? []),
|
|
133
|
+
...(sc?.extend?.sync ?? []),
|
|
134
|
+
],
|
|
135
|
+
listingCache: [
|
|
136
|
+
...(DEFAULT_SECTION_CONVENTIONS.listingCache ?? []),
|
|
137
|
+
...(sc?.extend?.listingCache ?? []),
|
|
138
|
+
],
|
|
139
|
+
staticCache: [
|
|
140
|
+
...(DEFAULT_SECTION_CONVENTIONS.staticCache ?? []),
|
|
141
|
+
...(sc?.extend?.staticCache ?? []),
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
return toSets(merged);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function toSets(c: SectionConventionConfig): SectionConventionSets {
|
|
148
|
+
return {
|
|
149
|
+
eagerSync: new Set(c.eagerSync ?? []),
|
|
150
|
+
sync: new Set(c.sync ?? []),
|
|
151
|
+
listingCache: new Set(c.listingCache ?? []),
|
|
152
|
+
staticCache: new Set(c.staticCache ?? []),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Cheap structural validation — throws on obviously invalid shapes. */
|
|
157
|
+
export function validateConfig(config: unknown): asserts config is MigrateConfig {
|
|
158
|
+
if (config === null || typeof config !== "object") {
|
|
159
|
+
throw new Error(".deco-migrate.config.json must be a JSON object");
|
|
160
|
+
}
|
|
161
|
+
const c = config as Record<string, unknown>;
|
|
162
|
+
const sc = c.sectionConventions;
|
|
163
|
+
if (sc === undefined) return;
|
|
164
|
+
if (typeof sc !== "object" || sc === null) {
|
|
165
|
+
throw new Error("sectionConventions must be an object");
|
|
166
|
+
}
|
|
167
|
+
const scObj = sc as Record<string, unknown>;
|
|
168
|
+
for (const key of ["replace", "extend"] as const) {
|
|
169
|
+
const v = scObj[key];
|
|
170
|
+
if (v === undefined) continue;
|
|
171
|
+
if (typeof v !== "object" || v === null) {
|
|
172
|
+
throw new Error(`sectionConventions.${key} must be an object`);
|
|
173
|
+
}
|
|
174
|
+
const sub = v as Record<string, unknown>;
|
|
175
|
+
for (const f of ["eagerSync", "sync", "listingCache", "staticCache"]) {
|
|
176
|
+
const arr = sub[f];
|
|
177
|
+
if (arr === undefined) continue;
|
|
178
|
+
if (
|
|
179
|
+
!Array.isArray(arr) ||
|
|
180
|
+
!arr.every((s): s is string => typeof s === "string")
|
|
181
|
+
) {
|
|
182
|
+
throw new Error(`sectionConventions.${key}.${f} must be string[]`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { resolveSectionConventions } from "./config";
|
|
3
4
|
import type { MigrationContext, TransformResult, SectionMeta } from "./types";
|
|
4
5
|
import { log, logPhase } from "./types";
|
|
5
6
|
import { transformImports } from "./transforms/imports";
|
|
@@ -8,7 +9,7 @@ import { transformFreshApis } from "./transforms/fresh-apis";
|
|
|
8
9
|
import { transformDenoIsms } from "./transforms/deno-isms";
|
|
9
10
|
import { transformTailwind } from "./transforms/tailwind";
|
|
10
11
|
import { transformDeadCode } from "./transforms/dead-code";
|
|
11
|
-
import {
|
|
12
|
+
import { createSectionConventionsTransform } from "./transforms/section-conventions";
|
|
12
13
|
|
|
13
14
|
/** Map of section path → metadata, populated per-run */
|
|
14
15
|
let sectionMetaMap: Map<string, SectionMeta> | null = null;
|
|
@@ -23,6 +24,22 @@ function getSectionMeta(ctx: MigrationContext, relPath: string): SectionMeta | u
|
|
|
23
24
|
return sectionMetaMap.get(relPath);
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Cached per-run section-conventions closure. Built once from the
|
|
29
|
+
* resolved config sets (`ctx.config.sectionConventions`), so casaevideo
|
|
30
|
+
* defaults still apply when no config file exists.
|
|
31
|
+
*/
|
|
32
|
+
let cachedSectionTransform:
|
|
33
|
+
| ReturnType<typeof createSectionConventionsTransform>
|
|
34
|
+
| null = null;
|
|
35
|
+
|
|
36
|
+
function getSectionConventionsTransform(ctx: MigrationContext) {
|
|
37
|
+
if (cachedSectionTransform) return cachedSectionTransform;
|
|
38
|
+
const sets = resolveSectionConventions(ctx.config ?? null);
|
|
39
|
+
cachedSectionTransform = createSectionConventionsTransform(sets);
|
|
40
|
+
return cachedSectionTransform;
|
|
41
|
+
}
|
|
42
|
+
|
|
26
43
|
/**
|
|
27
44
|
* Apply all transforms to a file's content in the correct order.
|
|
28
45
|
*/
|
|
@@ -59,7 +76,9 @@ function applyTransforms(content: string, filePath: string, ctx?: MigrationConte
|
|
|
59
76
|
// Section conventions (sync/eager/layout/cache) — only for section files
|
|
60
77
|
if (ctx && relPath && relPath.startsWith("sections/")) {
|
|
61
78
|
const meta = getSectionMeta(ctx, relPath);
|
|
62
|
-
|
|
79
|
+
// Build the closure once per ctx, cache it on the context.
|
|
80
|
+
const sectionTransform = getSectionConventionsTransform(ctx);
|
|
81
|
+
const result = sectionTransform(currentContent, meta);
|
|
63
82
|
if (result.changed) {
|
|
64
83
|
anyChanged = true;
|
|
65
84
|
currentContent = result.content;
|