@decocms/start 1.2.5 → 1.2.7

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 (33) hide show
  1. package/package.json +1 -1
  2. package/scripts/deco-migrate-cli.ts +444 -0
  3. package/scripts/migrate/analyzers/island-classifier.ts +73 -0
  4. package/scripts/migrate/analyzers/loader-inventory.ts +63 -0
  5. package/scripts/migrate/analyzers/section-metadata.ts +91 -0
  6. package/scripts/migrate/analyzers/theme-extractor.ts +122 -0
  7. package/scripts/migrate/phase-analyze.ts +147 -17
  8. package/scripts/migrate/phase-cleanup.ts +124 -2
  9. package/scripts/migrate/phase-report.ts +44 -16
  10. package/scripts/migrate/phase-scaffold.ts +38 -132
  11. package/scripts/migrate/phase-transform.ts +28 -3
  12. package/scripts/migrate/phase-verify.ts +127 -5
  13. package/scripts/migrate/templates/app-css.ts +204 -0
  14. package/scripts/migrate/templates/cache-config.ts +26 -0
  15. package/scripts/migrate/templates/commerce-loaders.ts +124 -0
  16. package/scripts/migrate/templates/hooks.ts +358 -0
  17. package/scripts/migrate/templates/package-json.ts +29 -6
  18. package/scripts/migrate/templates/routes.ts +41 -136
  19. package/scripts/migrate/templates/sdk-gen.ts +59 -0
  20. package/scripts/migrate/templates/section-loaders.ts +108 -0
  21. package/scripts/migrate/templates/server-entry.ts +174 -67
  22. package/scripts/migrate/templates/setup.ts +64 -55
  23. package/scripts/migrate/templates/types-gen.ts +119 -0
  24. package/scripts/migrate/templates/ui-components.ts +113 -0
  25. package/scripts/migrate/templates/vite-config.ts +18 -1
  26. package/scripts/migrate/templates/wrangler.ts +4 -1
  27. package/scripts/migrate/transforms/dead-code.ts +23 -2
  28. package/scripts/migrate/transforms/imports.ts +40 -10
  29. package/scripts/migrate/transforms/jsx.ts +9 -0
  30. package/scripts/migrate/transforms/section-conventions.ts +83 -0
  31. package/scripts/migrate/types.ts +74 -0
  32. package/src/cms/resolve.ts +10 -0
  33. package/src/routes/cmsRoute.ts +13 -0
@@ -1,7 +1,6 @@
1
1
  import type { MigrationContext } from "../types.ts";
2
2
 
3
3
  export function generateWrangler(ctx: MigrationContext): string {
4
- // Sanitize site name for worker name (lowercase, hyphens only)
5
4
  const workerName = ctx.siteName
6
5
  .toLowerCase()
7
6
  .replace(/[^a-z0-9-]/g, "-")
@@ -14,6 +13,10 @@ export function generateWrangler(ctx: MigrationContext): string {
14
13
  "main": "./src/worker-entry.ts",
15
14
  "workers_dev": true,
16
15
  "preview_urls": true,
16
+ // Uncomment for redirect/AB testing support via KV:
17
+ // "kv_namespaces": [
18
+ // { "binding": "SITES_KV", "id": "YOUR_KV_NAMESPACE_ID" }
19
+ // ],
17
20
  "observability": {
18
21
  "logs": {
19
22
  "enabled": true,
@@ -62,12 +62,22 @@ export function transformDeadCode(content: string): TransformResult {
62
62
  let changed = false;
63
63
  let result = content;
64
64
 
65
- // Remove old cache export: export const cache = "stale-while-revalidate";
66
- if (/^export\s+const\s+cache\s*=\s*["'][^"']*["']/m.test(result)) {
65
+ // Remove old cache export: export const cache = "stale-while-revalidate" or { maxAge: ... }
66
+ if (/^export\s+const\s+cache\s*=/m.test(result)) {
67
+ // String form: export const cache = "stale-while-revalidate";
67
68
  result = result.replace(
68
69
  /^export\s+const\s+cache\s*=\s*["'][^"']*["'];?\s*\n?/gm,
69
70
  "",
70
71
  );
72
+ // Object form: export const cache = { maxAge: 60 * 10 };
73
+ result = result.replace(
74
+ /^export\s+const\s+cache\s*=\s*\{[^}]*\};?\s*\n?/gm,
75
+ "",
76
+ );
77
+ // Multiline object form (use brace-counting)
78
+ if (/^export\s+const\s+cache\s*=/m.test(result)) {
79
+ result = removeExportConstBlock(result, "cache");
80
+ }
71
81
  changed = true;
72
82
  notes.push("Removed dead `export const cache` (old caching system)");
73
83
  }
@@ -97,6 +107,17 @@ export function transformDeadCode(content: string): TransformResult {
97
107
  notes.push("MANUAL: crypto.subtle.digestSync is Deno-only — replaced with crypto.subtle.digest (needs await)");
98
108
  }
99
109
 
110
+ // Replace logger usage from @deco/deco/o11y with console
111
+ if (result.includes("logger.")) {
112
+ result = result.replace(/\blogger\.error\b/g, "console.error");
113
+ result = result.replace(/\blogger\.warn\b/g, "console.warn");
114
+ result = result.replace(/\blogger\.info\b/g, "console.info");
115
+ result = result.replace(/\blogger\.debug\b/g, "console.debug");
116
+ result = result.replace(/\blogger\.log\b/g, "console.log");
117
+ changed = true;
118
+ notes.push("Replaced logger.* with console.* (logger from @deco/deco/o11y removed)");
119
+ }
120
+
100
121
  // invoke.* calls are server RPC via runtime.ts proxy → keep as-is
101
122
  // The runtime.ts scaffolded file creates a proxy that routes to /deco/invoke/*
102
123
  // where the CMS config (API keys, tokens) is available server-side.
@@ -9,6 +9,7 @@ const IMPORT_RULES: Array<[RegExp, string | null]> = [
9
9
  // Fresh — remove entirely (handled by fresh-apis transform)
10
10
  [/^"\$fresh\/runtime\.ts"/, null],
11
11
  [/^"\$fresh\/server\.ts"/, null],
12
+ [/^"\$fresh\//, null], // catch-all for any $fresh/* import
12
13
 
13
14
  // Preact → React
14
15
  [/^"preact\/hooks"$/, `"react"`],
@@ -18,19 +19,25 @@ const IMPORT_RULES: Array<[RegExp, string | null]> = [
18
19
  [/^"@preact\/signals-core"$/, `"~/sdk/signal"`],
19
20
  [/^"@preact\/signals"$/, `"~/sdk/signal"`],
20
21
 
21
- // Deco framework
22
+ // Deco framework — hooks need splitting (useDevice, useScript, useSection)
22
23
  [/^"@deco\/deco\/hooks"$/, `"@decocms/start/sdk/useScript"`],
23
- [/^"@deco\/deco\/blocks"$/, `"@decocms/start/types"`],
24
+ [/^"@deco\/deco\/blocks"$/, `"~/types/deco"`],
25
+ [/^"@deco\/deco\/o11y"$/, null], // logger — use console.log/warn/error instead
24
26
  [/^"@deco\/deco\/web"$/, null], // runtime.ts is rewritten
25
- [/^"@deco\/deco"$/, `"@decocms/start"`],
27
+ [/^"@deco\/deco\/utils\/invoke\.types\.ts"$/, null],
28
+ [/^"@deco\/deco\/utils\/([^"]+)"$/, null],
29
+ [/^"@deco\/deco"$/, `"~/types/deco"`],
26
30
 
27
31
  // Apps — widgets & components
28
- [/^"apps\/admin\/widgets\.ts"$/, `"@decocms/start/types/widgets"`],
29
- [/^"apps\/website\/components\/Image\.tsx"$/, `"@decocms/apps/commerce/components/Image"`],
30
- [/^"apps\/website\/components\/Picture\.tsx"$/, `"@decocms/apps/commerce/components/Picture"`],
31
- [/^"apps\/website\/components\/Video\.tsx"$/, `"@decocms/apps/commerce/components/Video"`],
32
+ [/^"apps\/admin\/widgets\.ts"$/, `"~/types/widgets"`],
33
+ [/^"apps\/website\/components\/Image\.tsx"$/, `"~/components/ui/Image"`],
34
+ [/^"apps\/website\/components\/Picture\.tsx"$/, `"~/components/ui/Picture"`],
35
+ [/^"apps\/website\/components\/Video\.tsx"$/, `"~/components/ui/Video"`],
32
36
  [/^"apps\/website\/components\/Theme\.tsx"$/, `"~/components/ui/Theme"`],
37
+ [/^"apps\/website\/components\/([^"]+?)(?:\.tsx?)?"$/, `"~/components/ui/$1"`],
33
38
  [/^"apps\/commerce\/types\.ts"$/, `"@decocms/apps/commerce/types"`],
39
+ [/^"apps\/commerce\/mod\.ts"$/, `"~/types/commerce-app"`],
40
+ [/^"apps\/commerce\/types"$/, `"@decocms/apps/commerce/types"`],
34
41
 
35
42
  // Apps — VTEX (hooks, utils, actions, loaders, types)
36
43
  [/^"apps\/vtex\/hooks\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/vtex/hooks/$1"`],
@@ -65,15 +72,29 @@ const IMPORT_RULES: Array<[RegExp, string | null]> = [
65
72
  [/^"site\/sdk\/useVariantPossiblities(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/useVariantPossibilities"`],
66
73
  [/^"site\/sdk\/usePlatform(?:\.tsx?)?.*"$/, null],
67
74
 
75
+ // $store/account.json → ~/account.json (JSON import with assertion)
76
+ [/^"\$store\/account\.json"$/, `"~/account.json"`],
77
+ [/^"site\/account\.json"$/, `"~/account.json"`],
78
+
68
79
  // $store/ → ~/ (common Deno import map alias for project root)
69
80
  [/^"\$store\/sdk\/clx(?:\.tsx?)?.*"$/, `"~/sdk/clx"`],
70
81
  [/^"\$store\/sdk\/useId(?:\.tsx?)?.*"$/, `"react"`],
71
82
  [/^"\$store\/sdk\/useOffer(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/useOffer"`],
83
+ [/^"\$store\/sdk\/format(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/formatPrice"`],
72
84
  [/^"\$store\/sdk\/useVariantPossiblities(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/useVariantPossibilities"`],
73
85
  [/^"\$store\/sdk\/usePlatform(?:\.tsx?)?.*"$/, null],
74
86
  [/^"\$store\/(.+)"$/, `"~/$1"`],
75
87
 
88
+ // $home/ → ~/ (another common alias)
89
+ [/^"\$home\/(.+)"$/, `"~/$1"`],
90
+
76
91
  // site/ → ~/
92
+ [/^"site\/sdk\/clx(?:\.tsx?)?.*"$/, `"~/sdk/clx"`],
93
+ [/^"site\/sdk\/useId(?:\.tsx?)?.*"$/, `"react"`],
94
+ [/^"site\/sdk\/useOffer(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/useOffer"`],
95
+ [/^"site\/sdk\/format(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/formatPrice"`],
96
+ [/^"site\/sdk\/useVariantPossiblities(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/useVariantPossibilities"`],
97
+ [/^"site\/sdk\/usePlatform(?:\.tsx?)?.*"$/, null],
77
98
  [/^"site\/(.+)"$/, `"~/$1"`],
78
99
  ];
79
100
 
@@ -89,6 +110,8 @@ const RELATIVE_SDK_REWRITES: Array<[RegExp, string]> = [
89
110
  [/(?:\.\.\/)*sdk\/useId(?:\.tsx?)?$/, "react"],
90
111
  // sdk/useOffer → @decocms/apps/commerce/sdk/useOffer
91
112
  [/(?:\.\.\/)*sdk\/useOffer(?:\.tsx?)?$/, "@decocms/apps/commerce/sdk/useOffer"],
113
+ // sdk/format → @decocms/apps/commerce/sdk/formatPrice
114
+ [/(?:\.\.\/)*sdk\/format(?:\.tsx?)?$/, "@decocms/apps/commerce/sdk/formatPrice"],
92
115
  // sdk/useVariantPossiblities → @decocms/apps/commerce/sdk/useVariantPossibilities
93
116
  [/(?:\.\.\/)*sdk\/useVariantPossiblities(?:\.tsx?)?$/, "@decocms/apps/commerce/sdk/useVariantPossibilities"],
94
117
  // sdk/usePlatform → remove entirely
@@ -113,12 +136,19 @@ export function transformImports(content: string): TransformResult {
113
136
  const notes: string[] = [];
114
137
  let changed = false;
115
138
 
139
+ // Strip BOM that prevents ^ matching on the first line
140
+ if (content.charCodeAt(0) === 0xfeff) {
141
+ content = content.slice(1);
142
+ changed = true;
143
+ }
144
+
116
145
  // Match import/export lines with their specifiers
146
+ // The suffix group also captures import assertions (with { type: "json" }) and assert syntax
117
147
  const importLineRegex =
118
- /^(import\s+(?:type\s+)?(?:\{[^}]*\}|[\w*]+(?:\s*,\s*\{[^}]*\})?)\s+from\s+)("[^"]+"|'[^']+')(;?\s*)$/gm;
148
+ /^(import\s+(?:type\s+)?(?:\{[^}]*\}|[\w*]+(?:\s*,\s*\{[^}]*\})?)\s+from\s+)("[^"]+"|'[^']+')((?:\s+(?:with|assert)\s+\{[^}]*\})?;?\s*)$/gm;
119
149
  const reExportLineRegex =
120
- /^(export\s+(?:type\s+)?\{[^}]*\}\s+from\s+)("[^"]+"|'[^']+')(;?\s*)$/gm;
121
- const sideEffectImportRegex = /^(import\s+)("[^"]+"|'[^']+')(;?\s*)$/gm;
150
+ /^(export\s+(?:type\s+)?\{[^}]*\}\s+from\s+)("[^"]+"|'[^']+')((?:\s+(?:with|assert)\s+\{[^}]*\})?;?\s*)$/gm;
151
+ const sideEffectImportRegex = /^(import\s+)("[^"]+"|'[^']+')((?:\s+(?:with|assert)\s+\{[^}]*\})?;?\s*)$/gm;
122
152
 
123
153
  /**
124
154
  * Post-process: split @deco/deco/hooks imports.
@@ -220,6 +220,15 @@ export function transformJsx(content: string): TransformResult {
220
220
  notes.push("Prefixed setTimeout/setInterval with window. for correct typing");
221
221
  }
222
222
 
223
+ // Strip data-fresh-disable-lock (Fresh-specific, not needed in React/TanStack)
224
+ if (result.includes("data-fresh-disable-lock")) {
225
+ result = result.replace(/\s*data-fresh-disable-lock=\{[^}]*\}/g, "");
226
+ result = result.replace(/\s*data-fresh-disable-lock="[^"]*"/g, "");
227
+ result = result.replace(/\s*data-fresh-disable-lock/g, "");
228
+ changed = true;
229
+ notes.push("Removed data-fresh-disable-lock attribute (Fresh-specific)");
230
+ }
231
+
223
232
  // Ensure React import exists if we introduced React.* references
224
233
  if (
225
234
  (result.includes("React.") || result.includes("React,")) &&
@@ -0,0 +1,83 @@
1
+ import type { TransformResult, SectionMeta } from "../types.ts";
2
+
3
+ /**
4
+ * Adds section convention exports (sync, eager, layout, cache)
5
+ * to section files based on metadata extracted during analysis.
6
+ *
7
+ * These exports are read by generate-sections.ts in @decocms/start
8
+ * to build the sections.gen.ts registry.
9
+ */
10
+ export function transformSectionConventions(
11
+ content: string,
12
+ sectionMeta: SectionMeta | undefined,
13
+ ): TransformResult {
14
+ if (!sectionMeta) {
15
+ return { content, changed: false, notes: [] };
16
+ }
17
+
18
+ const notes: string[] = [];
19
+ let result = content;
20
+ let changed = false;
21
+
22
+ // Header, footer, theme → eager + sync + layout
23
+ if (sectionMeta.isHeader || sectionMeta.isFooter || sectionMeta.isTheme) {
24
+ if (!result.includes("export const eager")) {
25
+ result += "\nexport const eager = true;\n";
26
+ notes.push("Added: export const eager = true");
27
+ changed = true;
28
+ }
29
+ if (!result.includes("export const sync")) {
30
+ result += "export const sync = true;\n";
31
+ notes.push("Added: export const sync = true");
32
+ changed = true;
33
+ }
34
+ if (!result.includes("export const layout")) {
35
+ result += "export const layout = true;\n";
36
+ notes.push("Added: export const layout = true");
37
+ changed = true;
38
+ }
39
+ }
40
+
41
+ // Listing sections → cache = "listing"
42
+ if (sectionMeta.isListing && !result.includes("export const cache")) {
43
+ result += '\nexport const cache = "listing";\n';
44
+ notes.push('Added: export const cache = "listing"');
45
+ changed = true;
46
+ }
47
+
48
+ // Sections with loaders that use device → add sync (needs SSR device detection)
49
+ if (sectionMeta.hasLoader && sectionMeta.loaderUsesDevice && !result.includes("export const sync")) {
50
+ result += "\nexport const sync = true;\n";
51
+ notes.push("Added: export const sync = true (loader uses device)");
52
+ changed = true;
53
+ }
54
+
55
+ // Generate a basic LoadingFallback if the section doesn't have one
56
+ // and it's a listing section (visible skeleton improvement)
57
+ if (sectionMeta.isListing && !sectionMeta.hasLoadingFallback) {
58
+ result += `
59
+ export function LoadingFallback() {
60
+ return (
61
+ <div className="w-full py-8">
62
+ <div className="container mx-auto px-4">
63
+ <div className="h-6 w-48 bg-base-200 animate-pulse rounded mb-4" />
64
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
65
+ {Array.from({ length: 4 }).map((_, i) => (
66
+ <div key={i} className="flex flex-col gap-2">
67
+ <div className="aspect-square bg-base-200 animate-pulse rounded" />
68
+ <div className="h-4 bg-base-200 animate-pulse rounded w-3/4" />
69
+ <div className="h-4 bg-base-200 animate-pulse rounded w-1/2" />
70
+ </div>
71
+ ))}
72
+ </div>
73
+ </div>
74
+ </div>
75
+ );
76
+ }
77
+ `;
78
+ notes.push("Added: LoadingFallback skeleton for listing section");
79
+ changed = true;
80
+ }
81
+
82
+ return { content: result, changed, notes };
83
+ }
@@ -63,6 +63,70 @@ export type DetectedPattern =
63
63
  | "define-app"
64
64
  | "invoke-proxy";
65
65
 
66
+ /** Metadata extracted from a section file during analysis */
67
+ export interface SectionMeta {
68
+ /** Relative path from source root (e.g. "sections/Header/Header.tsx") */
69
+ path: string;
70
+ /** Has export const loader or export function loader */
71
+ hasLoader: boolean;
72
+ /** Loader is async */
73
+ loaderIsAsync: boolean;
74
+ /** Has export function LoadingFallback */
75
+ hasLoadingFallback: boolean;
76
+ /** Is a header section (by filename) */
77
+ isHeader: boolean;
78
+ /** Is a footer section (by filename) */
79
+ isFooter: boolean;
80
+ /** Is a theme section (by filename) */
81
+ isTheme: boolean;
82
+ /** Is a shelf/carousel/listing section (by filename or content) */
83
+ isListing: boolean;
84
+ /** Has JSDoc @title */
85
+ hasTitle: boolean;
86
+ /** Has JSDoc @description */
87
+ hasDescription: boolean;
88
+ /** Loader uses ctx.device or similar device detection */
89
+ loaderUsesDevice: boolean;
90
+ /** Loader uses request URL / search params */
91
+ loaderUsesUrl: boolean;
92
+ /** Loader is an Account section (sections/Account/*) */
93
+ isAccountSection: boolean;
94
+ /** Loader only sets ctx.response.status (no real prop enrichment) */
95
+ isStatusOnly: boolean;
96
+ /** Loader sets isMobile (boolean) rather than device (string) */
97
+ usesMobileBoolean: boolean;
98
+ }
99
+
100
+ /** Classification of an island file */
101
+ export interface IslandClassification {
102
+ /** Relative path from source root */
103
+ path: string;
104
+ /** "wrapper" = thin re-export/bridge, "standalone" = has real logic */
105
+ type: "wrapper" | "standalone";
106
+ /** If wrapper, the target component path */
107
+ wrapsComponent?: string;
108
+ /** If standalone, the suggested target path under src/ */
109
+ suggestedTarget: string;
110
+ /** Line count (used as heuristic) */
111
+ lineCount: number;
112
+ }
113
+
114
+ /** Information about a loader file */
115
+ export interface LoaderInfo {
116
+ /** Relative path from source root */
117
+ path: string;
118
+ /** Has export const cache (SWR) */
119
+ hasCache: boolean;
120
+ /** Has export const cacheKey */
121
+ hasCacheKey: boolean;
122
+ /** Maps to a known @decocms/apps equivalent */
123
+ appsEquivalent: string | null;
124
+ /** Is a custom loader that needs dynamic import in commerce-loaders */
125
+ isCustom: boolean;
126
+ /** Detected platform relevance (vtex, shopify, etc.) */
127
+ platformRelevance: Platform | null;
128
+ }
129
+
66
130
  export interface MigrationContext {
67
131
  sourceDir: string;
68
132
  siteName: string;
@@ -83,6 +147,13 @@ export interface MigrationContext {
83
147
  /** All categorized source files */
84
148
  files: FileRecord[];
85
149
 
150
+ /** Section metadata extracted during analysis */
151
+ sectionMetas: SectionMeta[];
152
+ /** Island classifications */
153
+ islandClassifications: IslandClassification[];
154
+ /** Loader inventory */
155
+ loaderInventory: LoaderInfo[];
156
+
86
157
  /** Files created by scaffold phase */
87
158
  scaffoldedFiles: string[];
88
159
  /** Files transformed */
@@ -126,6 +197,9 @@ export function createContext(
126
197
  themeColors: {},
127
198
  fontFamily: null,
128
199
  files: [],
200
+ sectionMetas: [],
201
+ islandClassifications: [],
202
+ loaderInventory: [],
129
203
  scaffoldedFiles: [],
130
204
  transformedFiles: [],
131
205
  deletedFiles: [],
@@ -627,7 +627,17 @@ async function resolveRawSection(
627
627
  section: unknown,
628
628
  rctx: ResolveContext,
629
629
  ): Promise<ResolvedSection[]> {
630
+ const sectionRt = (section as any)?.__resolveType;
631
+ if (String(sectionRt).includes("eader")) {
632
+ console.log(`[RAW-SECTION-ENTER] sectionRt="${sectionRt}"`);
633
+ }
634
+
630
635
  const resolved = await internalResolve(section, rctx);
636
+
637
+ if (String(sectionRt).includes("eader")) {
638
+ console.log(`[RAW-SECTION-RESOLVED] resolved type=${typeof resolved} isObj=${resolved && typeof resolved === 'object'} keys=${resolved && typeof resolved === 'object' ? Object.keys(resolved as any).join(',') : 'N/A'}`);
639
+ }
640
+
631
641
  if (!resolved || typeof resolved !== "object") return [];
632
642
 
633
643
  const items = Array.isArray(resolved) ? resolved : [resolved];
@@ -99,8 +99,21 @@ async function loadCmsPageInternal(fullPath: string) {
99
99
  const request = new Request(urlWithSearch, {
100
100
  headers: originRequest.headers,
101
101
  });
102
+
103
+ for (const s of page.resolvedSections) {
104
+ if (s.component?.includes("eader")) {
105
+ console.log(`[CMS-ROUTE] BEFORE loaders: "${s.component}" propKeys=[${Object.keys(s.props || {}).join(",")}]`);
106
+ }
107
+ }
108
+
102
109
  const enrichedSections = await runSectionLoaders(page.resolvedSections, request);
103
110
 
111
+ for (const s of enrichedSections) {
112
+ if (s.component?.includes("eader")) {
113
+ console.log(`[CMS-ROUTE] AFTER loaders: "${s.component}" propKeys=[${Object.keys(s.props || {}).join(",")}]`);
114
+ }
115
+ }
116
+
104
117
  // Pre-import eager section modules so their default exports are cached
105
118
  // in resolvedComponents. This ensures SSR renders with direct component
106
119
  // refs, and the client hydration can skip React.lazy/Suspense.