@decocms/blocks-cli 7.0.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 +44 -0
- package/scripts/analyze-traces.mjs +1117 -0
- package/scripts/audit-observability-config.test.ts +446 -0
- package/scripts/audit-observability-config.ts +511 -0
- package/scripts/deco-migrate-cli.ts +444 -0
- package/scripts/fast-deploy-kv.test.ts +131 -0
- package/scripts/generate-blocks.test.ts +94 -0
- package/scripts/generate-blocks.ts +274 -0
- package/scripts/generate-invoke.test.ts +195 -0
- package/scripts/generate-invoke.ts +469 -0
- package/scripts/generate-loaders.ts +217 -0
- package/scripts/generate-schema.ts +1287 -0
- package/scripts/generate-sections.ts +237 -0
- package/scripts/htmx-analyze.ts +226 -0
- package/scripts/lib/blocks-dedupe.test.ts +179 -0
- package/scripts/lib/blocks-dedupe.ts +142 -0
- package/scripts/lib/cf-kv-rest.ts +78 -0
- package/scripts/lib/jsonc.ts +122 -0
- package/scripts/lib/kv-snapshot.ts +51 -0
- package/scripts/lib/read-decofile.ts +70 -0
- package/scripts/lib/sync-helpers.ts +44 -0
- package/scripts/migrate/analyzers/htmx-analyze.test.ts +372 -0
- package/scripts/migrate/analyzers/htmx-analyze.ts +425 -0
- package/scripts/migrate/analyzers/island-classifier.ts +96 -0
- package/scripts/migrate/analyzers/loader-inventory.ts +63 -0
- package/scripts/migrate/analyzers/section-metadata.ts +147 -0
- package/scripts/migrate/analyzers/theme-extractor.ts +122 -0
- package/scripts/migrate/colors.ts +46 -0
- package/scripts/migrate/config.test.ts +202 -0
- package/scripts/migrate/config.ts +186 -0
- package/scripts/migrate/phase-analyze.test.ts +63 -0
- package/scripts/migrate/phase-analyze.ts +782 -0
- package/scripts/migrate/phase-cleanup-audit.test.ts +137 -0
- package/scripts/migrate/phase-cleanup-audit.ts +105 -0
- package/scripts/migrate/phase-cleanup.test.ts +141 -0
- package/scripts/migrate/phase-cleanup.ts +1588 -0
- package/scripts/migrate/phase-compile.test.ts +193 -0
- package/scripts/migrate/phase-compile.ts +177 -0
- package/scripts/migrate/phase-report.ts +243 -0
- package/scripts/migrate/phase-scaffold.ts +593 -0
- package/scripts/migrate/phase-transform.ts +310 -0
- package/scripts/migrate/phase-verify.test.ts +127 -0
- package/scripts/migrate/phase-verify.ts +572 -0
- package/scripts/migrate/post-cleanup/rules.ts +1708 -0
- package/scripts/migrate/post-cleanup/runner.test.ts +1771 -0
- package/scripts/migrate/post-cleanup/runner.ts +137 -0
- package/scripts/migrate/post-cleanup/shim-classify.test.ts +352 -0
- package/scripts/migrate/post-cleanup/shim-classify.ts +246 -0
- package/scripts/migrate/post-cleanup/types.ts +106 -0
- package/scripts/migrate/source-layout.test.ts +111 -0
- package/scripts/migrate/source-layout.ts +103 -0
- package/scripts/migrate/templates/app-css.ts +366 -0
- package/scripts/migrate/templates/cache-config.ts +26 -0
- package/scripts/migrate/templates/commerce-loaders.ts +230 -0
- package/scripts/migrate/templates/cursor-rules.test.ts +59 -0
- package/scripts/migrate/templates/cursor-rules.ts +70 -0
- package/scripts/migrate/templates/hooks.test.ts +141 -0
- package/scripts/migrate/templates/hooks.ts +152 -0
- package/scripts/migrate/templates/knip-config.ts +27 -0
- package/scripts/migrate/templates/lib-utils.test.ts +139 -0
- package/scripts/migrate/templates/lib-utils.ts +326 -0
- package/scripts/migrate/templates/lockfile-check-yml.test.ts +26 -0
- package/scripts/migrate/templates/lockfile-check-yml.ts +66 -0
- package/scripts/migrate/templates/package-json.ts +177 -0
- package/scripts/migrate/templates/routes.ts +237 -0
- package/scripts/migrate/templates/sdk-gen.ts +59 -0
- package/scripts/migrate/templates/section-loaders.ts +456 -0
- package/scripts/migrate/templates/server-entry.ts +561 -0
- package/scripts/migrate/templates/setup.ts +148 -0
- package/scripts/migrate/templates/tsconfig.ts +21 -0
- package/scripts/migrate/templates/types-gen.ts +174 -0
- package/scripts/migrate/templates/ui-components.ts +144 -0
- package/scripts/migrate/templates/vite-config.ts +101 -0
- package/scripts/migrate/transforms/dead-code.ts +455 -0
- package/scripts/migrate/transforms/deno-isms.ts +85 -0
- package/scripts/migrate/transforms/fresh-apis.ts +223 -0
- package/scripts/migrate/transforms/htmx-on-events.test.ts +305 -0
- package/scripts/migrate/transforms/htmx-on-events.ts +193 -0
- package/scripts/migrate/transforms/imports.ts +385 -0
- package/scripts/migrate/transforms/jsx.ts +317 -0
- package/scripts/migrate/transforms/section-conventions.ts +210 -0
- package/scripts/migrate/transforms/tailwind.ts +739 -0
- package/scripts/migrate/types.ts +244 -0
- package/scripts/migrate-blocks-to-kv.ts +104 -0
- package/scripts/migrate-post-cleanup.ts +191 -0
- package/scripts/migrate-to-cf-observability.test.ts +215 -0
- package/scripts/migrate-to-cf-observability.ts +699 -0
- package/scripts/migrate.ts +282 -0
- package/scripts/smoke-otlp-errorlog.ts +46 -0
- package/scripts/smoke-otlp-meter.ts +48 -0
- package/scripts/sync-blocks-to-kv.ts +153 -0
- package/scripts/tailwind-lint.ts +518 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { resolveSectionConventions } from "./config";
|
|
4
|
+
import type { MigrationContext, TransformResult, SectionMeta } from "./types";
|
|
5
|
+
import { log, logPhase } from "./types";
|
|
6
|
+
import { transformImports } from "./transforms/imports";
|
|
7
|
+
import { transformJsx } from "./transforms/jsx";
|
|
8
|
+
import { transformFreshApis } from "./transforms/fresh-apis";
|
|
9
|
+
import { transformDenoIsms } from "./transforms/deno-isms";
|
|
10
|
+
import { transformTailwind } from "./transforms/tailwind";
|
|
11
|
+
import { transformDeadCode } from "./transforms/dead-code";
|
|
12
|
+
import { transformHtmxOnEvents } from "./transforms/htmx-on-events";
|
|
13
|
+
import { createSectionConventionsTransform } from "./transforms/section-conventions";
|
|
14
|
+
|
|
15
|
+
/** Map of section path → metadata, populated per-run */
|
|
16
|
+
let sectionMetaMap: Map<string, SectionMeta> | null = null;
|
|
17
|
+
|
|
18
|
+
function getSectionMeta(ctx: MigrationContext, relPath: string): SectionMeta | undefined {
|
|
19
|
+
if (!sectionMetaMap) {
|
|
20
|
+
sectionMetaMap = new Map();
|
|
21
|
+
for (const m of ctx.sectionMetas) {
|
|
22
|
+
sectionMetaMap.set(m.path, m);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return sectionMetaMap.get(relPath);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Cached per-run section-conventions closure. Built once from the
|
|
30
|
+
* resolved config sets (`ctx.config.sectionConventions`), so casaevideo
|
|
31
|
+
* defaults still apply when no config file exists.
|
|
32
|
+
*/
|
|
33
|
+
let cachedSectionTransform:
|
|
34
|
+
| ReturnType<typeof createSectionConventionsTransform>
|
|
35
|
+
| null = null;
|
|
36
|
+
|
|
37
|
+
function getSectionConventionsTransform(ctx: MigrationContext) {
|
|
38
|
+
if (cachedSectionTransform) return cachedSectionTransform;
|
|
39
|
+
const sets = resolveSectionConventions(ctx.config ?? null);
|
|
40
|
+
cachedSectionTransform = createSectionConventionsTransform(sets);
|
|
41
|
+
return cachedSectionTransform;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Apply all transforms to a file's content in the correct order.
|
|
46
|
+
*/
|
|
47
|
+
function applyTransforms(content: string, filePath: string, ctx?: MigrationContext, relPath?: string): TransformResult {
|
|
48
|
+
const allNotes: string[] = [];
|
|
49
|
+
let currentContent = content;
|
|
50
|
+
let anyChanged = false;
|
|
51
|
+
|
|
52
|
+
// Only transform code files
|
|
53
|
+
const ext = path.extname(filePath);
|
|
54
|
+
if (![".ts", ".tsx"].includes(ext)) {
|
|
55
|
+
return { content, changed: false, notes: [] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Pipeline: imports → jsx → htmx-on-events → fresh-apis → dead-code → deno-isms → tailwind
|
|
59
|
+
// htmx-on-events runs after jsx (which renames class/onChange) and
|
|
60
|
+
// before fresh-apis (which removes useScript imports the htmx
|
|
61
|
+
// codemod's TODO might still reference). The codemod is a no-op on
|
|
62
|
+
// files without hx-on, so it never adds latency to non-htmx sites.
|
|
63
|
+
const pipeline: Array<{ name: string; fn: (content: string) => TransformResult }> = [
|
|
64
|
+
{ name: "imports", fn: (c) => transformImports(c, ctx?.islandWrapperTargets) },
|
|
65
|
+
{ name: "jsx", fn: transformJsx },
|
|
66
|
+
{ name: "htmx-on-events", fn: transformHtmxOnEvents },
|
|
67
|
+
{ name: "fresh-apis", fn: transformFreshApis },
|
|
68
|
+
{ name: "dead-code", fn: (c) => transformDeadCode(c, ctx?.platform) },
|
|
69
|
+
{ name: "deno-isms", fn: transformDenoIsms },
|
|
70
|
+
{ name: "tailwind", fn: transformTailwind },
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
for (const step of pipeline) {
|
|
74
|
+
const result = step.fn(currentContent);
|
|
75
|
+
if (result.changed) {
|
|
76
|
+
anyChanged = true;
|
|
77
|
+
currentContent = result.content;
|
|
78
|
+
allNotes.push(...result.notes.map((n) => `[${step.name}] ${n}`));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Section conventions (sync/eager/layout/cache) — only for section files
|
|
83
|
+
if (ctx && relPath && relPath.startsWith("sections/")) {
|
|
84
|
+
const meta = getSectionMeta(ctx, relPath);
|
|
85
|
+
// Build the closure once per ctx, cache it on the context.
|
|
86
|
+
const sectionTransform = getSectionConventionsTransform(ctx);
|
|
87
|
+
const result = sectionTransform(currentContent, meta);
|
|
88
|
+
if (result.changed) {
|
|
89
|
+
anyChanged = true;
|
|
90
|
+
currentContent = result.content;
|
|
91
|
+
allNotes.push(...result.notes.map((n) => `[section-conventions] ${n}`));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { content: currentContent, changed: anyChanged, notes: allNotes };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function transform(ctx: MigrationContext): void {
|
|
99
|
+
logPhase("Transform");
|
|
100
|
+
|
|
101
|
+
const toTransform = ctx.files.filter((f) => f.action === "transform");
|
|
102
|
+
console.log(` Files to transform: ${toTransform.length}`);
|
|
103
|
+
|
|
104
|
+
for (const record of toTransform) {
|
|
105
|
+
const { absPath, targetPath } = record;
|
|
106
|
+
if (!targetPath) continue;
|
|
107
|
+
|
|
108
|
+
// Read source
|
|
109
|
+
const content = fs.readFileSync(absPath, "utf-8");
|
|
110
|
+
|
|
111
|
+
// Apply transforms
|
|
112
|
+
const result = applyTransforms(content, absPath, ctx, record.path);
|
|
113
|
+
|
|
114
|
+
// Fix section re-exports from wrapper islands — point to the wrapped component
|
|
115
|
+
const resolvedTarget = (record as any).__resolvedReExportTarget;
|
|
116
|
+
if (resolvedTarget && result.content.includes("~/components/")) {
|
|
117
|
+
// The import transform rewrote $store/islands/X → ~/components/X
|
|
118
|
+
// but for wrapper islands, the actual component is at a different path
|
|
119
|
+
const reExportRe = /from\s+"~\/components\/[^"]+"/g;
|
|
120
|
+
result.content = result.content.replace(reExportRe, `from "${resolvedTarget}"`);
|
|
121
|
+
result.notes.push(`Re-export resolved to wrapper target: ${resolvedTarget}`);
|
|
122
|
+
result.changed = true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Add manual review items
|
|
126
|
+
for (const note of result.notes) {
|
|
127
|
+
if (note.startsWith("[") && note.includes("MANUAL:")) {
|
|
128
|
+
ctx.manualReviewItems.push({
|
|
129
|
+
file: targetPath,
|
|
130
|
+
reason: note,
|
|
131
|
+
severity: "warning",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Flag files with HTMX patterns for manual React migration
|
|
137
|
+
if (/\bhx-(?:get|post|put|delete|trigger|target|swap|on|indicator|sync|select)\b/.test(result.content)) {
|
|
138
|
+
ctx.manualReviewItems.push({
|
|
139
|
+
file: targetPath,
|
|
140
|
+
reason: "HTMX attributes (hx-*) found — needs manual migration to React state/effects. HTMX server-side rendering (hx-get/hx-post with useSection) must be converted to React components with useState/useEffect or server functions.",
|
|
141
|
+
severity: "warning",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Flag files with hx-on:click that use useScript (simpler pattern)
|
|
146
|
+
if (/hx-on:click=\{useScript/.test(result.content)) {
|
|
147
|
+
ctx.manualReviewItems.push({
|
|
148
|
+
file: targetPath,
|
|
149
|
+
reason: "hx-on:click with useScript found — convert to onClick with React event handler. The useScript serialization won't work as onClick value.",
|
|
150
|
+
severity: "warning",
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Flag the legacy sections/Component.tsx dynamic-section loader.
|
|
155
|
+
// This file uses Deno-specific APIs (toFileUrl, import.meta.resolve)
|
|
156
|
+
// and the HTMX-driven `useComponent(component, props)` pattern, which
|
|
157
|
+
// do not run on Cloudflare Workers and have no equivalent in
|
|
158
|
+
// @decocms/start. The whole file must be deleted.
|
|
159
|
+
if (
|
|
160
|
+
/sections\/Component\.tsx?$/.test(record.path) ||
|
|
161
|
+
/sections\/Component\.tsx?$/.test(targetPath)
|
|
162
|
+
) {
|
|
163
|
+
ctx.manualReviewItems.push({
|
|
164
|
+
file: targetPath,
|
|
165
|
+
reason:
|
|
166
|
+
"sections/Component.tsx (Deno HTMX dynamic-section loader) is incompatible with TanStack Start / Cloudflare Workers. " +
|
|
167
|
+
"DELETE this file and migrate every `useComponent(...)` call site to one of: " +
|
|
168
|
+
"(a) local React state for client-side toggles, " +
|
|
169
|
+
"(b) `createServerFn` + `useMutation` for server actions, or " +
|
|
170
|
+
"(c) a direct `invoke` call (`~/server/invoke`) for ad-hoc loaders. " +
|
|
171
|
+
"See: deco-to-tanstack-migration skill, 'useComponent / partial sections' section.",
|
|
172
|
+
severity: "error",
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Flag any import of useComponent — typically `import { useComponent } from "site/sections/Component.tsx"`.
|
|
177
|
+
// We also catch `from "../../sections/Component"` and similar relative variants.
|
|
178
|
+
if (
|
|
179
|
+
/\buseComponent\b/.test(result.content) &&
|
|
180
|
+
/from\s+["'][^"']*sections\/Component(?:\.tsx?)?["']/.test(result.content)
|
|
181
|
+
) {
|
|
182
|
+
ctx.manualReviewItems.push({
|
|
183
|
+
file: targetPath,
|
|
184
|
+
reason:
|
|
185
|
+
"useComponent({ ... }) call site detected. This is the HTMX-style dynamic-section render pattern " +
|
|
186
|
+
"that ships HTML fragments and swaps them client-side. It does not work on TanStack Start. " +
|
|
187
|
+
"Recipes: " +
|
|
188
|
+
"(1) Self-contained UI toggles → keep state in React (`useState` + event handlers); " +
|
|
189
|
+
"(2) Form submissions / mutations → `createServerFn` + `useMutation` (see casaevideo-storefront for canonical examples); " +
|
|
190
|
+
"(3) Ad-hoc data fetches → call the loader/action via `~/server/invoke` and store results in `useState`. " +
|
|
191
|
+
"Remove the import after refactoring, then delete `src/sections/Component.tsx`.",
|
|
192
|
+
severity: "error",
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (ctx.dryRun) {
|
|
197
|
+
if (result.changed) {
|
|
198
|
+
log(ctx, `[DRY] Would transform: ${record.path} → ${targetPath}`);
|
|
199
|
+
for (const note of result.notes) {
|
|
200
|
+
log(ctx, ` ${note}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
ctx.transformedFiles.push(targetPath);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Write to target path
|
|
208
|
+
const fullTargetPath = path.join(ctx.sourceDir, targetPath);
|
|
209
|
+
const dir = path.dirname(fullTargetPath);
|
|
210
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
211
|
+
fs.writeFileSync(fullTargetPath, result.content, "utf-8");
|
|
212
|
+
|
|
213
|
+
ctx.transformedFiles.push(targetPath);
|
|
214
|
+
if (result.changed) {
|
|
215
|
+
log(
|
|
216
|
+
ctx,
|
|
217
|
+
`Transformed: ${record.path} → ${targetPath} (${result.notes.length} changes)`,
|
|
218
|
+
);
|
|
219
|
+
} else {
|
|
220
|
+
log(ctx, `Copied: ${record.path} → ${targetPath}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log(` Transformed ${ctx.transformedFiles.length} files`);
|
|
225
|
+
|
|
226
|
+
// Post-transform: resolve ~/islands/ imports to actual file locations.
|
|
227
|
+
// Islands are moved to src/sections/ during migration, but components
|
|
228
|
+
// import them via ~/islands/X which no longer exists. Scan src/ for
|
|
229
|
+
// the actual file and rewrite the import.
|
|
230
|
+
if (!ctx.dryRun) {
|
|
231
|
+
fixIslandImports(ctx);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Scan all transformed files for ~/islands/ imports and rewrite them
|
|
237
|
+
* to the actual path where the file was placed (sections/, components/, etc.).
|
|
238
|
+
*/
|
|
239
|
+
function fixIslandImports(ctx: MigrationContext): void {
|
|
240
|
+
const srcDir = path.join(ctx.sourceDir, "src");
|
|
241
|
+
if (!fs.existsSync(srcDir)) return;
|
|
242
|
+
|
|
243
|
+
// Build a lookup: filename → relative path from src/
|
|
244
|
+
const fileLookup = new Map<string, string[]>();
|
|
245
|
+
function scanDir(dir: string) {
|
|
246
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
247
|
+
if (entry.isDirectory()) {
|
|
248
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
249
|
+
scanDir(path.join(dir, entry.name));
|
|
250
|
+
} else if (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts")) {
|
|
251
|
+
const relPath = path.relative(srcDir, path.join(dir, entry.name)).replace(/\\/g, "/");
|
|
252
|
+
const base = entry.name.replace(/\.tsx?$/, "");
|
|
253
|
+
if (!fileLookup.has(base)) fileLookup.set(base, []);
|
|
254
|
+
fileLookup.get(base)!.push(relPath);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
scanDir(srcDir);
|
|
259
|
+
|
|
260
|
+
// Scan all .ts/.tsx files in src/ for ~/islands/ imports
|
|
261
|
+
const islandImportRe = /from\s+["'](~\/islands\/([^"']+))["']/g;
|
|
262
|
+
let fixCount = 0;
|
|
263
|
+
|
|
264
|
+
function walkAndFix(dir: string) {
|
|
265
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
266
|
+
if (entry.isDirectory()) {
|
|
267
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
268
|
+
walkAndFix(path.join(dir, entry.name));
|
|
269
|
+
} else if (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts")) {
|
|
270
|
+
const filePath = path.join(dir, entry.name);
|
|
271
|
+
let content = fs.readFileSync(filePath, "utf-8");
|
|
272
|
+
let modified = false;
|
|
273
|
+
|
|
274
|
+
content = content.replace(islandImportRe, (match, fullImport, islandPath) => {
|
|
275
|
+
// islandPath = "Cart/Indicator" or "SliderJS" or "Searchbar"
|
|
276
|
+
const basename = islandPath.replace(/\.tsx?$/, "").split("/").pop()!;
|
|
277
|
+
|
|
278
|
+
// Try to find the file — prefer components/ over sections/
|
|
279
|
+
const candidates = fileLookup.get(basename) || [];
|
|
280
|
+
// Exclude islands/ paths themselves and routes/
|
|
281
|
+
const valid = candidates.filter(
|
|
282
|
+
(c) => !c.startsWith("islands/") && !c.startsWith("routes/"),
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
if (valid.length === 0) return match; // can't resolve, leave as-is
|
|
286
|
+
|
|
287
|
+
// Prefer components/ over sections/
|
|
288
|
+
const preferred =
|
|
289
|
+
valid.find((c) => c.startsWith("components/")) ??
|
|
290
|
+
valid.find((c) => c.startsWith("sections/")) ??
|
|
291
|
+
valid[0];
|
|
292
|
+
|
|
293
|
+
const newPath = "~/" + preferred.replace(/\.tsx?$/, "");
|
|
294
|
+
modified = true;
|
|
295
|
+
return match.replace(fullImport, newPath);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
if (modified) {
|
|
299
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
300
|
+
fixCount++;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
walkAndFix(srcDir);
|
|
307
|
+
if (fixCount > 0) {
|
|
308
|
+
console.log(` Fixed ~/islands/ imports in ${fixCount} files`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
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, vi } from "vitest";
|
|
5
|
+
import { checks } from "./phase-verify";
|
|
6
|
+
import type { MigrationContext } from "./types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The "No relative imports to deleted SDK files" check used to flag any
|
|
10
|
+
* relative import to `sdk/{clx,useId,useOffer,useVariantPossiblities,
|
|
11
|
+
* usePlatform}`. But `useOffer` and `useVariantPossiblities` are KEPT as
|
|
12
|
+
* site files (see RELATIVE_SDK_REWRITES in transforms/imports.ts) — so
|
|
13
|
+
* every site that legitimately imports them via a relative path was
|
|
14
|
+
* failing verify with a misleading error. See #212.
|
|
15
|
+
*
|
|
16
|
+
* Pull the specific check out of the registered array and exercise it on
|
|
17
|
+
* a fixture tree, so we don't have to satisfy the other 24 checks that
|
|
18
|
+
* `verify()` runs at once.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
function makeCtx(sourceDir: string): MigrationContext {
|
|
22
|
+
return {
|
|
23
|
+
sourceDir,
|
|
24
|
+
siteName: "test-site",
|
|
25
|
+
platform: "custom",
|
|
26
|
+
vtexAccount: null,
|
|
27
|
+
gtmId: null,
|
|
28
|
+
importMap: {},
|
|
29
|
+
discoveredNpmDeps: {},
|
|
30
|
+
themeColors: {},
|
|
31
|
+
fontFamily: null,
|
|
32
|
+
files: [],
|
|
33
|
+
sectionMetas: [],
|
|
34
|
+
islandClassifications: [],
|
|
35
|
+
islandWrapperTargets: new Map(),
|
|
36
|
+
loaderInventory: [],
|
|
37
|
+
scaffoldedFiles: [],
|
|
38
|
+
transformedFiles: [],
|
|
39
|
+
deletedFiles: [],
|
|
40
|
+
movedFiles: [],
|
|
41
|
+
manualReviewItems: [],
|
|
42
|
+
frameworkFindings: [],
|
|
43
|
+
dryRun: false,
|
|
44
|
+
verbose: false,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const sdkCheck = checks.find(
|
|
49
|
+
(c) => c.name === "No relative imports to deleted SDK files",
|
|
50
|
+
);
|
|
51
|
+
if (!sdkCheck) {
|
|
52
|
+
throw new Error("verify check not found — name changed?");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function runCheck(ctx: MigrationContext): { ok: boolean; output: string } {
|
|
56
|
+
const lines: string[] = [];
|
|
57
|
+
const spy = vi.spyOn(console, "log").mockImplementation((...args: unknown[]) => {
|
|
58
|
+
lines.push(args.map((a) => String(a)).join(" "));
|
|
59
|
+
});
|
|
60
|
+
try {
|
|
61
|
+
const ok = sdkCheck!.fn(ctx);
|
|
62
|
+
return { ok, output: lines.join("\n") };
|
|
63
|
+
} finally {
|
|
64
|
+
spy.mockRestore();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe("verify check: 'No relative imports to deleted SDK files'", () => {
|
|
69
|
+
let tmp: string;
|
|
70
|
+
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), "verify-sdk-"));
|
|
73
|
+
fs.mkdirSync(path.join(tmp, "src", "components"), { recursive: true });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
afterEach(() => {
|
|
77
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("passes when src/ imports useOffer/useVariantPossiblities relatively (kept files, #212)", () => {
|
|
81
|
+
fs.writeFileSync(
|
|
82
|
+
path.join(tmp, "src", "components", "ProductCard.tsx"),
|
|
83
|
+
`import { useOffer } from "../../sdk/useOffer";\n` +
|
|
84
|
+
`import { useVariantPossibilities } from "../../sdk/useVariantPossiblities";\n` +
|
|
85
|
+
`export const x = 1;\n`,
|
|
86
|
+
);
|
|
87
|
+
const { ok, output } = runCheck(makeCtx(tmp));
|
|
88
|
+
expect(ok).toBe(true);
|
|
89
|
+
expect(output).toBe("");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("fails when src/ imports clx/useId/usePlatform relatively (truly deleted)", () => {
|
|
93
|
+
fs.writeFileSync(
|
|
94
|
+
path.join(tmp, "src", "components", "Bad.tsx"),
|
|
95
|
+
`import { clx } from "../../sdk/clx";\n` +
|
|
96
|
+
`import { useId } from "../../sdk/useId";\n` +
|
|
97
|
+
`import { usePlatform } from "../../sdk/usePlatform";\n` +
|
|
98
|
+
`export const x = 1;\n`,
|
|
99
|
+
);
|
|
100
|
+
const { ok, output } = runCheck(makeCtx(tmp));
|
|
101
|
+
expect(ok).toBe(false);
|
|
102
|
+
// The improved error reports each offending line, not just the file.
|
|
103
|
+
expect(output).toMatch(/components\/Bad\.tsx:.*sdk\/clx/);
|
|
104
|
+
expect(output).toMatch(/components\/Bad\.tsx:.*sdk\/useId/);
|
|
105
|
+
expect(output).toMatch(/components\/Bad\.tsx:.*sdk\/usePlatform/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("ignores commented-out references to deleted SDK files", () => {
|
|
109
|
+
fs.writeFileSync(
|
|
110
|
+
path.join(tmp, "src", "components", "Docs.tsx"),
|
|
111
|
+
`// import { clx } from "../../sdk/clx"\n` +
|
|
112
|
+
`// from "../../sdk/useId"\n` +
|
|
113
|
+
`export const x = 1;\n`,
|
|
114
|
+
);
|
|
115
|
+
const { ok } = runCheck(makeCtx(tmp));
|
|
116
|
+
expect(ok).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("matches both .ts-suffixed and unsuffixed import paths", () => {
|
|
120
|
+
fs.writeFileSync(
|
|
121
|
+
path.join(tmp, "src", "components", "WithExt.tsx"),
|
|
122
|
+
`import { clx } from "../../sdk/clx.ts";\nexport const x = 1;\n`,
|
|
123
|
+
);
|
|
124
|
+
const { ok } = runCheck(makeCtx(tmp));
|
|
125
|
+
expect(ok).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
});
|