@decocms/start 1.6.1 → 1.6.3

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.
Files changed (32) hide show
  1. package/.cursor/skills/deco-to-tanstack-migration/SKILL.md +85 -12
  2. package/.cursor/skills/deco-to-tanstack-migration/references/gotchas.md +98 -0
  3. package/.cursor/skills/deco-to-tanstack-migration/templates/package-json.md +45 -25
  4. package/.cursor/skills/deco-to-tanstack-migration/templates/root-route.md +56 -39
  5. package/.cursor/skills/deco-to-tanstack-migration/templates/setup-ts.md +122 -141
  6. package/package.json +1 -1
  7. package/scripts/generate-blocks.ts +8 -5
  8. package/scripts/migrate/analyzers/island-classifier.ts +23 -0
  9. package/scripts/migrate/analyzers/section-metadata.ts +63 -7
  10. package/scripts/migrate/phase-analyze.ts +136 -11
  11. package/scripts/migrate/phase-cleanup.ts +1057 -6
  12. package/scripts/migrate/phase-scaffold.ts +294 -5
  13. package/scripts/migrate/phase-transform.ts +14 -3
  14. package/scripts/migrate/templates/app-css.ts +149 -2
  15. package/scripts/migrate/templates/commerce-loaders.ts +173 -68
  16. package/scripts/migrate/templates/lib-utils.ts +255 -0
  17. package/scripts/migrate/templates/package-json.ts +30 -22
  18. package/scripts/migrate/templates/routes.ts +81 -11
  19. package/scripts/migrate/templates/section-loaders.ts +365 -32
  20. package/scripts/migrate/templates/server-entry.ts +350 -80
  21. package/scripts/migrate/templates/setup.ts +78 -8
  22. package/scripts/migrate/templates/types-gen.ts +58 -0
  23. package/scripts/migrate/templates/ui-components.ts +47 -16
  24. package/scripts/migrate/templates/vite-config.ts +17 -6
  25. package/scripts/migrate/templates/wrangler.ts +3 -1
  26. package/scripts/migrate/transforms/dead-code.ts +330 -4
  27. package/scripts/migrate/transforms/deno-isms.ts +19 -0
  28. package/scripts/migrate/transforms/imports.ts +93 -30
  29. package/scripts/migrate/transforms/jsx.ts +79 -4
  30. package/scripts/migrate/transforms/section-conventions.ts +105 -3
  31. package/scripts/migrate/types.ts +6 -0
  32. package/src/cms/resolve.ts +4 -0
@@ -1,167 +1,148 @@
1
1
  # setup.ts Template
2
2
 
3
- Annotated template based on espacosmart-storefront (100+ sections, VTEX, async rendering).
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
- // 1. CMS BLOCKS & META
8
- // ==========================================================================
9
-
10
- import blocksJson from "./server/cms/blocks.gen.ts";
11
- import metaData from "./server/admin/meta.gen.json";
12
- import { setBlocks } from "@decocms/start/cms/loader";
13
- import { setMetaData, setInvokeLoaders, setRenderShell } from "@decocms/start/admin";
14
- import { registerSections, registerSectionsSync, setResolvedComponent } from "@decocms/start/cms/registry";
15
- import { registerSectionLoaders, registerLayoutSections } from "@decocms/start/cms/sectionLoaders";
16
- import { registerCommerceLoaders, setAsyncRenderingConfig, onBeforeResolve } from "@decocms/start/cms/resolve";
17
- import { createCachedLoader } from "@decocms/start/sdk/cachedLoader";
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
- // Load CMS blocks (pages, sections, configs) from generated JSON
21
- setBlocks(blocksJson);
45
+ import "./setup/section-loaders";
22
46
 
23
- // Set admin schema for /live/_meta endpoint
24
- setMetaData(metaData);
25
-
26
- // Configure admin preview HTML shell
27
- setRenderShell({
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: ["https://fonts.googleapis.com/css2?family=YourFont:wght@400;500;600;700&display=swap"],
30
- theme: "light", // data-theme="light" on <html> for DaisyUI
31
- bodyClass: "bg-base-100 text-base-content",
32
- lang: "pt-BR",
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
- // 2. SECTION REGISTRATION
37
- // ==========================================================================
69
+ // -- Platform fetch instrumentation (optional, for observability) --
70
+ setShopifyFetch(createInstrumentedFetch("shopify"));
38
71
 
39
- // Critical sections above-the-fold, bundled synchronously
40
- import HeaderSection from "./components/header/Header";
41
- import FooterSection from "./sections/Footer/Footer";
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
- const criticalSections: Record<string, any> = {
44
- "site/sections/Header/Header.tsx": HeaderSection,
45
- "site/sections/Footer/Footer.tsx": FooterSection,
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
- // Register sync components for instant SSR (no Suspense boundary)
49
- for (const [key, mod] of Object.entries(criticalSections)) {
50
- setResolvedComponent(key, mod.default || mod);
51
- }
52
- registerSectionsSync(criticalSections);
53
-
54
- // All sections lazy-loaded via dynamic import
55
- registerSections({
56
- "site/sections/Header/Header.tsx": () => import("./sections/Header/Header"),
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
- import { vtexProductList } from "@decocms/apps/vtex/loaders/productList";
95
- import { vtexProductDetailsPage } from "@decocms/apps/vtex/loaders/productDetailsPage";
96
- import { vtexProductListingPage } from "@decocms/apps/vtex/loaders/productListingPage";
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
- // SWR-cached commerce loaders avoids re-fetching on every page navigation
101
- const cachedProductList = createCachedLoader("vtex/productList", vtexProductList, {
102
- policy: "stale-while-revalidate", maxAge: 60_000,
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
- // Map CMS __resolveType strings to actual loader functions
115
- registerCommerceLoaders({
116
- "vtex/loaders/intelligentSearch/productList.ts": cachedProductList,
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
- // 6. VTEX INITIALIZATION
126
- // ==========================================================================
115
+ export function LoadingFallback() {
116
+ return <div className="animate-pulse h-96 bg-base-200" />;
117
+ }
118
+ ```
127
119
 
128
- // onBeforeResolve runs once before the first CMS page resolution.
129
- // initVtexFromBlocks reads VTEX config (account, publicUrl) from CMS blocks.
130
- onBeforeResolve(() => {
131
- initVtexFromBlocks();
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
- setInvokeLoaders({
155
- "vtex/loaders/intelligentSearch/productList.ts": cachedProductList,
156
- "vtex/loaders/intelligentSearch/suggestions.ts": cachedSuggestions,
157
- // Used by the admin to preview loader results
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
- ## Key Patterns
143
+ ## See also
162
144
 
163
- 1. **Order matters**: blocks → meta → sections → loaders → commerce → async config
164
- 2. **Critical sections**: Import synchronously for instant SSR, also register as lazy for client code-splitting
165
- 3. **SWR caching**: `createCachedLoader` wraps commerce loaders with stale-while-revalidate
166
- 4. **onBeforeResolve**: Deferred initialization — VTEX config is read from CMS blocks at first request
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/start",
3
- "version": "1.6.1",
3
+ "version": "1.6.3",
4
4
  "type": "module",
5
5
  "description": "Deco framework for TanStack Start - CMS bridge, admin protocol, hooks, schema generation",
6
6
  "main": "./src/index.ts",
@@ -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: prefer the non-URL-encoded filename when both exist
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
- const isEncoded = file !== `${name}.json`;
70
- if (blockFiles[name] && !isEncoded) {
71
- // Plain filename wins over URL-encoded variant
72
- } else if (blockFiles[name] && isEncoded) {
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;
@@ -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 LOADING_FALLBACK_RE = /^export\s+(?:const|function)\s+LoadingFallback\b/m;
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 hasLoader = hasLoaderConst || hasLoaderFn;
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(content),
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: hasLoader && CTX_DEVICE_RE.test(content),
77
- loaderUsesUrl: hasLoader && CTX_URL_RE.test(content),
132
+ loaderUsesDevice: usesDevice,
133
+ loaderUsesUrl: hasLoader && usesUrl,
78
134
  isAccountSection,
79
135
  isStatusOnly: hasLoader && isStatusOnlyLoader(content),
80
- usesMobileBoolean: hasLoader && IS_MOBILE_RE.test(content) && !DEVICE_PROP_RE.test(content),
136
+ usesMobileBoolean: usesMobile,
81
137
  };
82
138
 
83
139
  ctx.sectionMetas.push(meta);