@decocms/start 1.6.2 → 1.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/.cursor/skills/deco-to-tanstack-migration/SKILL.md +85 -12
- package/.cursor/skills/deco-to-tanstack-migration/references/gotchas.md +98 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/package-json.md +45 -25
- package/.cursor/skills/deco-to-tanstack-migration/templates/root-route.md +56 -39
- package/.cursor/skills/deco-to-tanstack-migration/templates/setup-ts.md +122 -141
- package/.releaserc.json +1 -0
- package/package.json +1 -1
- package/scripts/generate-blocks.ts +8 -5
- package/scripts/generate-loaders.ts +79 -12
- package/scripts/migrate/analyzers/island-classifier.ts +23 -0
- package/scripts/migrate/analyzers/section-metadata.ts +63 -7
- package/scripts/migrate/phase-analyze.ts +190 -11
- package/scripts/migrate/phase-cleanup.ts +1162 -7
- package/scripts/migrate/phase-scaffold.ts +294 -5
- package/scripts/migrate/phase-transform.ts +56 -3
- package/scripts/migrate/templates/app-css.ts +149 -2
- package/scripts/migrate/templates/commerce-loaders.ts +174 -69
- package/scripts/migrate/templates/lib-utils.ts +255 -0
- package/scripts/migrate/templates/package-json.ts +30 -22
- package/scripts/migrate/templates/routes.ts +81 -11
- package/scripts/migrate/templates/section-loaders.ts +369 -33
- package/scripts/migrate/templates/server-entry.ts +350 -80
- package/scripts/migrate/templates/setup.ts +78 -8
- package/scripts/migrate/templates/types-gen.ts +58 -0
- package/scripts/migrate/templates/ui-components.ts +47 -16
- package/scripts/migrate/templates/vite-config.ts +17 -6
- package/scripts/migrate/templates/wrangler.ts +3 -1
- package/scripts/migrate/transforms/dead-code.ts +330 -4
- package/scripts/migrate/transforms/deno-isms.ts +19 -0
- package/scripts/migrate/transforms/imports.ts +93 -30
- package/scripts/migrate/transforms/jsx.ts +79 -4
- package/scripts/migrate/transforms/section-conventions.ts +105 -3
- package/scripts/migrate/types.ts +9 -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
|
@@ -1,167 +1,148 @@
|
|
|
1
1
|
# setup.ts Template
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Minimal, convention-driven setup. Matches `@decocms/start >= 1.6.2`. Based on the storefront-tanstack Shopify port.
|
|
4
|
+
|
|
5
|
+
Three framework composers do the work — the site file is short by design:
|
|
6
|
+
|
|
7
|
+
- `createSiteSetup(options)` — CMS engine + admin protocol + matcher registration
|
|
8
|
+
- `applySectionConventions(gen)` — reads `sections.gen.ts` and wires eager/layout/seo/cache/sync sections
|
|
9
|
+
- `autoconfigApps(blocks, APP_REGISTRY)` — dual-registers every app's loaders + actions from `@decocms/apps/registry`
|
|
4
10
|
|
|
5
11
|
```typescript
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Site setup — orchestrator that wires framework, apps, and sections.
|
|
14
|
+
*
|
|
15
|
+
* App-installed loaders + actions (Shopify, VTEX, Resend, …) are wired via
|
|
16
|
+
* `autoconfigApps(blocks, APP_REGISTRY)` — adding a new app is a one-line
|
|
17
|
+
* entry in `@decocms/apps/registry.ts`, no change needed here.
|
|
18
|
+
*
|
|
19
|
+
* Section-specific prop enrichment lives in `setup/section-loaders.ts`.
|
|
20
|
+
* Section metadata (eager, sync, layout, cache, LoadingFallback) is declared
|
|
21
|
+
* in each section file and auto-extracted by generate-sections.ts.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import "./cache-config";
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
registerCommerceLoaders,
|
|
28
|
+
applySectionConventions,
|
|
29
|
+
} from "@decocms/start/cms";
|
|
30
|
+
import { createSiteSetup } from "@decocms/start/setup";
|
|
31
|
+
import { autoconfigApps } from "@decocms/start/apps";
|
|
32
|
+
import { createInstrumentedFetch } from "@decocms/start/sdk/instrumentedFetch";
|
|
33
|
+
import { initShopifyFromBlocks, setShopifyFetch } from "@decocms/apps/shopify";
|
|
34
|
+
import { APP_REGISTRY } from "@decocms/apps/registry";
|
|
35
|
+
import { blocks as generatedBlocks } from "./server/cms/blocks.gen";
|
|
36
|
+
import {
|
|
37
|
+
sectionMeta,
|
|
38
|
+
syncComponents,
|
|
39
|
+
loadingFallbacks,
|
|
40
|
+
} from "./server/cms/sections.gen";
|
|
41
|
+
import { PreviewProviders } from "@decocms/start/hooks";
|
|
42
|
+
// @ts-ignore Vite ?url import
|
|
18
43
|
import appCss from "./styles/app.css?url";
|
|
19
44
|
|
|
20
|
-
|
|
21
|
-
setBlocks(blocksJson);
|
|
45
|
+
import "./setup/section-loaders";
|
|
22
46
|
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
47
|
+
// -- Framework setup --
|
|
48
|
+
createSiteSetup({
|
|
49
|
+
sections: import.meta.glob("./sections/**/*.tsx") as Record<string, () => Promise<any>>,
|
|
50
|
+
blocks: generatedBlocks,
|
|
51
|
+
meta: () => import("./server/admin/meta.gen.json").then((m) => m.default),
|
|
28
52
|
css: appCss,
|
|
29
|
-
fonts: [
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
53
|
+
fonts: [],
|
|
54
|
+
productionOrigins: [
|
|
55
|
+
"https://www.<SITE>.com.br",
|
|
56
|
+
"https://<SITE>.com.br",
|
|
57
|
+
],
|
|
58
|
+
previewWrapper: PreviewProviders,
|
|
59
|
+
initPlatform: (blocks) => initShopifyFromBlocks(blocks), // or initVtexFromBlocks
|
|
60
|
+
onResolveError: (error, resolveType, context) => {
|
|
61
|
+
console.error(`[CMS-DEBUG] ${context} "${resolveType}" failed:`, error);
|
|
62
|
+
},
|
|
63
|
+
onDanglingReference: (resolveType) => {
|
|
64
|
+
console.warn(`[CMS-DEBUG] Dangling reference: ${resolveType}`);
|
|
65
|
+
return null;
|
|
66
|
+
},
|
|
33
67
|
});
|
|
34
68
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
// ==========================================================================
|
|
69
|
+
// -- Platform fetch instrumentation (optional, for observability) --
|
|
70
|
+
setShopifyFetch(createInstrumentedFetch("shopify"));
|
|
38
71
|
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
72
|
+
// -- Convention-driven section registration --
|
|
73
|
+
applySectionConventions({
|
|
74
|
+
meta: sectionMeta,
|
|
75
|
+
syncComponents,
|
|
76
|
+
loadingFallbacks,
|
|
77
|
+
sectionGlob: import.meta.glob("./sections/**/*.tsx") as Record<string, () => Promise<any>>,
|
|
78
|
+
});
|
|
42
79
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
80
|
+
// -- Apps: auto-configure from decofile against the @decocms/apps registry --
|
|
81
|
+
// Dual-registers into commerce loaders (CMS resolve) + invoke handlers (admin)
|
|
82
|
+
// for every configured app. Adding a new app = add an entry in
|
|
83
|
+
// @decocms/apps/registry.ts. No change here per app.
|
|
84
|
+
await autoconfigApps(generatedBlocks, APP_REGISTRY);
|
|
47
85
|
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"site/
|
|
57
|
-
"site/sections/Footer/Footer.tsx": () => import("./sections/Footer/Footer"),
|
|
58
|
-
"site/sections/Theme/Theme.tsx": () => import("./sections/Theme/Theme"),
|
|
59
|
-
// ... register ALL sections from src/sections/ here
|
|
60
|
-
// Pattern: "site/sections/Path/Name.tsx": () => import("./sections/Path/Name")
|
|
86
|
+
// -- Site-local loaders (not shipped by an app) --
|
|
87
|
+
// Register .ts and bare variants — CMS resolver may query either.
|
|
88
|
+
registerCommerceLoaders({
|
|
89
|
+
"site/loaders/minicart.ts": async () => (await import("./loaders/minicart")).default(),
|
|
90
|
+
"site/loaders/minicart": async () => (await import("./loaders/minicart")).default(),
|
|
91
|
+
"site/loaders/user.ts": async () => (await import("./loaders/user")).default(),
|
|
92
|
+
"site/loaders/user": async () => (await import("./loaders/user")).default(),
|
|
93
|
+
"site/loaders/wishlist.ts": async () => (await import("./loaders/wishlist")).default(),
|
|
94
|
+
"site/loaders/wishlist": async () => (await import("./loaders/wishlist")).default(),
|
|
61
95
|
});
|
|
96
|
+
```
|
|
62
97
|
|
|
63
|
-
|
|
64
|
-
// 3. LAYOUT SECTIONS
|
|
65
|
-
// ==========================================================================
|
|
66
|
-
|
|
67
|
-
// Layout sections are always rendered eagerly (never lazy/deferred),
|
|
68
|
-
// even if wrapped in Lazy.tsx in the CMS.
|
|
69
|
-
registerLayoutSections([
|
|
70
|
-
"site/sections/Header/Header.tsx",
|
|
71
|
-
"site/sections/Footer/Footer.tsx",
|
|
72
|
-
"site/sections/Theme/Theme.tsx",
|
|
73
|
-
"site/sections/Social/WhatsApp.tsx",
|
|
74
|
-
]);
|
|
75
|
-
|
|
76
|
-
// ==========================================================================
|
|
77
|
-
// 4. SECTION LOADERS
|
|
78
|
-
// ==========================================================================
|
|
79
|
-
|
|
80
|
-
// Section loaders enrich CMS props with server-side data (e.g., VTEX API calls).
|
|
81
|
-
// Only needed for sections that export `const loader`.
|
|
82
|
-
registerSectionLoaders({
|
|
83
|
-
"site/sections/Product/ProductShelf.tsx": (props: any, req: Request) =>
|
|
84
|
-
import("./components/product/ProductShelf").then((m) => m.loader(props, req)),
|
|
85
|
-
"site/sections/Product/SearchResult.tsx": (props: any, req: Request) =>
|
|
86
|
-
import("./components/search/SearchResult").then((m) => m.loader(props, req)),
|
|
87
|
-
// ... add for each section that has `export const loader`
|
|
88
|
-
});
|
|
98
|
+
## Section metadata convention
|
|
89
99
|
|
|
90
|
-
|
|
91
|
-
// 5. COMMERCE LOADERS (VTEX)
|
|
92
|
-
// ==========================================================================
|
|
100
|
+
Declare section behavior in the section file, not in setup.ts. `generate-sections.ts` scans `src/sections/**` and emits `src/server/cms/sections.gen.ts`.
|
|
93
101
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
import { vtexSuggestions } from "@decocms/apps/vtex/loaders/suggestions";
|
|
98
|
-
import { initVtexFromBlocks } from "@decocms/apps/vtex/setup";
|
|
102
|
+
```typescript
|
|
103
|
+
// src/sections/Header/Header.tsx
|
|
104
|
+
export default function Header(props) { /* ... */ }
|
|
99
105
|
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const cachedPDP = createCachedLoader("vtex/pdp", vtexProductDetailsPage, {
|
|
105
|
-
policy: "stale-while-revalidate", maxAge: 30_000,
|
|
106
|
-
});
|
|
107
|
-
const cachedPLP = createCachedLoader("vtex/plp", vtexProductListingPage, {
|
|
108
|
-
policy: "stale-while-revalidate", maxAge: 60_000,
|
|
109
|
-
});
|
|
110
|
-
const cachedSuggestions = createCachedLoader("vtex/suggestions", vtexSuggestions, {
|
|
111
|
-
policy: "stale-while-revalidate", maxAge: 120_000,
|
|
112
|
-
});
|
|
106
|
+
export const eager = true; // always render eagerly (bypass Lazy wrappers)
|
|
107
|
+
export const sync = true; // import bundled, not code-split (for first paint)
|
|
108
|
+
export const layout = true; // render as a layout section (header/footer/theme)
|
|
109
|
+
```
|
|
113
110
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
"vtex/loaders/intelligentSearch/productListingPage.ts": cachedPLP,
|
|
118
|
-
"vtex/loaders/intelligentSearch/productDetailsPage.ts": cachedPDP,
|
|
119
|
-
"vtex/loaders/intelligentSearch/suggestions.ts": cachedSuggestions,
|
|
120
|
-
// Add passthrough loaders for types that don't need caching:
|
|
121
|
-
// "vtex/loaders/config.ts": (props) => props,
|
|
122
|
-
});
|
|
111
|
+
```typescript
|
|
112
|
+
// src/sections/Product/SearchResult.tsx
|
|
113
|
+
export const cache = "listing"; // SWR cache profile
|
|
123
114
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
115
|
+
export function LoadingFallback() {
|
|
116
|
+
return <div className="animate-pulse h-96 bg-base-200" />;
|
|
117
|
+
}
|
|
118
|
+
```
|
|
127
119
|
|
|
128
|
-
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
});
|
|
120
|
+
```typescript
|
|
121
|
+
// src/sections/SEO/SeoPDP.tsx
|
|
122
|
+
export const seo = true; // extract page SEO from this section's output
|
|
123
|
+
```
|
|
133
124
|
|
|
134
|
-
|
|
135
|
-
// 7. ASYNC RENDERING
|
|
136
|
-
// ==========================================================================
|
|
137
|
-
|
|
138
|
-
// Enable deferred section loading (scroll-triggered).
|
|
139
|
-
// Respects CMS Lazy wrappers. Layout sections and alwaysEager are never deferred.
|
|
140
|
-
setAsyncRenderingConfig({
|
|
141
|
-
alwaysEager: [
|
|
142
|
-
"site/sections/Header/Header.tsx",
|
|
143
|
-
"site/sections/Footer/Footer.tsx",
|
|
144
|
-
"site/sections/Theme/Theme.tsx",
|
|
145
|
-
"site/sections/Images/Carousel.tsx",
|
|
146
|
-
// Add above-the-fold sections here
|
|
147
|
-
],
|
|
148
|
-
});
|
|
125
|
+
After changes to any `export const <flag>` or `LoadingFallback`, re-run `npm run generate:sections` (or `npm run build`).
|
|
149
126
|
|
|
150
|
-
|
|
151
|
-
// 8. INVOKE LOADERS (for /deco/invoke endpoint)
|
|
152
|
-
// ==========================================================================
|
|
127
|
+
## What NOT to put here
|
|
153
128
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
129
|
+
- **Section imports** — lazy via Vite glob, sync via `sectionMeta.sync` + `syncComponents`. Zero manual imports needed.
|
|
130
|
+
- **App loaders/actions** — `autoconfigApps` registers every entry in `APP_REGISTRY`. Adding Shopify/VTEX/Resend loaders by hand is a bug.
|
|
131
|
+
- **alwaysEager arrays** — driven by `export const eager = true` in section files.
|
|
132
|
+
- **SEO registration** — driven by `export const seo = true`.
|
|
133
|
+
- **Cache profile arrays** — driven by `export const cache = "..."`.
|
|
134
|
+
- **`setMetaData` / `setRenderShell` / `setInvokeLoaders` calls** — `createSiteSetup` handles them. Only reach for the low-level APIs if composing a non-standard pipeline.
|
|
135
|
+
|
|
136
|
+
## Adding a new app (e.g., 4th commerce integration)
|
|
137
|
+
|
|
138
|
+
1. `@decocms/apps/registry.ts` gets one new entry: `{ blockKey, module, displayName, category, description }`
|
|
139
|
+
2. Bump `@decocms/apps` minor version; site installs the bump
|
|
140
|
+
3. Add the block in `.deco/blocks/<blockKey>.json` with platform config
|
|
141
|
+
4. Nothing else changes — `autoconfigApps` picks it up on next boot
|
|
160
142
|
|
|
161
|
-
##
|
|
143
|
+
## See also
|
|
162
144
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
5. **alwaysEager**: Sections that must render on first paint (no deferred loading)
|
|
145
|
+
- `applySectionConventions` source: `@decocms/start/src/cms/applySectionConventions.ts`
|
|
146
|
+
- `autoconfigApps` source: `@decocms/start/src/apps/autoconfig.ts`
|
|
147
|
+
- Registry source: `@decocms/apps/registry.ts`
|
|
148
|
+
- Generate scripts: `@decocms/start/scripts/generate-{blocks,schema,sections,loaders,invoke}.ts`
|
package/.releaserc.json
CHANGED
package/package.json
CHANGED
|
@@ -62,14 +62,17 @@ if (!fs.existsSync(blocksDir)) {
|
|
|
62
62
|
|
|
63
63
|
const files = fs.readdirSync(blocksDir).filter((f) => f.endsWith(".json"));
|
|
64
64
|
|
|
65
|
-
// Deduplicate:
|
|
65
|
+
// Deduplicate: when multiple files decode to the same key, prefer the one
|
|
66
|
+
// with actual content (largest file size wins over empty {} stubs).
|
|
66
67
|
const blockFiles: Record<string, string> = {};
|
|
67
68
|
for (const file of files) {
|
|
68
69
|
const name = decodeBlockName(file);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
if (blockFiles[name]) {
|
|
71
|
+
const existingSize = fs.statSync(path.join(blocksDir, blockFiles[name])).size;
|
|
72
|
+
const newSize = fs.statSync(path.join(blocksDir, file)).size;
|
|
73
|
+
if (newSize > existingSize) {
|
|
74
|
+
blockFiles[name] = file;
|
|
75
|
+
}
|
|
73
76
|
continue;
|
|
74
77
|
}
|
|
75
78
|
blockFiles[name] = file;
|
|
@@ -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
|
);
|
|
@@ -4,6 +4,7 @@ import type { MigrationContext, IslandClassification } from "../types.ts";
|
|
|
4
4
|
import { log } from "../types.ts";
|
|
5
5
|
|
|
6
6
|
const REEXPORT_RE = /^export\s+\{\s*default\s*\}\s+from\s+["']([^"']+)["']/m;
|
|
7
|
+
const NAMED_REEXPORT_RE = /^export\s+\{[^}]+\}\s+from\s+["']([^"']+)["']/m;
|
|
7
8
|
const THIN_WRAPPER_RE = /^import\s+(\w+)\s+from\s+["']([^"']+)["']/m;
|
|
8
9
|
const RETURN_COMPONENT_RE = /return\s+<\s*\w+\s+\{\.\.\.props\}/;
|
|
9
10
|
|
|
@@ -42,6 +43,28 @@ export function classifyIslands(ctx: MigrationContext): void {
|
|
|
42
43
|
continue;
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
// Check for named re-export only file: export { A, B } from "..."
|
|
47
|
+
// (entire file is just re-exports, no other logic)
|
|
48
|
+
if (lineCount <= 10) {
|
|
49
|
+
const namedReExportMatch = content.match(NAMED_REEXPORT_RE);
|
|
50
|
+
if (namedReExportMatch) {
|
|
51
|
+
const trimmedLines = nonEmptyLines.map((l) => l.trim());
|
|
52
|
+
const hasOtherCode = trimmedLines.some(
|
|
53
|
+
(l) => !l.startsWith("export") && !l.startsWith("//") && !l.startsWith("/*") && !l.startsWith("*") && !l.startsWith("}") && !l.startsWith(",") && !/^\w+,?$/.test(l)
|
|
54
|
+
);
|
|
55
|
+
if (!hasOtherCode) {
|
|
56
|
+
ctx.islandClassifications.push({
|
|
57
|
+
path: file.path,
|
|
58
|
+
type: "wrapper",
|
|
59
|
+
wrapsComponent: namedReExportMatch[1],
|
|
60
|
+
suggestedTarget: `src/${file.path.replace("islands/", "components/")}`,
|
|
61
|
+
lineCount,
|
|
62
|
+
});
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
45
68
|
// Check for thin wrapper pattern: import X from "...", return <X {...props} />
|
|
46
69
|
if (lineCount <= 15) {
|
|
47
70
|
const importMatch = content.match(THIN_WRAPPER_RE);
|
|
@@ -10,15 +10,18 @@ const LISTING_RE = /\b(?:shelf|carousel|slider|product\s*list|search\s*result)\b
|
|
|
10
10
|
|
|
11
11
|
const LOADER_CONST_RE = /^export\s+const\s+loader\b/m;
|
|
12
12
|
const LOADER_FN_RE = /^export\s+(?:async\s+)?function\s+loader\b/m;
|
|
13
|
-
const
|
|
13
|
+
const LOADER_REEXPORT_RE = /^export\s*\{[^}]*\bloader\b[^}]*\}\s*from/m;
|
|
14
|
+
const LOADING_FALLBACK_RE = /^export\s+(?:const|function)\s+LoadingFallback\b|^export\s*\{[^}]*LoadingFallback[^}]*\}/m;
|
|
14
15
|
const JSDOC_TITLE_RE = /@title\b/;
|
|
15
16
|
const JSDOC_DESC_RE = /@description\b/;
|
|
16
17
|
const CTX_DEVICE_RE = /ctx\.device|useDevice|device.*(?:mobile|desktop)/i;
|
|
18
|
+
const DEVICE_PROP_RE_BROAD = /\bdevice\b.*(?:mobile|desktop|tablet)|\bisMobile\b|\bis_mobile\b/i;
|
|
17
19
|
const CTX_URL_RE = /ctx\.url|req\.url|ctx\.request|searchParam|pathname/i;
|
|
18
20
|
const ASYNC_RE = /^export\s+async\s+function\s+loader\b/m;
|
|
19
21
|
const STATUS_ONLY_RE = /ctx\.response\.status\s*=/;
|
|
20
22
|
const IS_MOBILE_RE = /isMobile|is_mobile|ctx\.device\s*===?\s*["']mobile["']/i;
|
|
21
23
|
const DEVICE_PROP_RE = /device\s*:\s*ctx\.device/;
|
|
24
|
+
const REEXPORT_FROM_RE = /^export\s*\{[^}]*\}\s*from\s*["']([^"']+)["']/m;
|
|
22
25
|
|
|
23
26
|
function isStatusOnlyLoader(content: string): boolean {
|
|
24
27
|
const loaderMatch = content.match(
|
|
@@ -39,6 +42,50 @@ function isStatusOnlyLoader(content: string): boolean {
|
|
|
39
42
|
return meaningful.replace(/[\s{}();,]/g, "").length < 30;
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Resolve a re-export target path to an absolute file path.
|
|
47
|
+
* Handles `~/` prefix (mapped to `src/`) and relative paths.
|
|
48
|
+
*/
|
|
49
|
+
function resolveReExportTarget(importPath: string, sectionAbsPath: string, sourceDir: string): string | null {
|
|
50
|
+
let resolved: string;
|
|
51
|
+
if (importPath.startsWith("~/")) {
|
|
52
|
+
resolved = path.join(sourceDir, "src", importPath.slice(2));
|
|
53
|
+
} else if (importPath.startsWith("./") || importPath.startsWith("../")) {
|
|
54
|
+
resolved = path.resolve(path.dirname(sectionAbsPath), importPath);
|
|
55
|
+
} else {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const candidates = [
|
|
60
|
+
resolved + ".tsx", resolved + ".ts",
|
|
61
|
+
path.join(resolved, "index.tsx"), path.join(resolved, "index.ts"),
|
|
62
|
+
resolved, // might already have extension
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
for (const c of candidates) {
|
|
66
|
+
if (fs.existsSync(c)) return c;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Try to read the content of the component that a section barrel re-exports from.
|
|
73
|
+
* Returns null if the section is not a barrel or the target can't be found.
|
|
74
|
+
*/
|
|
75
|
+
function readReExportTargetContent(content: string, sectionAbsPath: string, sourceDir: string): string | null {
|
|
76
|
+
const match = content.match(REEXPORT_FROM_RE);
|
|
77
|
+
if (!match) return null;
|
|
78
|
+
|
|
79
|
+
const targetPath = resolveReExportTarget(match[1], sectionAbsPath, sourceDir);
|
|
80
|
+
if (!targetPath) return null;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
return fs.readFileSync(targetPath, "utf-8");
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
42
89
|
export function extractSectionMetadata(ctx: MigrationContext): void {
|
|
43
90
|
const sectionFiles = ctx.files.filter(
|
|
44
91
|
(f) => f.category === "section" && f.action !== "delete",
|
|
@@ -58,26 +105,35 @@ export function extractSectionMetadata(ctx: MigrationContext): void {
|
|
|
58
105
|
|
|
59
106
|
const hasLoaderConst = LOADER_CONST_RE.test(content);
|
|
60
107
|
const hasLoaderFn = LOADER_FN_RE.test(content);
|
|
61
|
-
const
|
|
108
|
+
const hasLoaderReExport = LOADER_REEXPORT_RE.test(content);
|
|
109
|
+
const hasLoader = hasLoaderConst || hasLoaderFn || hasLoaderReExport;
|
|
62
110
|
|
|
63
111
|
const isAccountSection = parentDirs.some((d) => d.toLowerCase() === "account");
|
|
64
112
|
|
|
113
|
+
// For re-export barrels, also check the target component for device/url usage
|
|
114
|
+
const targetContent = readReExportTargetContent(content, file.absPath, ctx.sourceDir);
|
|
115
|
+
const combined = targetContent ? content + "\n" + targetContent : content;
|
|
116
|
+
|
|
117
|
+
const usesDevice = CTX_DEVICE_RE.test(combined) || DEVICE_PROP_RE_BROAD.test(combined);
|
|
118
|
+
const usesUrl = CTX_URL_RE.test(combined);
|
|
119
|
+
const usesMobile = IS_MOBILE_RE.test(combined) && !DEVICE_PROP_RE.test(combined);
|
|
120
|
+
|
|
65
121
|
const meta: SectionMeta = {
|
|
66
122
|
path: file.path,
|
|
67
123
|
hasLoader,
|
|
68
|
-
loaderIsAsync: hasLoader && ASYNC_RE.test(
|
|
69
|
-
hasLoadingFallback: LOADING_FALLBACK_RE.test(content),
|
|
124
|
+
loaderIsAsync: hasLoader && ASYNC_RE.test(combined),
|
|
125
|
+
hasLoadingFallback: LOADING_FALLBACK_RE.test(content) || (targetContent ? LOADING_FALLBACK_RE.test(targetContent) : false),
|
|
70
126
|
isHeader: HEADER_RE.test(basename) || HEADER_RE.test(dirName),
|
|
71
127
|
isFooter: FOOTER_RE.test(basename) || FOOTER_RE.test(dirName),
|
|
72
128
|
isTheme: THEME_RE.test(basename) || THEME_RE.test(dirName),
|
|
73
129
|
isListing: LISTING_RE.test(basename) || LISTING_RE.test(dirName),
|
|
74
130
|
hasTitle: JSDOC_TITLE_RE.test(content),
|
|
75
131
|
hasDescription: JSDOC_DESC_RE.test(content),
|
|
76
|
-
loaderUsesDevice:
|
|
77
|
-
loaderUsesUrl: hasLoader &&
|
|
132
|
+
loaderUsesDevice: usesDevice,
|
|
133
|
+
loaderUsesUrl: hasLoader && usesUrl,
|
|
78
134
|
isAccountSection,
|
|
79
135
|
isStatusOnly: hasLoader && isStatusOnlyLoader(content),
|
|
80
|
-
usesMobileBoolean:
|
|
136
|
+
usesMobileBoolean: usesMobile,
|
|
81
137
|
};
|
|
82
138
|
|
|
83
139
|
ctx.sectionMetas.push(meta);
|