@decocms/start 2.5.0 → 2.7.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-to-tanstack-migration/SKILL.md +1 -1
- package/.agents/skills/deco-to-tanstack-migration/references/post-migration-cleanup.md +33 -2
- package/.cursor/skills/deco-to-tanstack-migration/SKILL.md +1 -1
- package/.cursor/skills/deco-to-tanstack-migration/references/post-migration-cleanup.md +33 -2
- package/package.json +1 -1
- package/scripts/migrate/phase-cleanup.test.ts +141 -0
- package/scripts/migrate/phase-cleanup.ts +103 -0
- package/scripts/migrate/phase-scaffold.ts +7 -3
- package/scripts/migrate/phase-verify.ts +5 -2
- package/scripts/migrate/templates/lib-utils.test.ts +91 -0
- package/scripts/migrate/templates/lib-utils.ts +51 -19
- package/scripts/migrate/templates/types-gen.ts +6 -9
- package/scripts/migrate/transforms/imports.ts +6 -1
- package/vitest.config.ts +11 -1
|
@@ -136,7 +136,7 @@ See: `references/deco-framework/README.md`
|
|
|
136
136
|
|
|
137
137
|
**Actions**:
|
|
138
138
|
1. `from "apps/commerce/types.ts"` → `from "@decocms/apps/commerce/types"`
|
|
139
|
-
2. `from "apps/admin/widgets.ts"` → `from "
|
|
139
|
+
2. `from "apps/admin/widgets.ts"` → `from "@decocms/start/types/widgets"` (framework owns the string aliases — do **not** create a local `src/types/widgets.ts`)
|
|
140
140
|
3. `from "apps/website/components/Image.tsx"` → `from "~/components/ui/Image"` (create local)
|
|
141
141
|
4. SDK utilities: `~/sdk/useOffer` → `@decocms/apps/commerce/sdk/useOffer`, etc.
|
|
142
142
|
|
|
@@ -135,7 +135,38 @@ utility). Your runtime behavior gets MUCH better — segment cookies, IS
|
|
|
135
135
|
cookies, VTEX session auth all start working again instead of being
|
|
136
136
|
silently stubbed to `{}` / `null`.
|
|
137
137
|
|
|
138
|
-
## 6.
|
|
138
|
+
## 6. Drop `src/types/widgets.ts` — framework owns it
|
|
139
|
+
|
|
140
|
+
Older migrations scaffold a local `src/types/widgets.ts` containing 8
|
|
141
|
+
string-aliased widget types (`ImageWidget`, `HTMLWidget`, …). The
|
|
142
|
+
framework now exports the same set (plus `TextArea`) at
|
|
143
|
+
`@decocms/start/types/widgets`, and the schema generator detects the
|
|
144
|
+
widgets via type-text matching, so the local file is purely
|
|
145
|
+
duplicated boilerplate.
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# Quick check
|
|
149
|
+
rg -n "from ['\"]~/types/widgets['\"]" src/ | wc -l # >0 → cleanup applies
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Replace all imports in one pass:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
# macOS / BSD sed: drop the empty quotes after -i
|
|
156
|
+
rg -l "from ['\"]~/types/widgets['\"]" src/ \
|
|
157
|
+
| xargs sed -i '' "s|from ['\"]~/types/widgets['\"]|from \"@decocms/start/types/widgets\"|g"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Then delete the now-orphan local file:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
rm src/types/widgets.ts
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Confirm `tsc --noEmit` is still clean — the framework version is a
|
|
167
|
+
strict superset of what the migration script generated.
|
|
168
|
+
|
|
169
|
+
## 7. Search for orphan `TODO: move into framework` comments
|
|
139
170
|
|
|
140
171
|
Real sites accumulate `TODO` comments like `// TODO: move into decoVitePlugin
|
|
141
172
|
in next @decocms/start release`. These are roadmap items the framework
|
|
@@ -152,7 +183,7 @@ For each hit, decide:
|
|
|
152
183
|
|
|
153
184
|
## Verification checklist
|
|
154
185
|
|
|
155
|
-
After completing 1-
|
|
186
|
+
After completing 1-7:
|
|
156
187
|
|
|
157
188
|
- [ ] `npm run typecheck` baseline matches pre-cleanup count (no new errors)
|
|
158
189
|
- [ ] `npm run dev` starts and `/`, `/some-pdp/p`, `/s?q=foo` all render
|
|
@@ -218,7 +218,7 @@ See: `references/deco-framework/README.md`
|
|
|
218
218
|
|
|
219
219
|
**Actions**:
|
|
220
220
|
1. `from "apps/commerce/types.ts"` → `from "@decocms/apps/commerce/types"`
|
|
221
|
-
2. `from "apps/admin/widgets.ts"` → `from "
|
|
221
|
+
2. `from "apps/admin/widgets.ts"` → `from "@decocms/start/types/widgets"` (framework owns the string aliases — `ImageWidget`, `HTMLWidget`, etc.; do **not** create a local `src/types/widgets.ts`)
|
|
222
222
|
3. `from "apps/website/components/Image.tsx"` → `from "~/components/ui/Image"` (create local components)
|
|
223
223
|
4. SDK utilities: `~/sdk/useOffer` → `@decocms/apps/commerce/sdk/useOffer`, `~/sdk/format` → `@decocms/apps/commerce/sdk/formatPrice`, etc.
|
|
224
224
|
|
|
@@ -135,7 +135,38 @@ utility). Your runtime behavior gets MUCH better — segment cookies, IS
|
|
|
135
135
|
cookies, VTEX session auth all start working again instead of being
|
|
136
136
|
silently stubbed to `{}` / `null`.
|
|
137
137
|
|
|
138
|
-
## 6.
|
|
138
|
+
## 6. Drop `src/types/widgets.ts` — framework owns it
|
|
139
|
+
|
|
140
|
+
Older migrations scaffold a local `src/types/widgets.ts` containing 8
|
|
141
|
+
string-aliased widget types (`ImageWidget`, `HTMLWidget`, …). The
|
|
142
|
+
framework now exports the same set (plus `TextArea`) at
|
|
143
|
+
`@decocms/start/types/widgets`, and the schema generator detects the
|
|
144
|
+
widgets via type-text matching, so the local file is purely
|
|
145
|
+
duplicated boilerplate.
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# Quick check
|
|
149
|
+
rg -n "from ['\"]~/types/widgets['\"]" src/ | wc -l # >0 → cleanup applies
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Replace all imports in one pass:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
# macOS / BSD sed: drop the empty quotes after -i
|
|
156
|
+
rg -l "from ['\"]~/types/widgets['\"]" src/ \
|
|
157
|
+
| xargs sed -i '' "s|from ['\"]~/types/widgets['\"]|from \"@decocms/start/types/widgets\"|g"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Then delete the now-orphan local file:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
rm src/types/widgets.ts
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Confirm `tsc --noEmit` is still clean — the framework version is a
|
|
167
|
+
strict superset of what the migration script generated.
|
|
168
|
+
|
|
169
|
+
## 7. Search for orphan `TODO: move into framework` comments
|
|
139
170
|
|
|
140
171
|
Real sites accumulate `TODO` comments like `// TODO: move into decoVitePlugin
|
|
141
172
|
in next @decocms/start release`. These are roadmap items the framework
|
|
@@ -152,7 +183,7 @@ For each hit, decide:
|
|
|
152
183
|
|
|
153
184
|
## Verification checklist
|
|
154
185
|
|
|
155
|
-
After completing 1-
|
|
186
|
+
After completing 1-7:
|
|
156
187
|
|
|
157
188
|
- [ ] `npm run typecheck` baseline matches pre-cleanup count (no new errors)
|
|
158
189
|
- [ ] `npm run dev` starts and `/`, `/some-pdp/p`, `/s?q=foo` all render
|
package/package.json
CHANGED
|
@@ -0,0 +1,141 @@
|
|
|
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 { writeImportedLibShims } from "./phase-cleanup";
|
|
6
|
+
import type { MigrationContext } from "./types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build a minimal `MigrationContext` for integration tests of
|
|
10
|
+
* `writeImportedLibShims`. Only the fields that function reads are
|
|
11
|
+
* non-default; everything else is a placeholder with the right shape.
|
|
12
|
+
*/
|
|
13
|
+
function makeCtx(sourceDir: string, dryRun = false): MigrationContext {
|
|
14
|
+
return {
|
|
15
|
+
sourceDir,
|
|
16
|
+
siteName: "test-site",
|
|
17
|
+
platform: "vtex",
|
|
18
|
+
vtexAccount: null,
|
|
19
|
+
gtmId: null,
|
|
20
|
+
importMap: {},
|
|
21
|
+
discoveredNpmDeps: {},
|
|
22
|
+
themeColors: {},
|
|
23
|
+
fontFamily: null,
|
|
24
|
+
files: [],
|
|
25
|
+
sectionMetas: [],
|
|
26
|
+
islandClassifications: [],
|
|
27
|
+
islandWrapperTargets: new Map(),
|
|
28
|
+
loaderInventory: [],
|
|
29
|
+
scaffoldedFiles: [],
|
|
30
|
+
transformedFiles: [],
|
|
31
|
+
deletedFiles: [],
|
|
32
|
+
movedFiles: [],
|
|
33
|
+
manualReviewItems: [],
|
|
34
|
+
frameworkFindings: [],
|
|
35
|
+
dryRun,
|
|
36
|
+
verbose: false,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("writeImportedLibShims (integration)", () => {
|
|
41
|
+
let tmpDir: string;
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "lazy-shim-test-"));
|
|
45
|
+
fs.mkdirSync(path.join(tmpDir, "src", "loaders"), { recursive: true });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("writes nothing when no ~/lib imports are found", () => {
|
|
53
|
+
fs.writeFileSync(
|
|
54
|
+
path.join(tmpDir, "src", "loaders", "products.ts"),
|
|
55
|
+
`import { something } from "@decocms/apps/vtex";\nexport const x = 1;\n`,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
writeImportedLibShims(makeCtx(tmpDir));
|
|
59
|
+
|
|
60
|
+
expect(fs.existsSync(path.join(tmpDir, "src", "lib"))).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("writes only the shim files matching imports actually present in src/", () => {
|
|
64
|
+
fs.writeFileSync(
|
|
65
|
+
path.join(tmpDir, "src", "loaders", "search.ts"),
|
|
66
|
+
`import { getSegmentFromBag } from "~/lib/vtex-segment";\n` +
|
|
67
|
+
`import { toFilterSearchString } from "~/lib/filter-navigate";\n`,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
writeImportedLibShims(makeCtx(tmpDir));
|
|
71
|
+
|
|
72
|
+
const libDir = path.join(tmpDir, "src", "lib");
|
|
73
|
+
expect(fs.existsSync(libDir)).toBe(true);
|
|
74
|
+
const written = fs.readdirSync(libDir).sort();
|
|
75
|
+
expect(written).toEqual(["filter-navigate.ts", "vtex-segment.ts"]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("writes nothing in dry-run mode (but does not throw)", () => {
|
|
79
|
+
fs.writeFileSync(
|
|
80
|
+
path.join(tmpDir, "src", "loaders", "search.ts"),
|
|
81
|
+
`import { getSegmentFromBag } from "~/lib/vtex-segment";\n`,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
writeImportedLibShims(makeCtx(tmpDir, /* dryRun */ true));
|
|
85
|
+
|
|
86
|
+
expect(fs.existsSync(path.join(tmpDir, "src", "lib"))).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("strips trailing .ts from the import specifier when scanning", () => {
|
|
90
|
+
// Some Fresh sites use explicit .ts extensions in their imports.
|
|
91
|
+
fs.writeFileSync(
|
|
92
|
+
path.join(tmpDir, "src", "loaders", "search.ts"),
|
|
93
|
+
`import { fn } from "~/lib/vtex-transform.ts";\n`,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
writeImportedLibShims(makeCtx(tmpDir));
|
|
97
|
+
|
|
98
|
+
expect(
|
|
99
|
+
fs.existsSync(path.join(tmpDir, "src", "lib", "vtex-transform.ts")),
|
|
100
|
+
).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("ignores imports inside the lib dir itself (no self-amplification)", () => {
|
|
104
|
+
fs.mkdirSync(path.join(tmpDir, "src", "lib"));
|
|
105
|
+
fs.writeFileSync(
|
|
106
|
+
path.join(tmpDir, "src", "lib", "existing.ts"),
|
|
107
|
+
`import { x } from "~/lib/should-not-be-generated";\nexport const y = 1;\n`,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
writeImportedLibShims(makeCtx(tmpDir));
|
|
111
|
+
|
|
112
|
+
// Only the existing file should remain; nothing new generated.
|
|
113
|
+
const files = fs.readdirSync(path.join(tmpDir, "src", "lib"));
|
|
114
|
+
expect(files).toEqual(["existing.ts"]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("scans .tsx files too, not just .ts", () => {
|
|
118
|
+
fs.mkdirSync(path.join(tmpDir, "src", "components"), { recursive: true });
|
|
119
|
+
fs.writeFileSync(
|
|
120
|
+
path.join(tmpDir, "src", "components", "Filter.tsx"),
|
|
121
|
+
`import { toFilterSearchString } from "~/lib/filter-navigate";\n` +
|
|
122
|
+
`export const C = () => null;\n`,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
writeImportedLibShims(makeCtx(tmpDir));
|
|
126
|
+
|
|
127
|
+
expect(
|
|
128
|
+
fs.existsSync(path.join(tmpDir, "src", "lib", "filter-navigate.ts")),
|
|
129
|
+
).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("does nothing when src/ does not exist", () => {
|
|
133
|
+
const empty = fs.mkdtempSync(path.join(os.tmpdir(), "no-src-"));
|
|
134
|
+
try {
|
|
135
|
+
writeImportedLibShims(makeCtx(empty));
|
|
136
|
+
expect(fs.readdirSync(empty)).toEqual([]);
|
|
137
|
+
} finally {
|
|
138
|
+
fs.rmSync(empty, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -2,6 +2,10 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { MigrationContext } from "./types";
|
|
4
4
|
import { log, logPhase } from "./types";
|
|
5
|
+
import {
|
|
6
|
+
LIB_TEMPLATES,
|
|
7
|
+
selectImportedLibTemplates,
|
|
8
|
+
} from "./templates/lib-utils";
|
|
5
9
|
|
|
6
10
|
/** Directories to remove entirely after migration */
|
|
7
11
|
const DIRS_TO_DELETE = [
|
|
@@ -1479,7 +1483,106 @@ export function cleanup(ctx: MigrationContext): void {
|
|
|
1479
1483
|
console.log(" Fixing Worker-incompatible APIs...");
|
|
1480
1484
|
fixWorkerIncompatibleApis(ctx);
|
|
1481
1485
|
|
|
1486
|
+
// 14. LAZY: write only the src/lib/* shim files that the migrated
|
|
1487
|
+
// codebase actually imports. Run after every previous step so we
|
|
1488
|
+
// observe the final import graph (transforms + cleanup rewrites
|
|
1489
|
+
// + inline-stub hoisting all settled).
|
|
1490
|
+
console.log(" Writing src/lib/* shims (lazy)...");
|
|
1491
|
+
writeImportedLibShims(ctx);
|
|
1492
|
+
|
|
1482
1493
|
console.log(
|
|
1483
1494
|
` Deleted ${ctx.deletedFiles.length} files/dirs, moved ${ctx.movedFiles.length} files`,
|
|
1484
1495
|
);
|
|
1485
1496
|
}
|
|
1497
|
+
|
|
1498
|
+
/**
|
|
1499
|
+
* Walk `src/**\/*.{ts,tsx}` looking for `from "~/lib/<name>"` imports.
|
|
1500
|
+
* For each unique `<name>` that has a registered template in
|
|
1501
|
+
* `LIB_TEMPLATES`, write `src/lib/<name>.ts` with the template content.
|
|
1502
|
+
*
|
|
1503
|
+
* Sites that import none of the shims end up with no `src/lib/` directory
|
|
1504
|
+
* at all, instead of 11 dead files.
|
|
1505
|
+
*
|
|
1506
|
+
* Exported (rather than file-local) for unit testability — see
|
|
1507
|
+
* `phase-cleanup.test.ts`.
|
|
1508
|
+
*/
|
|
1509
|
+
export function writeImportedLibShims(ctx: MigrationContext): void {
|
|
1510
|
+
const srcRoot = path.join(ctx.sourceDir, "src");
|
|
1511
|
+
if (!fs.existsSync(srcRoot)) return;
|
|
1512
|
+
|
|
1513
|
+
const importedSpecifiers = new Set<string>();
|
|
1514
|
+
// Match `from "~/lib/<name>"` or `from '~/lib/<name>'`. We don't care
|
|
1515
|
+
// about default vs named imports here — only the path matters.
|
|
1516
|
+
const importRe = /from\s+["']~\/lib\/([^"']+?)(?:\.ts)?["']/g;
|
|
1517
|
+
|
|
1518
|
+
/** Recursively scan a directory for .ts/.tsx files and collect specifiers. */
|
|
1519
|
+
const walk = (dir: string) => {
|
|
1520
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1521
|
+
const full = path.join(dir, entry.name);
|
|
1522
|
+
if (entry.isDirectory()) {
|
|
1523
|
+
// Skip the lib dir itself so we don't count internal cross-refs
|
|
1524
|
+
// when we later add them.
|
|
1525
|
+
if (full === path.join(srcRoot, "lib")) continue;
|
|
1526
|
+
walk(full);
|
|
1527
|
+
continue;
|
|
1528
|
+
}
|
|
1529
|
+
if (!entry.isFile()) continue;
|
|
1530
|
+
if (!/\.(ts|tsx)$/.test(entry.name)) continue;
|
|
1531
|
+
const content = fs.readFileSync(full, "utf-8");
|
|
1532
|
+
let m: RegExpExecArray | null;
|
|
1533
|
+
while ((m = importRe.exec(content)) !== null) {
|
|
1534
|
+
importedSpecifiers.add(m[1]);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1538
|
+
walk(srcRoot);
|
|
1539
|
+
|
|
1540
|
+
if (importedSpecifiers.size === 0) {
|
|
1541
|
+
log(ctx, " No ~/lib/* imports detected — no shims written.");
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
const known = new Set(
|
|
1546
|
+
Object.keys(LIB_TEMPLATES).map((k) =>
|
|
1547
|
+
k.replace(/^src\/lib\//, "").replace(/\.ts$/, ""),
|
|
1548
|
+
),
|
|
1549
|
+
);
|
|
1550
|
+
const unknown: string[] = [];
|
|
1551
|
+
for (const spec of importedSpecifiers) {
|
|
1552
|
+
if (!known.has(spec)) unknown.push(spec);
|
|
1553
|
+
}
|
|
1554
|
+
if (unknown.length > 0) {
|
|
1555
|
+
log(
|
|
1556
|
+
ctx,
|
|
1557
|
+
` Warning: ~/lib/{${unknown.join(", ")}} imported but no template registered. ` +
|
|
1558
|
+
`Write the file by hand or add a template to scripts/migrate/templates/lib-utils.ts.`,
|
|
1559
|
+
);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
const toWrite = selectImportedLibTemplates(importedSpecifiers);
|
|
1563
|
+
if (Object.keys(toWrite).length === 0) return;
|
|
1564
|
+
|
|
1565
|
+
if (ctx.dryRun) {
|
|
1566
|
+
for (const relPath of Object.keys(toWrite)) {
|
|
1567
|
+
log(ctx, ` [DRY] Would write: ${relPath}`);
|
|
1568
|
+
}
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
const libDir = path.join(srcRoot, "lib");
|
|
1573
|
+
if (!fs.existsSync(libDir)) {
|
|
1574
|
+
fs.mkdirSync(libDir, { recursive: true });
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
for (const [relPath, content] of Object.entries(toWrite)) {
|
|
1578
|
+
const fullPath = path.join(ctx.sourceDir, relPath);
|
|
1579
|
+
fs.writeFileSync(fullPath, content, "utf-8");
|
|
1580
|
+
log(ctx, ` Wrote: ${relPath}`);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
log(
|
|
1584
|
+
ctx,
|
|
1585
|
+
` Generated ${Object.keys(toWrite).length} src/lib/* shim(s) ` +
|
|
1586
|
+
`(out of ${Object.keys(LIB_TEMPLATES).length} available templates).`,
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
@@ -18,7 +18,9 @@ import { generateCommerceLoaders } from "./templates/commerce-loaders";
|
|
|
18
18
|
import { generateSectionLoaders } from "./templates/section-loaders";
|
|
19
19
|
import { generateCacheConfig } from "./templates/cache-config";
|
|
20
20
|
import { generateSdkFiles } from "./templates/sdk-gen";
|
|
21
|
-
|
|
21
|
+
// `lib-utils` is imported lazily — see end of phase-cleanup. Eager
|
|
22
|
+
// generation of all 11 shims left every site with dead code that had
|
|
23
|
+
// to be cleaned up by hand.
|
|
22
24
|
import { extractTheme } from "./analyzers/theme-extractor";
|
|
23
25
|
|
|
24
26
|
function writeFile(ctx: MigrationContext, relPath: string, content: string) {
|
|
@@ -108,8 +110,10 @@ export function scaffold(ctx: MigrationContext): void {
|
|
|
108
110
|
writeFile(ctx, "src/sdk/logger.ts", generateLoggerStub());
|
|
109
111
|
writeMultiFile(ctx, generateSdkFiles(ctx));
|
|
110
112
|
|
|
111
|
-
// VTEX utility wrappers (signature-compatible stubs
|
|
112
|
-
|
|
113
|
+
// VTEX utility wrappers (signature-compatible stubs) are no longer
|
|
114
|
+
// generated eagerly here. They're written lazily at end of phase-cleanup,
|
|
115
|
+
// after all import rewrites have run, so that we only emit shims that
|
|
116
|
+
// some file actually imports. See `writeImportedLibShims` in phase-cleanup.
|
|
113
117
|
|
|
114
118
|
// Replace Context-based useDevice with SSR-safe useSyncExternalStore version.
|
|
115
119
|
// @decocms/start shell-renders sections in a separate React root without
|
|
@@ -30,7 +30,9 @@ const REQUIRED_FILES = [
|
|
|
30
30
|
"src/hooks/useCart.ts",
|
|
31
31
|
"src/hooks/useUser.ts",
|
|
32
32
|
"src/hooks/useWishlist.ts",
|
|
33
|
-
|
|
33
|
+
// src/types/widgets.ts intentionally omitted — provided by the
|
|
34
|
+
// framework at `@decocms/start/types/widgets`; sites no longer
|
|
35
|
+
// shadow the file locally.
|
|
34
36
|
"src/types/deco.ts",
|
|
35
37
|
"src/types/commerce-app.ts",
|
|
36
38
|
"src/components/ui/Image.tsx",
|
|
@@ -442,7 +444,8 @@ const checks: Check[] = [
|
|
|
442
444
|
severity: "warning",
|
|
443
445
|
fn: (ctx) => {
|
|
444
446
|
const typeFiles = [
|
|
445
|
-
|
|
447
|
+
// widgets.ts is provided by @decocms/start/types/widgets, not
|
|
448
|
+
// scaffolded locally.
|
|
446
449
|
"src/types/deco.ts",
|
|
447
450
|
"src/types/commerce-app.ts",
|
|
448
451
|
];
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
LIB_TEMPLATES,
|
|
4
|
+
selectImportedLibTemplates,
|
|
5
|
+
} from "./lib-utils";
|
|
6
|
+
|
|
7
|
+
describe("LIB_TEMPLATES registry", () => {
|
|
8
|
+
it("has entries", () => {
|
|
9
|
+
expect(Object.keys(LIB_TEMPLATES).length).toBeGreaterThan(0);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("uses src/lib/<name>.ts keys (relative paths the writer expects)", () => {
|
|
13
|
+
for (const key of Object.keys(LIB_TEMPLATES)) {
|
|
14
|
+
expect(key).toMatch(/^src\/lib\/[a-z][a-z0-9-]*\.ts$/);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("has non-empty contents for every entry", () => {
|
|
19
|
+
for (const [key, value] of Object.entries(LIB_TEMPLATES)) {
|
|
20
|
+
expect(value, `${key} should have content`).toBeTruthy();
|
|
21
|
+
expect(value.length, `${key} length`).toBeGreaterThan(20);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("has unique keys (no shadowing)", () => {
|
|
26
|
+
const keys = Object.keys(LIB_TEMPLATES);
|
|
27
|
+
const set = new Set(keys);
|
|
28
|
+
expect(set.size).toBe(keys.length);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("selectImportedLibTemplates()", () => {
|
|
33
|
+
it("returns empty record when no specifiers are imported", () => {
|
|
34
|
+
expect(selectImportedLibTemplates(new Set())).toEqual({});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns only the templates whose specifier is in the set", () => {
|
|
38
|
+
const result = selectImportedLibTemplates(new Set(["vtex-segment"]));
|
|
39
|
+
expect(Object.keys(result)).toEqual(["src/lib/vtex-segment.ts"]);
|
|
40
|
+
expect(result["src/lib/vtex-segment.ts"]).toBe(LIB_TEMPLATES["src/lib/vtex-segment.ts"]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("returns multiple templates when multiple specifiers are imported", () => {
|
|
44
|
+
const result = selectImportedLibTemplates(
|
|
45
|
+
new Set(["vtex-segment", "vtex-transform", "filter-navigate"]),
|
|
46
|
+
);
|
|
47
|
+
const keys = Object.keys(result).sort();
|
|
48
|
+
expect(keys).toEqual([
|
|
49
|
+
"src/lib/filter-navigate.ts",
|
|
50
|
+
"src/lib/vtex-segment.ts",
|
|
51
|
+
"src/lib/vtex-transform.ts",
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("ignores unknown specifiers without throwing", () => {
|
|
56
|
+
const result = selectImportedLibTemplates(
|
|
57
|
+
new Set(["vtex-segment", "this-template-does-not-exist"]),
|
|
58
|
+
);
|
|
59
|
+
expect(Object.keys(result)).toEqual(["src/lib/vtex-segment.ts"]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("does not mutate LIB_TEMPLATES (returns a fresh object)", () => {
|
|
63
|
+
const before = JSON.stringify(LIB_TEMPLATES);
|
|
64
|
+
const result = selectImportedLibTemplates(new Set(["vtex-segment"]));
|
|
65
|
+
result["src/lib/vtex-segment.ts"] = "// HIJACKED";
|
|
66
|
+
expect(JSON.stringify(LIB_TEMPLATES)).toBe(before);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("covers every well-known migration target the writer might emit", () => {
|
|
70
|
+
// Sanity check: the names that `transforms/imports.ts` rewrites to
|
|
71
|
+
// and that phase-cleanup hoists for inline-stub injection MUST all
|
|
72
|
+
// have templates registered, otherwise migrated sites get import
|
|
73
|
+
// errors with no warning.
|
|
74
|
+
const expectedSpecifiers = [
|
|
75
|
+
"vtex-transform",
|
|
76
|
+
"vtex-intelligent-search",
|
|
77
|
+
"vtex-segment",
|
|
78
|
+
"vtex-fetch",
|
|
79
|
+
"vtex-id",
|
|
80
|
+
"vtex-client",
|
|
81
|
+
"fetch-utils",
|
|
82
|
+
"http-utils",
|
|
83
|
+
"graphql-utils",
|
|
84
|
+
"filter-navigate",
|
|
85
|
+
];
|
|
86
|
+
for (const spec of expectedSpecifiers) {
|
|
87
|
+
const key = `src/lib/${spec}.ts`;
|
|
88
|
+
expect(LIB_TEMPLATES, `expected template for ${key}`).toHaveProperty(key);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -1,25 +1,41 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Templates for src/lib/ utility wrappers that bridge signature gaps
|
|
3
|
+
* between deco-cx/apps (old stack) and @decocms/apps-start (new stack).
|
|
4
|
+
*
|
|
5
|
+
* These are written *lazily*: only shims that are actually imported by
|
|
6
|
+
* the migrated codebase get generated. See `selectImportedLibTemplates`.
|
|
7
|
+
*
|
|
8
|
+
* Lazy generation matters because:
|
|
9
|
+
* - Most sites end up importing zero of these (apps-start exports
|
|
10
|
+
* direct equivalents for most VTEX utilities — see migrate#107).
|
|
11
|
+
* - Eager generation creates dead `src/lib/*.ts` files that every site
|
|
12
|
+
* then has to clean up by hand (see baggagio-tanstack#7, ~235 LOC).
|
|
13
|
+
*
|
|
14
|
+
* Registry shape: `"src/lib/<name>.ts"` → file contents.
|
|
15
|
+
*/
|
|
16
|
+
export const LIB_TEMPLATES: Record<string, string> = {
|
|
17
|
+
// Filled in below after the const declarations to keep the registry
|
|
18
|
+
// and template literals colocated. See trailing assignment.
|
|
19
|
+
};
|
|
2
20
|
|
|
3
21
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
22
|
+
* Given the set of `~/lib/X` imports actually present in the migrated
|
|
23
|
+
* codebase, return the subset of templates to write.
|
|
24
|
+
*
|
|
25
|
+
* `importedSpecifiers` are the `X` parts (without `~/lib/` prefix and
|
|
26
|
+
* without `.ts` extension), e.g. `"vtex-transform"`, `"http-utils"`.
|
|
9
27
|
*/
|
|
10
|
-
export function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"src/lib/filter-navigate.ts": LIB_FILTER_NAVIGATE,
|
|
22
|
-
};
|
|
28
|
+
export function selectImportedLibTemplates(
|
|
29
|
+
importedSpecifiers: Set<string>,
|
|
30
|
+
): Record<string, string> {
|
|
31
|
+
const out: Record<string, string> = {};
|
|
32
|
+
for (const spec of importedSpecifiers) {
|
|
33
|
+
const key = `src/lib/${spec}.ts`;
|
|
34
|
+
if (key in LIB_TEMPLATES) {
|
|
35
|
+
out[key] = LIB_TEMPLATES[key];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
23
39
|
}
|
|
24
40
|
|
|
25
41
|
const LIB_VTEX_TRANSFORM = `import type { Product } from "@decocms/apps/commerce/types";
|
|
@@ -253,3 +269,19 @@ export function toFilterSearchString(filterUrl: string): string {
|
|
|
253
269
|
return clean ? \`?\${clean}\` : "";
|
|
254
270
|
}
|
|
255
271
|
`;
|
|
272
|
+
|
|
273
|
+
// Populate the registry now that all template literals are declared.
|
|
274
|
+
// Keys must match the relative path the migration script writes; values
|
|
275
|
+
// are the file contents.
|
|
276
|
+
Object.assign(LIB_TEMPLATES, {
|
|
277
|
+
"src/lib/vtex-transform.ts": LIB_VTEX_TRANSFORM,
|
|
278
|
+
"src/lib/vtex-intelligent-search.ts": LIB_VTEX_INTELLIGENT_SEARCH,
|
|
279
|
+
"src/lib/vtex-segment.ts": LIB_VTEX_SEGMENT,
|
|
280
|
+
"src/lib/http-utils.ts": LIB_HTTP_UTILS,
|
|
281
|
+
"src/lib/vtex-client.ts": LIB_VTEX_CLIENT,
|
|
282
|
+
"src/lib/fetch-utils.ts": LIB_FETCH_UTILS,
|
|
283
|
+
"src/lib/vtex-fetch.ts": LIB_VTEX_FETCH,
|
|
284
|
+
"src/lib/vtex-id.ts": LIB_VTEX_ID,
|
|
285
|
+
"src/lib/graphql-utils.ts": LIB_GRAPHQL_UTILS,
|
|
286
|
+
"src/lib/filter-navigate.ts": LIB_FILTER_NAVIGATE,
|
|
287
|
+
});
|
|
@@ -3,15 +3,12 @@ import type { MigrationContext } from "../types";
|
|
|
3
3
|
export function generateTypeFiles(ctx: MigrationContext): Record<string, string> {
|
|
4
4
|
const files: Record<string, string> = {};
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export type Color = string;
|
|
13
|
-
export type ButtonWidget = string;
|
|
14
|
-
`;
|
|
6
|
+
// src/types/widgets.ts is no longer generated — the framework owns these
|
|
7
|
+
// string aliases (`ImageWidget`, `HTMLWidget`, …) at
|
|
8
|
+
// `@decocms/start/types/widgets`, and `transforms/imports.ts` rewrites
|
|
9
|
+
// `apps/admin/widgets.ts` directly to that path. Schema generation
|
|
10
|
+
// works the same way: the generator matches by type *text*, not module
|
|
11
|
+
// identity (see scripts/generate-schema.ts:WIDGET_TYPE_FORMATS).
|
|
15
12
|
|
|
16
13
|
files["src/types/deco.ts"] = `export type SectionProps<T extends (...args: any[]) => any> = Awaited<
|
|
17
14
|
ReturnType<T>
|
|
@@ -29,7 +29,12 @@ const IMPORT_RULES: Array<[RegExp, string | null]> = [
|
|
|
29
29
|
[/^"@deco\/deco"$/, `"~/types/deco"`],
|
|
30
30
|
|
|
31
31
|
// Apps — widgets & components
|
|
32
|
-
|
|
32
|
+
// Widget aliases (ImageWidget, HTMLWidget, ...) are framework-owned —
|
|
33
|
+
// every site has the same type set, and the schema generator detects
|
|
34
|
+
// them via type-text matching, not module identity. Re-export from
|
|
35
|
+
// @decocms/start/types/widgets so we don't keep a duplicated 8-line
|
|
36
|
+
// file in every site.
|
|
37
|
+
[/^"apps\/admin\/widgets\.ts"$/, `"@decocms/start/types/widgets"`],
|
|
33
38
|
[/^"apps\/website\/components\/Image\.tsx"$/, `"~/components/ui/Image"`],
|
|
34
39
|
[/^"apps\/website\/components\/Picture\.tsx"$/, `"~/components/ui/Picture"`],
|
|
35
40
|
[/^"apps\/website\/components\/Video\.tsx"$/, `"~/components/ui/Video"`],
|
package/vitest.config.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { defineConfig } from "vitest/config";
|
|
2
2
|
|
|
3
|
+
// Two test suites, one runner:
|
|
4
|
+
// - `src/**` runs in jsdom (React rendering, hooks, browser globals).
|
|
5
|
+
// - `scripts/**` runs in node (filesystem, migration script logic).
|
|
6
|
+
// vitest applies env-per-file via the `environmentMatchGlobs` map.
|
|
3
7
|
export default defineConfig({
|
|
4
8
|
test: {
|
|
5
9
|
environment: "jsdom",
|
|
6
|
-
|
|
10
|
+
environmentMatchGlobs: [
|
|
11
|
+
["scripts/**", "node"],
|
|
12
|
+
],
|
|
13
|
+
include: [
|
|
14
|
+
"src/**/*.test.{ts,tsx}",
|
|
15
|
+
"scripts/**/*.test.ts",
|
|
16
|
+
],
|
|
7
17
|
globals: true,
|
|
8
18
|
},
|
|
9
19
|
});
|