@decocms/start 1.6.3 → 2.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/.releaserc.json +1 -0
- package/package.json +1 -1
- package/scripts/generate-loaders.ts +79 -12
- package/scripts/migrate/phase-analyze.ts +54 -0
- package/scripts/migrate/phase-cleanup.ts +105 -1
- package/scripts/migrate/phase-transform.ts +42 -0
- package/scripts/migrate/templates/commerce-loaders.ts +1 -1
- package/scripts/migrate/templates/section-loaders.ts +4 -1
- package/scripts/migrate/types.ts +3 -1
- package/src/cms/resolve.ts +12 -1
- package/src/sdk/useScript.ts +27 -6
- package/src/sdk/workerEntry.ts +11 -2
- package/src/setup.ts +1 -1
package/.releaserc.json
CHANGED
package/package.json
CHANGED
|
@@ -5,23 +5,31 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Each loader/action file that exports a default function gets a generated
|
|
7
7
|
* entry like:
|
|
8
|
-
* "site/loaders/SAP/getUser": async (props) => {
|
|
8
|
+
* "site/loaders/SAP/getUser": async (props, request) => {
|
|
9
9
|
* const mod = await import("../../loaders/SAP/getUser");
|
|
10
|
-
* return mod.default(props);
|
|
10
|
+
* return mod.default(props, request);
|
|
11
11
|
* },
|
|
12
12
|
*
|
|
13
13
|
* Both keyed with and without `.ts` suffix for CMS block compatibility.
|
|
14
14
|
*
|
|
15
15
|
* Files listed in --exclude are skipped (they need custom wiring in setup.ts).
|
|
16
16
|
*
|
|
17
|
+
* CMS-aware filtering (`--decofile-dir`): when supplied, the script walks
|
|
18
|
+
* every JSON file in the directory and collects the set of `__resolveType`
|
|
19
|
+
* references. Only loaders whose key appears in that set are emitted —
|
|
20
|
+
* keeping the registry to what the site actually uses and avoiding the
|
|
21
|
+
* "200 dead passthroughs" pattern.
|
|
22
|
+
*
|
|
17
23
|
* Usage (from site root):
|
|
18
24
|
* npx tsx node_modules/@decocms/start/scripts/generate-loaders.ts
|
|
25
|
+
* npx tsx node_modules/@decocms/start/scripts/generate-loaders.ts --decofile-dir .deco/blocks
|
|
19
26
|
*
|
|
20
27
|
* CLI:
|
|
21
|
-
* --loaders-dir
|
|
22
|
-
* --actions-dir
|
|
23
|
-
* --out-file
|
|
24
|
-
* --exclude
|
|
28
|
+
* --loaders-dir override loaders input (default: src/loaders)
|
|
29
|
+
* --actions-dir override actions input (default: src/actions)
|
|
30
|
+
* --out-file override output (default: src/server/cms/loaders.gen.ts)
|
|
31
|
+
* --exclude comma-separated list of loader keys to skip (they have custom wiring)
|
|
32
|
+
* --decofile-dir if provided, only emit entries whose key appears as `__resolveType` in any JSON
|
|
25
33
|
*/
|
|
26
34
|
import fs from "node:fs";
|
|
27
35
|
import path from "node:path";
|
|
@@ -37,6 +45,8 @@ const actionsDir = path.resolve(process.cwd(), arg("actions-dir", "src/actions")
|
|
|
37
45
|
const outFile = path.resolve(process.cwd(), arg("out-file", "src/server/cms/loaders.gen.ts"));
|
|
38
46
|
const excludeRaw = arg("exclude", "");
|
|
39
47
|
const excludeSet = new Set(excludeRaw.split(",").map((s) => s.trim()).filter(Boolean));
|
|
48
|
+
const decofileDirRaw = arg("decofile-dir", "");
|
|
49
|
+
const decofileDir = decofileDirRaw ? path.resolve(process.cwd(), decofileDirRaw) : null;
|
|
40
50
|
|
|
41
51
|
function walkDir(dir: string): string[] {
|
|
42
52
|
const results: string[] = [];
|
|
@@ -68,6 +78,48 @@ function hasDefaultExport(content: string): boolean {
|
|
|
68
78
|
return /export\s+default\b/.test(content) || /export\s*\{[^}]*\bdefault\b/.test(content);
|
|
69
79
|
}
|
|
70
80
|
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// CMS-referenced loader discovery
|
|
83
|
+
//
|
|
84
|
+
// Walk every JSON file under decofileDir and collect the set of strings that
|
|
85
|
+
// appear as `__resolveType` values. The migration script + generators emit
|
|
86
|
+
// pass-throughs for every loader/action file on disk; without this filter,
|
|
87
|
+
// 90%+ of those entries are dead code (the CMS never references them) and
|
|
88
|
+
// they pollute the type system and bundle.
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
function collectResolveTypes(dir: string): Set<string> {
|
|
92
|
+
const found = new Set<string>();
|
|
93
|
+
if (!fs.existsSync(dir)) return found;
|
|
94
|
+
|
|
95
|
+
const RESOLVE_RE = /"__resolveType"\s*:\s*"([^"]+)"/g;
|
|
96
|
+
|
|
97
|
+
function visit(d: string) {
|
|
98
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
99
|
+
const fullPath = path.join(d, entry.name);
|
|
100
|
+
if (entry.isDirectory()) {
|
|
101
|
+
visit(fullPath);
|
|
102
|
+
} else if (entry.name.endsWith(".json")) {
|
|
103
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
104
|
+
let m: RegExpExecArray | null;
|
|
105
|
+
while ((m = RESOLVE_RE.exec(content)) !== null) {
|
|
106
|
+
found.add(m[1]);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
visit(dir);
|
|
113
|
+
return found;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const cmsReferences = decofileDir ? collectResolveTypes(decofileDir) : null;
|
|
117
|
+
|
|
118
|
+
function isReferenced(key: string): boolean {
|
|
119
|
+
if (!cmsReferences) return true;
|
|
120
|
+
return cmsReferences.has(key) || cmsReferences.has(`${key}.ts`);
|
|
121
|
+
}
|
|
122
|
+
|
|
71
123
|
// ---------------------------------------------------------------------------
|
|
72
124
|
|
|
73
125
|
interface LoaderEntry {
|
|
@@ -76,12 +128,17 @@ interface LoaderEntry {
|
|
|
76
128
|
}
|
|
77
129
|
|
|
78
130
|
const entries: LoaderEntry[] = [];
|
|
131
|
+
let prunedCount = 0;
|
|
79
132
|
|
|
80
133
|
for (const filePath of walkDir(loadersDir)) {
|
|
81
134
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
82
135
|
if (!hasDefaultExport(content)) continue;
|
|
83
136
|
const key = fileToKey(filePath, loadersDir, "site/loaders");
|
|
84
137
|
if (excludeSet.has(key) || excludeSet.has(`${key}.ts`)) continue;
|
|
138
|
+
if (!isReferenced(key)) {
|
|
139
|
+
prunedCount++;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
85
142
|
entries.push({
|
|
86
143
|
key,
|
|
87
144
|
importPath: relativeImportPath(outFile, filePath),
|
|
@@ -93,6 +150,10 @@ for (const filePath of walkDir(actionsDir)) {
|
|
|
93
150
|
if (!hasDefaultExport(content)) continue;
|
|
94
151
|
const key = fileToKey(filePath, actionsDir, "site/actions");
|
|
95
152
|
if (excludeSet.has(key) || excludeSet.has(`${key}.ts`)) continue;
|
|
153
|
+
if (!isReferenced(key)) {
|
|
154
|
+
prunedCount++;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
96
157
|
entries.push({
|
|
97
158
|
key,
|
|
98
159
|
importPath: relativeImportPath(outFile, filePath),
|
|
@@ -108,17 +169,20 @@ const lines: string[] = [
|
|
|
108
169
|
"// Pass-through loader/action entries for COMMERCE_LOADERS.",
|
|
109
170
|
"// Custom-wired entries should be excluded via --exclude and added manually in setup.ts.",
|
|
110
171
|
"",
|
|
111
|
-
"export const siteLoaders: Record<string, (props: any) => Promise<any>> = {",
|
|
172
|
+
"export const siteLoaders: Record<string, (props: any, request?: Request) => Promise<any>> = {",
|
|
112
173
|
];
|
|
113
174
|
|
|
175
|
+
// Cast the dynamic-import default to `any` so legacy 3-arg
|
|
176
|
+
// `(props, req, ctx)` Fresh/Deno loaders still type-check. Any ctx-dependent
|
|
177
|
+
// path in the loader body throws at runtime and must be refactored.
|
|
114
178
|
for (const entry of entries) {
|
|
115
|
-
lines.push(` "${entry.key}": async (props: any) => {`);
|
|
179
|
+
lines.push(` "${entry.key}": async (props: any, request?: Request) => {`);
|
|
116
180
|
lines.push(` const mod = await import("${entry.importPath}");`);
|
|
117
|
-
lines.push(" return mod.default(props);");
|
|
181
|
+
lines.push(" return (mod.default as any)(props, request);");
|
|
118
182
|
lines.push(" },");
|
|
119
|
-
lines.push(` "${entry.key}.ts": async (props: any) => {`);
|
|
183
|
+
lines.push(` "${entry.key}.ts": async (props: any, request?: Request) => {`);
|
|
120
184
|
lines.push(` const mod = await import("${entry.importPath}");`);
|
|
121
|
-
lines.push(" return mod.default(props);");
|
|
185
|
+
lines.push(" return (mod.default as any)(props, request);");
|
|
122
186
|
lines.push(" },");
|
|
123
187
|
}
|
|
124
188
|
|
|
@@ -128,6 +192,9 @@ lines.push("");
|
|
|
128
192
|
fs.mkdirSync(path.dirname(outFile), { recursive: true });
|
|
129
193
|
fs.writeFileSync(outFile, lines.join("\n"));
|
|
130
194
|
|
|
195
|
+
const filterNote = cmsReferences
|
|
196
|
+
? ` (filtered against ${cmsReferences.size} CMS __resolveType references; pruned ${prunedCount} dead entries)`
|
|
197
|
+
: "";
|
|
131
198
|
console.log(
|
|
132
|
-
`Generated ${entries.length} loader entries (${entries.length * 2} with .ts aliases) → ${path.relative(process.cwd(), outFile)}`,
|
|
199
|
+
`Generated ${entries.length} loader entries (${entries.length * 2} with .ts aliases) → ${path.relative(process.cwd(), outFile)}${filterNote}`,
|
|
133
200
|
);
|
|
@@ -32,6 +32,10 @@ const PATTERN_DETECTORS: Array<[DetectedPattern, RegExp]> = [
|
|
|
32
32
|
["head-component", /<Head[\s>]/],
|
|
33
33
|
["define-app", /defineApp\(/],
|
|
34
34
|
["invoke-proxy", /proxy<Manifest/],
|
|
35
|
+
// Bagaggio-style HTMX dynamic-section loader. Both the source file and
|
|
36
|
+
// every call site need manual conversion to React state / createServerFn.
|
|
37
|
+
["sections-component-loader", /sections\/Component\.tsx?$/m],
|
|
38
|
+
["use-component", /import\s*\{[^}]*\buseComponent\b[^}]*\}\s*from\s*["'][^"']*sections\/Component(?:\.tsx?)?["']/],
|
|
35
39
|
];
|
|
36
40
|
|
|
37
41
|
/** Files/dirs that should be completely skipped during scanning */
|
|
@@ -312,6 +316,25 @@ function decideAction(
|
|
|
312
316
|
};
|
|
313
317
|
}
|
|
314
318
|
|
|
319
|
+
// Bagaggio-style HTMX dynamic-section loader → delete and flag.
|
|
320
|
+
// The file uses Deno-only APIs (`toFileUrl(Deno.cwd())`, `import.meta.resolve`)
|
|
321
|
+
// and the `useComponent(component, props)` HTMX render-and-swap pattern, none
|
|
322
|
+
// of which work on TanStack Start / Cloudflare Workers. The site author must
|
|
323
|
+
// refactor every `useComponent(...)` call site to React state, `createServerFn`
|
|
324
|
+
// + `useMutation`, or a direct `~/server/invoke` call BEFORE this file is
|
|
325
|
+
// safe to remove.
|
|
326
|
+
if (
|
|
327
|
+
relPath === "sections/Component.tsx" ||
|
|
328
|
+
relPath === "sections/Component.ts"
|
|
329
|
+
) {
|
|
330
|
+
return {
|
|
331
|
+
action: "delete",
|
|
332
|
+
notes:
|
|
333
|
+
"HTMX dynamic-section loader (useComponent) — incompatible with TanStack Start. " +
|
|
334
|
+
"Migrate every useComponent(...) call site to React state / createServerFn before deploy.",
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
315
338
|
// Static code/tooling files → delete
|
|
316
339
|
if (STATIC_DELETE.has(relPath)) {
|
|
317
340
|
return { action: "delete", notes: "Code/tooling file, not an asset" };
|
|
@@ -637,6 +660,37 @@ export function analyze(ctx: MigrationContext): void {
|
|
|
637
660
|
console.log(` By category: ${JSON.stringify(byCategory)}`);
|
|
638
661
|
console.log(` By action: ${JSON.stringify(byAction)}`);
|
|
639
662
|
|
|
663
|
+
// Surface the HTMX dynamic-section loader and every `useComponent` call site
|
|
664
|
+
// up-front. These need manual conversion to React state / createServerFn before
|
|
665
|
+
// the migrated site will run on Cloudflare Workers — the analyzer cannot do
|
|
666
|
+
// it automatically, so it must be loud enough to land in the report.
|
|
667
|
+
const useComponentSites = ctx.files.filter(
|
|
668
|
+
(f) => f.patterns.includes("use-component"),
|
|
669
|
+
);
|
|
670
|
+
const componentLoaderFile = ctx.files.find(
|
|
671
|
+
(f) =>
|
|
672
|
+
f.path === "sections/Component.tsx" ||
|
|
673
|
+
f.path === "sections/Component.ts",
|
|
674
|
+
);
|
|
675
|
+
if (componentLoaderFile || useComponentSites.length > 0) {
|
|
676
|
+
console.log("\n ⚠ HTMX dynamic-section loader detected (Bagaggio-style)");
|
|
677
|
+
if (componentLoaderFile) {
|
|
678
|
+
console.log(` • ${componentLoaderFile.path} (will be deleted)`);
|
|
679
|
+
}
|
|
680
|
+
if (useComponentSites.length > 0) {
|
|
681
|
+
console.log(` • ${useComponentSites.length} useComponent(...) call site(s) need manual conversion:`);
|
|
682
|
+
for (const f of useComponentSites.slice(0, 10)) {
|
|
683
|
+
console.log(` - ${f.path}`);
|
|
684
|
+
}
|
|
685
|
+
if (useComponentSites.length > 10) {
|
|
686
|
+
console.log(` ... and ${useComponentSites.length - 10} more`);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
console.log(
|
|
690
|
+
" See: deco-to-tanstack-migration skill, 'useComponent / partial sections' section",
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
640
694
|
// Run analyzers
|
|
641
695
|
extractSectionMetadata(ctx);
|
|
642
696
|
classifyIslands(ctx);
|
|
@@ -20,7 +20,10 @@ const ROOT_FILES_TO_DELETE = [
|
|
|
20
20
|
"tailwind.css",
|
|
21
21
|
"tailwind.config.ts",
|
|
22
22
|
"runtime.ts",
|
|
23
|
-
|
|
23
|
+
// NOTE: `constants.ts` is intentionally NOT deleted here — it holds
|
|
24
|
+
// site-specific UI constants (form/drawer IDs, header heights, etc.)
|
|
25
|
+
// that components reference via `~/constants` or `../../constants`.
|
|
26
|
+
// We move it to `src/constants.ts` instead — see `moveRootConstantsToSrc`.
|
|
24
27
|
"fresh.gen.ts",
|
|
25
28
|
"manifest.gen.ts",
|
|
26
29
|
"fresh.config.ts",
|
|
@@ -1251,6 +1254,94 @@ function normalizeImportCasing(ctx: MigrationContext) {
|
|
|
1251
1254
|
});
|
|
1252
1255
|
}
|
|
1253
1256
|
|
|
1257
|
+
/**
|
|
1258
|
+
* Move root-level `constants.ts` → `src/constants.ts`.
|
|
1259
|
+
*
|
|
1260
|
+
* Old stack: a root-level `constants.ts` exporting site-wide UI constants
|
|
1261
|
+
* (MINICART_FORM_ID, SIDEMENU_DRAWER_ID, HEADER_HEIGHT, USER_ID, etc.) that
|
|
1262
|
+
* components reference via `../../constants` or `~/constants`.
|
|
1263
|
+
*
|
|
1264
|
+
* Without this step, `phase-cleanup` deletes the file and the build fails
|
|
1265
|
+
* with `Could not resolve "../../constants"` from many components. The CMS
|
|
1266
|
+
* doesn't reference these IDs, so a 1:1 file move is sufficient.
|
|
1267
|
+
*
|
|
1268
|
+
* If `src/constants.ts` already exists (rare — usually means the migration
|
|
1269
|
+
* was re-run), we leave it alone.
|
|
1270
|
+
*/
|
|
1271
|
+
function moveRootConstantsToSrc(ctx: MigrationContext) {
|
|
1272
|
+
const rootPath = path.join(ctx.sourceDir, "constants.ts");
|
|
1273
|
+
const srcPath = path.join(ctx.sourceDir, "src", "constants.ts");
|
|
1274
|
+
|
|
1275
|
+
if (!fs.existsSync(rootPath)) return;
|
|
1276
|
+
if (fs.existsSync(srcPath)) {
|
|
1277
|
+
log(ctx, `Skipped move: src/constants.ts already exists; deleting root constants.ts`);
|
|
1278
|
+
if (!ctx.dryRun) fs.unlinkSync(rootPath);
|
|
1279
|
+
ctx.deletedFiles.push("constants.ts");
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
if (ctx.dryRun) {
|
|
1284
|
+
log(ctx, `[DRY] Would move: constants.ts → src/constants.ts`);
|
|
1285
|
+
ctx.movedFiles.push({ from: "constants.ts", to: "src/constants.ts" });
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
fs.mkdirSync(path.dirname(srcPath), { recursive: true });
|
|
1290
|
+
fs.renameSync(rootPath, srcPath);
|
|
1291
|
+
ctx.movedFiles.push({ from: "constants.ts", to: "src/constants.ts" });
|
|
1292
|
+
log(ctx, `Moved: constants.ts → src/constants.ts`);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/**
|
|
1296
|
+
* Rewrite the legacy multi-platform `loaders/minicart.ts` file.
|
|
1297
|
+
*
|
|
1298
|
+
* Old stack: `loaders/minicart.ts` runtime-dispatches on `usePlatform()` to
|
|
1299
|
+
* platform-specific loaders under `sdk/cart/{vtex,vnda,wake,linx,shopify,nuvemshop}/loader.ts`.
|
|
1300
|
+
* The cleanup phase already deletes `sdk/cart/` entirely, leaving the loader
|
|
1301
|
+
* with broken imports.
|
|
1302
|
+
*
|
|
1303
|
+
* New stack: the canonical Minicart contract + VTEX transform live in
|
|
1304
|
+
* `@decocms/apps/vtex/inline-loaders/minicart`. We replace the loader with a
|
|
1305
|
+
* thin VTEX-only re-export. Sites on Shopify/VNDA/Wake/Linx/Nuvemshop are
|
|
1306
|
+
* not currently in production on the new stack — when one is, swap this for
|
|
1307
|
+
* a runtime dispatcher again or add a platform-flagged rewrite.
|
|
1308
|
+
*/
|
|
1309
|
+
function rewriteMinicartLoader(ctx: MigrationContext) {
|
|
1310
|
+
const candidates = [
|
|
1311
|
+
path.join(ctx.sourceDir, "src", "loaders", "minicart.ts"),
|
|
1312
|
+
path.join(ctx.sourceDir, "loaders", "minicart.ts"),
|
|
1313
|
+
];
|
|
1314
|
+
|
|
1315
|
+
for (const filePath of candidates) {
|
|
1316
|
+
if (!fs.existsSync(filePath)) continue;
|
|
1317
|
+
|
|
1318
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
1319
|
+
// Only rewrite if it actually imports the legacy multi-platform sdk/cart layout.
|
|
1320
|
+
const isLegacyLoader = /from\s+["'](?:~|\.\.?)\/sdk\/cart\/(?:vtex|vnda|wake|linx|shopify|nuvemshop)\/loader["']/
|
|
1321
|
+
.test(content);
|
|
1322
|
+
if (!isLegacyLoader) continue;
|
|
1323
|
+
|
|
1324
|
+
const newContent = `// VTEX-only minicart loader.
|
|
1325
|
+
//
|
|
1326
|
+
// The legacy site shipped per-platform loaders behind a \`usePlatform()\`
|
|
1327
|
+
// switch (vnda, wake, linx, shopify, nuvemshop). The canonical minicart
|
|
1328
|
+
// contract now lives in \`@decocms/apps\`. Until a non-VTEX customer comes
|
|
1329
|
+
// online on the new stack, we re-export the framework loader directly.
|
|
1330
|
+
// TODO: when adding another platform, replace this with a runtime
|
|
1331
|
+
// dispatcher and import the matching framework loader.
|
|
1332
|
+
export { default } from "@decocms/apps/vtex/inline-loaders/minicart";
|
|
1333
|
+
export type { MinicartProps } from "@decocms/apps/vtex/inline-loaders/minicart";
|
|
1334
|
+
`;
|
|
1335
|
+
|
|
1336
|
+
if (ctx.dryRun) {
|
|
1337
|
+
log(ctx, `[DRY] Would rewrite: ${path.relative(ctx.sourceDir, filePath)} (VTEX-only re-export)`);
|
|
1338
|
+
} else {
|
|
1339
|
+
fs.writeFileSync(filePath, newContent);
|
|
1340
|
+
log(ctx, `Rewrote: ${path.relative(ctx.sourceDir, filePath)} (VTEX-only re-export of @decocms/apps/vtex/inline-loaders/minicart)`);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1254
1345
|
/**
|
|
1255
1346
|
* Fix APIs that don't exist in Cloudflare Workers:
|
|
1256
1347
|
* - window.setTimeout → setTimeout
|
|
@@ -1367,6 +1458,19 @@ export function cleanup(ctx: MigrationContext): void {
|
|
|
1367
1458
|
console.log(" Rewriting VTEX utility imports → ~/lib/ wrappers...");
|
|
1368
1459
|
rewriteVtexUtilImports(ctx);
|
|
1369
1460
|
|
|
1461
|
+
// 11a. Preserve root-level constants.ts (site-wide UI IDs/heights) by
|
|
1462
|
+
// moving it to src/constants.ts. The cleanup phase used to delete it
|
|
1463
|
+
// unconditionally, breaking every component that imports `~/constants`.
|
|
1464
|
+
console.log(" Moving root constants.ts → src/constants.ts...");
|
|
1465
|
+
moveRootConstantsToSrc(ctx);
|
|
1466
|
+
|
|
1467
|
+
// 11b. Rewrite legacy multi-platform minicart loader → VTEX-only re-export.
|
|
1468
|
+
// `sdk/cart/` is deleted by DIRS_TO_DELETE, leaving loaders/minicart.ts
|
|
1469
|
+
// with broken imports. Replace it with a thin re-export of the
|
|
1470
|
+
// framework's @decocms/apps/vtex/inline-loaders/minicart loader.
|
|
1471
|
+
console.log(" Rewriting loaders/minicart.ts → VTEX-only re-export...");
|
|
1472
|
+
rewriteMinicartLoader(ctx);
|
|
1473
|
+
|
|
1370
1474
|
// 12. Fix useVariantPossiblities omit set
|
|
1371
1475
|
console.log(" Fixing useVariantPossiblities omit set...");
|
|
1372
1476
|
fixVariantOmitSet(ctx);
|
|
@@ -126,6 +126,48 @@ export function transform(ctx: MigrationContext): void {
|
|
|
126
126
|
});
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
// Flag the legacy sections/Component.tsx dynamic-section loader.
|
|
130
|
+
// This file uses Deno-specific APIs (toFileUrl, import.meta.resolve)
|
|
131
|
+
// and the HTMX-driven `useComponent(component, props)` pattern, which
|
|
132
|
+
// do not run on Cloudflare Workers and have no equivalent in
|
|
133
|
+
// @decocms/start. The whole file must be deleted.
|
|
134
|
+
if (
|
|
135
|
+
/sections\/Component\.tsx?$/.test(record.path) ||
|
|
136
|
+
/sections\/Component\.tsx?$/.test(targetPath)
|
|
137
|
+
) {
|
|
138
|
+
ctx.manualReviewItems.push({
|
|
139
|
+
file: targetPath,
|
|
140
|
+
reason:
|
|
141
|
+
"sections/Component.tsx (Deno HTMX dynamic-section loader) is incompatible with TanStack Start / Cloudflare Workers. " +
|
|
142
|
+
"DELETE this file and migrate every `useComponent(...)` call site to one of: " +
|
|
143
|
+
"(a) local React state for client-side toggles, " +
|
|
144
|
+
"(b) `createServerFn` + `useMutation` for server actions, or " +
|
|
145
|
+
"(c) a direct `invoke` call (`~/server/invoke`) for ad-hoc loaders. " +
|
|
146
|
+
"See: deco-to-tanstack-migration skill, 'useComponent / partial sections' section.",
|
|
147
|
+
severity: "error",
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Flag any import of useComponent — typically `import { useComponent } from "site/sections/Component.tsx"`.
|
|
152
|
+
// We also catch `from "../../sections/Component"` and similar relative variants.
|
|
153
|
+
if (
|
|
154
|
+
/\buseComponent\b/.test(result.content) &&
|
|
155
|
+
/from\s+["'][^"']*sections\/Component(?:\.tsx?)?["']/.test(result.content)
|
|
156
|
+
) {
|
|
157
|
+
ctx.manualReviewItems.push({
|
|
158
|
+
file: targetPath,
|
|
159
|
+
reason:
|
|
160
|
+
"useComponent({ ... }) call site detected. This is the HTMX-style dynamic-section render pattern " +
|
|
161
|
+
"that ships HTML fragments and swaps them client-side. It does not work on TanStack Start. " +
|
|
162
|
+
"Recipes: " +
|
|
163
|
+
"(1) Self-contained UI toggles → keep state in React (`useState` + event handlers); " +
|
|
164
|
+
"(2) Form submissions / mutations → `createServerFn` + `useMutation` (see casaevideo-storefront for canonical examples); " +
|
|
165
|
+
"(3) Ad-hoc data fetches → call the loader/action via `~/server/invoke` and store results in `useState`. " +
|
|
166
|
+
"Remove the import after refactoring, then delete `src/sections/Component.tsx`.",
|
|
167
|
+
severity: "error",
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
129
171
|
if (ctx.dryRun) {
|
|
130
172
|
if (result.changed) {
|
|
131
173
|
log(ctx, `[DRY] Would transform: ${record.path} → ${targetPath}`);
|
|
@@ -83,7 +83,7 @@ export function generateCommerceLoaders(ctx: MigrationContext): string {
|
|
|
83
83
|
lines.push(``);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
lines.push(`export const COMMERCE_LOADERS: Record<string, (props: any) => Promise<any>> = {`);
|
|
86
|
+
lines.push(`export const COMMERCE_LOADERS: Record<string, (props: any, request?: Request) => Promise<any>> = {`);
|
|
87
87
|
|
|
88
88
|
if (ctx.platform === "vtex") {
|
|
89
89
|
lines.push(` ...vtexLoaders,`);
|
|
@@ -156,7 +156,10 @@ export function generateSectionLoaders(ctx: MigrationContext): string {
|
|
|
156
156
|
const importPath = `~/` + meta.path.replace(/\.tsx?$/, "");
|
|
157
157
|
entries.push(` "${sectionKey}": async (props: any, req: Request) => {`);
|
|
158
158
|
entries.push(` const mod = await import("${importPath}");`);
|
|
159
|
-
|
|
159
|
+
// Cast to any: legacy Fresh/Deno section loaders are typed `(props, req, ctx)`.
|
|
160
|
+
// We invoke with 2 args; any ctx-dependent code path inside the loader will throw
|
|
161
|
+
// at runtime and must be refactored — the migration phase-transform flags these.
|
|
162
|
+
entries.push(` if (typeof mod.loader === "function") return (mod.loader as any)(props, req);`);
|
|
160
163
|
entries.push(` return props;`);
|
|
161
164
|
entries.push(` },`);
|
|
162
165
|
}
|
package/scripts/migrate/types.ts
CHANGED
|
@@ -61,7 +61,9 @@ export type DetectedPattern =
|
|
|
61
61
|
| "asset-function"
|
|
62
62
|
| "head-component"
|
|
63
63
|
| "define-app"
|
|
64
|
-
| "invoke-proxy"
|
|
64
|
+
| "invoke-proxy"
|
|
65
|
+
| "use-component"
|
|
66
|
+
| "sections-component-loader";
|
|
65
67
|
|
|
66
68
|
/** Metadata extracted from a section file during analysis */
|
|
67
69
|
export interface SectionMeta {
|
package/src/cms/resolve.ts
CHANGED
|
@@ -228,7 +228,18 @@ function isBot(userAgent?: string): boolean {
|
|
|
228
228
|
return botPatterns.some((re) => re.test(userAgent));
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
|
|
231
|
+
/**
|
|
232
|
+
* A loader registered against a `__resolveType` key. The runtime invokes it
|
|
233
|
+
* through two paths:
|
|
234
|
+
*
|
|
235
|
+
* 1. CMS resolution (`commerceLoader(resolvedProps)`) — 1-arg call.
|
|
236
|
+
* 2. `/deco/invoke/...` endpoint — `(props, request)` 2-arg call.
|
|
237
|
+
*
|
|
238
|
+
* Loaders that need the `Request` (cookies, geo, headers) declare the second
|
|
239
|
+
* parameter; pure loaders ignore it. This shape lets a single registry serve
|
|
240
|
+
* both invocation paths without `as any` casts at every wrapper.
|
|
241
|
+
*/
|
|
242
|
+
export type CommerceLoader = (props: any, request?: Request) => Promise<any>;
|
|
232
243
|
|
|
233
244
|
/**
|
|
234
245
|
* Context passed through the resolution pipeline.
|
package/src/sdk/useScript.ts
CHANGED
|
@@ -137,13 +137,34 @@ export function inlineScript(js: string) {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
140
|
+
* @deprecated Removed in TanStack Start.
|
|
141
|
+
*
|
|
142
|
+
* The Fresh/Deno HTMX-based partial-section pattern (`useSection` /
|
|
143
|
+
* `usePartialSection` + `sections/Component.tsx`) does not apply on
|
|
144
|
+
* Cloudflare Workers and React. Replace call-sites with one of:
|
|
145
|
+
*
|
|
146
|
+
* 1. Local React state (`useState` + event handlers) for client-side toggles.
|
|
147
|
+
* 2. `createServerFn` + `useMutation` for server actions.
|
|
148
|
+
* 3. Direct `invoke` calls (`~/server/invoke`) for ad-hoc loaders.
|
|
149
|
+
*
|
|
150
|
+
* See: deco-to-tanstack-migration skill, "useComponent / partial sections"
|
|
151
|
+
* section, for the per-pattern recipes.
|
|
152
|
+
*
|
|
153
|
+
* Both stubs throw at runtime (and at import time, if you call them at
|
|
154
|
+
* module top level) so legacy code surfaces a clear error instead of a
|
|
155
|
+
* silent no-op.
|
|
142
156
|
*/
|
|
143
|
-
|
|
144
|
-
|
|
157
|
+
const DEPRECATION_MESSAGE =
|
|
158
|
+
"[@decocms/start] useSection / usePartialSection were removed. " +
|
|
159
|
+
"The Fresh/Deno HTMX partial-section pattern does not apply on " +
|
|
160
|
+
"TanStack Start / Cloudflare Workers. Replace call-sites with " +
|
|
161
|
+
"createServerFn + useMutation, or local React state. See the " +
|
|
162
|
+
"deco-to-tanstack-migration skill for per-pattern recipes.";
|
|
163
|
+
|
|
164
|
+
export function usePartialSection(_props?: Record<string, unknown>): never {
|
|
165
|
+
throw new Error(DEPRECATION_MESSAGE);
|
|
145
166
|
}
|
|
146
167
|
|
|
147
|
-
export function useSection(_props?: Record<string, unknown>) {
|
|
148
|
-
|
|
168
|
+
export function useSection(_props?: Record<string, unknown>): never {
|
|
169
|
+
throw new Error(DEPRECATION_MESSAGE);
|
|
149
170
|
}
|
package/src/sdk/workerEntry.ts
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
} from "./cacheHeaders";
|
|
35
35
|
import { buildHtmlShell } from "./htmlShell";
|
|
36
36
|
import { cleanPathForCacheKey } from "./urlUtils";
|
|
37
|
-
import { isMobileUA } from "./useDevice";
|
|
37
|
+
import { type Device, isMobileUA } from "./useDevice";
|
|
38
38
|
import { getRenderShellConfig } from "../admin/setup";
|
|
39
39
|
import { RequestContext } from "./requestContext";
|
|
40
40
|
import { getAppMiddleware } from "./setupApps";
|
|
@@ -88,7 +88,16 @@ interface ServerEntry {
|
|
|
88
88
|
* cache entry; different segments get different cached responses.
|
|
89
89
|
*/
|
|
90
90
|
export interface SegmentKey {
|
|
91
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Device class derived from the request User-Agent.
|
|
93
|
+
*
|
|
94
|
+
* Accepts the full `Device` union (`"mobile" | "desktop" | "tablet"`) so
|
|
95
|
+
* that callers can pass `detectDevice(...)` directly without manual
|
|
96
|
+
* narrowing. Sites that want to share cache entries between mobile and
|
|
97
|
+
* tablet can collapse the value at the call site (e.g.
|
|
98
|
+
* `device === "tablet" ? "mobile" : device`).
|
|
99
|
+
*/
|
|
100
|
+
device: Device;
|
|
92
101
|
/** Whether the user is logged in (e.g., has a valid auth cookie). */
|
|
93
102
|
loggedIn?: boolean;
|
|
94
103
|
/** Commerce sales channel / price list. */
|
package/src/setup.ts
CHANGED
|
@@ -96,7 +96,7 @@ export interface SiteSetupOptions {
|
|
|
96
96
|
* { getCommerceLoaders: () => COMMERCE_LOADERS }
|
|
97
97
|
* ```
|
|
98
98
|
*/
|
|
99
|
-
getCommerceLoaders?: () => Record<string, (props: any) => Promise<any>>;
|
|
99
|
+
getCommerceLoaders?: () => Record<string, (props: any, request?: Request) => Promise<any>>;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
/**
|