@decocms/blocks-cli 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +44 -0
- package/scripts/analyze-traces.mjs +1117 -0
- package/scripts/audit-observability-config.test.ts +446 -0
- package/scripts/audit-observability-config.ts +511 -0
- package/scripts/deco-migrate-cli.ts +444 -0
- package/scripts/fast-deploy-kv.test.ts +131 -0
- package/scripts/generate-blocks.test.ts +94 -0
- package/scripts/generate-blocks.ts +274 -0
- package/scripts/generate-invoke.test.ts +195 -0
- package/scripts/generate-invoke.ts +469 -0
- package/scripts/generate-loaders.ts +217 -0
- package/scripts/generate-schema.ts +1287 -0
- package/scripts/generate-sections.ts +237 -0
- package/scripts/htmx-analyze.ts +226 -0
- package/scripts/lib/blocks-dedupe.test.ts +179 -0
- package/scripts/lib/blocks-dedupe.ts +142 -0
- package/scripts/lib/cf-kv-rest.ts +78 -0
- package/scripts/lib/jsonc.ts +122 -0
- package/scripts/lib/kv-snapshot.ts +51 -0
- package/scripts/lib/read-decofile.ts +70 -0
- package/scripts/lib/sync-helpers.ts +44 -0
- package/scripts/migrate/analyzers/htmx-analyze.test.ts +372 -0
- package/scripts/migrate/analyzers/htmx-analyze.ts +425 -0
- package/scripts/migrate/analyzers/island-classifier.ts +96 -0
- package/scripts/migrate/analyzers/loader-inventory.ts +63 -0
- package/scripts/migrate/analyzers/section-metadata.ts +147 -0
- package/scripts/migrate/analyzers/theme-extractor.ts +122 -0
- package/scripts/migrate/colors.ts +46 -0
- package/scripts/migrate/config.test.ts +202 -0
- package/scripts/migrate/config.ts +186 -0
- package/scripts/migrate/phase-analyze.test.ts +63 -0
- package/scripts/migrate/phase-analyze.ts +782 -0
- package/scripts/migrate/phase-cleanup-audit.test.ts +137 -0
- package/scripts/migrate/phase-cleanup-audit.ts +105 -0
- package/scripts/migrate/phase-cleanup.test.ts +141 -0
- package/scripts/migrate/phase-cleanup.ts +1588 -0
- package/scripts/migrate/phase-compile.test.ts +193 -0
- package/scripts/migrate/phase-compile.ts +177 -0
- package/scripts/migrate/phase-report.ts +243 -0
- package/scripts/migrate/phase-scaffold.ts +593 -0
- package/scripts/migrate/phase-transform.ts +310 -0
- package/scripts/migrate/phase-verify.test.ts +127 -0
- package/scripts/migrate/phase-verify.ts +572 -0
- package/scripts/migrate/post-cleanup/rules.ts +1708 -0
- package/scripts/migrate/post-cleanup/runner.test.ts +1771 -0
- package/scripts/migrate/post-cleanup/runner.ts +137 -0
- package/scripts/migrate/post-cleanup/shim-classify.test.ts +352 -0
- package/scripts/migrate/post-cleanup/shim-classify.ts +246 -0
- package/scripts/migrate/post-cleanup/types.ts +106 -0
- package/scripts/migrate/source-layout.test.ts +111 -0
- package/scripts/migrate/source-layout.ts +103 -0
- package/scripts/migrate/templates/app-css.ts +366 -0
- package/scripts/migrate/templates/cache-config.ts +26 -0
- package/scripts/migrate/templates/commerce-loaders.ts +230 -0
- package/scripts/migrate/templates/cursor-rules.test.ts +59 -0
- package/scripts/migrate/templates/cursor-rules.ts +70 -0
- package/scripts/migrate/templates/hooks.test.ts +141 -0
- package/scripts/migrate/templates/hooks.ts +152 -0
- package/scripts/migrate/templates/knip-config.ts +27 -0
- package/scripts/migrate/templates/lib-utils.test.ts +139 -0
- package/scripts/migrate/templates/lib-utils.ts +326 -0
- package/scripts/migrate/templates/lockfile-check-yml.test.ts +26 -0
- package/scripts/migrate/templates/lockfile-check-yml.ts +66 -0
- package/scripts/migrate/templates/package-json.ts +177 -0
- package/scripts/migrate/templates/routes.ts +237 -0
- package/scripts/migrate/templates/sdk-gen.ts +59 -0
- package/scripts/migrate/templates/section-loaders.ts +456 -0
- package/scripts/migrate/templates/server-entry.ts +561 -0
- package/scripts/migrate/templates/setup.ts +148 -0
- package/scripts/migrate/templates/tsconfig.ts +21 -0
- package/scripts/migrate/templates/types-gen.ts +174 -0
- package/scripts/migrate/templates/ui-components.ts +144 -0
- package/scripts/migrate/templates/vite-config.ts +101 -0
- package/scripts/migrate/transforms/dead-code.ts +455 -0
- package/scripts/migrate/transforms/deno-isms.ts +85 -0
- package/scripts/migrate/transforms/fresh-apis.ts +223 -0
- package/scripts/migrate/transforms/htmx-on-events.test.ts +305 -0
- package/scripts/migrate/transforms/htmx-on-events.ts +193 -0
- package/scripts/migrate/transforms/imports.ts +385 -0
- package/scripts/migrate/transforms/jsx.ts +317 -0
- package/scripts/migrate/transforms/section-conventions.ts +210 -0
- package/scripts/migrate/transforms/tailwind.ts +739 -0
- package/scripts/migrate/types.ts +244 -0
- package/scripts/migrate-blocks-to-kv.ts +104 -0
- package/scripts/migrate-post-cleanup.ts +191 -0
- package/scripts/migrate-to-cf-observability.test.ts +215 -0
- package/scripts/migrate-to-cf-observability.ts +699 -0
- package/scripts/migrate.ts +282 -0
- package/scripts/smoke-otlp-errorlog.ts +46 -0
- package/scripts/smoke-otlp-meter.ts +48 -0
- package/scripts/sync-blocks-to-kv.ts +153 -0
- package/scripts/tailwind-lint.ts +518 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import type { TransformResult } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convert JSX attribute syntax to object literal entries.
|
|
5
|
+
* e.g. `page={page}` → `page`, `foo={bar.baz}` → `foo: bar.baz`, `label="hi"` → `label: "hi"`
|
|
6
|
+
*/
|
|
7
|
+
function jsxAttrsToObjectEntries(jsxAttrs: string): string {
|
|
8
|
+
return jsxAttrs
|
|
9
|
+
.replace(/(\w+)=\{(\1)\}/g, "$1") // shorthand: name={name} → name
|
|
10
|
+
.replace(/(\w+)=\{([^}]+)\}/g, "$1: $2") // name={expr} → name: expr
|
|
11
|
+
.replace(/(\w+)="([^"]*)"/g, '$1: "$2"') // name="str" → name: "str"
|
|
12
|
+
.replace(/(\w+)='([^']*)'/g, "$1: '$2'"); // name='str' → name: 'str'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Transforms Preact JSX patterns to React JSX patterns.
|
|
17
|
+
*
|
|
18
|
+
* - class= → className= (in JSX context)
|
|
19
|
+
* - onInput= → onChange= (React's onChange fires on every keystroke)
|
|
20
|
+
* - ComponentChildren → React.ReactNode
|
|
21
|
+
* - JSX.SVGAttributes → React.SVGAttributes
|
|
22
|
+
* - JSX.GenericEventHandler → React.FormEventHandler / React.EventHandler
|
|
23
|
+
* - type { JSX } from "preact" → (removed, use React types)
|
|
24
|
+
*/
|
|
25
|
+
export function transformJsx(content: string): TransformResult {
|
|
26
|
+
const notes: string[] = [];
|
|
27
|
+
let changed = false;
|
|
28
|
+
let result = content;
|
|
29
|
+
|
|
30
|
+
// class= → className= in JSX attributes
|
|
31
|
+
// Match class= that's preceded by whitespace and inside a JSX tag
|
|
32
|
+
if (/(<[a-zA-Z][^>]*?\s)class(\s*=)/.test(result)) {
|
|
33
|
+
result = result.replace(
|
|
34
|
+
/(<[a-zA-Z][^>]*?\s)class(\s*=)/g,
|
|
35
|
+
"$1className$2",
|
|
36
|
+
);
|
|
37
|
+
changed = true;
|
|
38
|
+
notes.push("Replaced class= with className=");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Also handle class= at the start of a line in JSX (multi-line attributes)
|
|
42
|
+
if (/^(\s+)class(\s*=)/m.test(result)) {
|
|
43
|
+
result = result.replace(/^(\s+)class(\s*=)/gm, "$1className$2");
|
|
44
|
+
changed = true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// onInput= → onChange= (React onChange fires on every keystroke, like Preact onInput)
|
|
48
|
+
if (result.includes("onInput=")) {
|
|
49
|
+
if (result.includes("onChange=") && result.includes("onInput=")) {
|
|
50
|
+
// Both exist — remove onInput blocks to avoid duplicate JSX attributes.
|
|
51
|
+
// onInput is redundant in React since onChange already fires on every keystroke.
|
|
52
|
+
// Match onInput={...handler...} including multi-line arrow functions
|
|
53
|
+
result = result.replace(
|
|
54
|
+
/\s*onInput=\{[^{}]*(?:\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}[^{}]*)*\}/g,
|
|
55
|
+
""
|
|
56
|
+
);
|
|
57
|
+
changed = true;
|
|
58
|
+
notes.push("Removed onInput= (redundant with existing onChange= in React)");
|
|
59
|
+
} else {
|
|
60
|
+
result = result.replace(/onInput=/g, "onChange=");
|
|
61
|
+
changed = true;
|
|
62
|
+
notes.push("Replaced onInput= with onChange=");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// for= → htmlFor= in JSX (label elements)
|
|
67
|
+
if (/(<(?:label|Label)[^>]*?\s)for(\s*=)/.test(result)) {
|
|
68
|
+
result = result.replace(
|
|
69
|
+
/(<(?:label|Label)[^>]*?\s)for(\s*=)/g,
|
|
70
|
+
"$1htmlFor$2",
|
|
71
|
+
);
|
|
72
|
+
changed = true;
|
|
73
|
+
notes.push("Replaced for= with htmlFor= on label elements");
|
|
74
|
+
}
|
|
75
|
+
// Also handle for= at the start of a line in multi-line JSX attributes
|
|
76
|
+
if (/^\s+for\s*=\s*\{/m.test(result)) {
|
|
77
|
+
result = result.replace(/^(\s+)for(\s*=\s*\{)/gm, "$1htmlFor$2");
|
|
78
|
+
changed = true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ComponentChildren → ReactNode (named import, not React.ReactNode)
|
|
82
|
+
if (result.includes("ComponentChildren")) {
|
|
83
|
+
result = result.replace(/\bComponentChildren\b/g, "ReactNode");
|
|
84
|
+
// Add ReactNode import if not already imported
|
|
85
|
+
if (
|
|
86
|
+
!result.match(/\bReactNode\b.*from\s+["']react["']/) &&
|
|
87
|
+
!result.match(/from\s+["']react["'].*\bReactNode\b/)
|
|
88
|
+
) {
|
|
89
|
+
// Check if there's already a react import we can extend
|
|
90
|
+
const reactImportMatch = result.match(
|
|
91
|
+
/^(import\s+(?:type\s+)?\{)([^}]*?)(\}\s+from\s+["']react["'];?)$/m,
|
|
92
|
+
);
|
|
93
|
+
if (reactImportMatch) {
|
|
94
|
+
const [fullMatch, prefix, existing, suffix] = reactImportMatch;
|
|
95
|
+
const items = existing.trim();
|
|
96
|
+
result = result.replace(
|
|
97
|
+
fullMatch,
|
|
98
|
+
`${prefix}${items ? `${items}, ` : ""}type ReactNode${suffix}`,
|
|
99
|
+
);
|
|
100
|
+
} else {
|
|
101
|
+
result = `import type { ReactNode } from "react";\n${result}`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
changed = true;
|
|
105
|
+
notes.push("Replaced ComponentChildren with ReactNode");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// JSX.SVGAttributes<SVGSVGElement> → React.SVGAttributes<SVGSVGElement>
|
|
109
|
+
if (result.includes("JSX.SVGAttributes")) {
|
|
110
|
+
result = result.replace(/\bJSX\.SVGAttributes/g, "React.SVGAttributes");
|
|
111
|
+
changed = true;
|
|
112
|
+
notes.push("Replaced JSX.SVGAttributes with React.SVGAttributes");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// JSX.GenericEventHandler<X> → React.FormEventHandler<X>
|
|
116
|
+
if (result.includes("JSX.GenericEventHandler")) {
|
|
117
|
+
result = result.replace(
|
|
118
|
+
/\bJSX\.GenericEventHandler/g,
|
|
119
|
+
"React.FormEventHandler",
|
|
120
|
+
);
|
|
121
|
+
changed = true;
|
|
122
|
+
notes.push(
|
|
123
|
+
"Replaced JSX.GenericEventHandler with React.FormEventHandler",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// JSX.HTMLAttributes<X> → React.HTMLAttributes<X>
|
|
128
|
+
if (result.includes("JSX.HTMLAttributes")) {
|
|
129
|
+
result = result.replace(
|
|
130
|
+
/\bJSX\.HTMLAttributes/g,
|
|
131
|
+
"React.HTMLAttributes",
|
|
132
|
+
);
|
|
133
|
+
changed = true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// JSX.IntrinsicElements → React.JSX.IntrinsicElements
|
|
137
|
+
if (result.includes("JSX.IntrinsicElements")) {
|
|
138
|
+
result = result.replace(
|
|
139
|
+
/\bJSX\.IntrinsicElements/g,
|
|
140
|
+
"React.JSX.IntrinsicElements",
|
|
141
|
+
);
|
|
142
|
+
changed = true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Remove standalone "import type { JSX } from 'preact'" if JSX no longer used
|
|
146
|
+
// (it was already removed by imports transform, but double check)
|
|
147
|
+
result = result.replace(
|
|
148
|
+
/^import\s+type\s+\{\s*JSX\s*\}\s+from\s+["']preact["'];?\s*\n?/gm,
|
|
149
|
+
"",
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// tabindex → tabIndex in JSX
|
|
153
|
+
if (/\btabindex\s*=/.test(result)) {
|
|
154
|
+
result = result.replace(/\btabindex(\s*=)/g, "tabIndex$1");
|
|
155
|
+
changed = true;
|
|
156
|
+
notes.push("Replaced tabindex with tabIndex");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// frameBorder → frameBorder (already camelCase, but just in case)
|
|
160
|
+
// referrerpolicy → referrerPolicy
|
|
161
|
+
if (result.includes("referrerpolicy=")) {
|
|
162
|
+
result = result.replace(/referrerpolicy=/g, "referrerPolicy=");
|
|
163
|
+
changed = true;
|
|
164
|
+
notes.push("Replaced referrerpolicy with referrerPolicy");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// allowFullScreen={true} is fine in React, but allowfullscreen is not
|
|
168
|
+
if (result.includes("allowfullscreen")) {
|
|
169
|
+
result = result.replace(/\ballowfullscreen\b/g, "allowFullScreen");
|
|
170
|
+
changed = true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// `class` as a prop name in destructuring patterns → `className`
|
|
174
|
+
// Matches: { class: someVar } or { class, } or { ..., class: x } in function params
|
|
175
|
+
if (/[{,]\s*class\s*[,}:]/.test(result)) {
|
|
176
|
+
// class: varName → className: varName (anywhere in destructuring)
|
|
177
|
+
result = result.replace(
|
|
178
|
+
/([{,]\s*)class(\s*:\s*\w+)/g,
|
|
179
|
+
"$1className$2",
|
|
180
|
+
);
|
|
181
|
+
// class, → className, (shorthand, anywhere in destructuring)
|
|
182
|
+
result = result.replace(
|
|
183
|
+
/([{,]\s*)class(\s*[,}])/g,
|
|
184
|
+
"$1className$2",
|
|
185
|
+
);
|
|
186
|
+
changed = true;
|
|
187
|
+
notes.push("Replaced 'class' prop in destructuring with 'className'");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// `class` in interface/type definitions → className
|
|
191
|
+
// Matches: class?: string; or class: string;
|
|
192
|
+
if (/^\s+class\??\s*:/m.test(result)) {
|
|
193
|
+
result = result.replace(/^(\s+)class(\??\s*:)/gm, "$1className$2");
|
|
194
|
+
changed = true;
|
|
195
|
+
notes.push("Replaced 'class' in interface definitions with 'className'");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Remove `alt` prop from non-img elements (<a>, <iframe>, <div>, etc.)
|
|
199
|
+
// In React, `alt` is only valid on <img>, <input type="image">, <area>
|
|
200
|
+
const altOnNonImgRegex = /(<(?:a|iframe|div|span|button|section)\s[^>]*?)\s+alt=(?:\{[^}]*\}|"[^"]*"|'[^']*')/g;
|
|
201
|
+
if (altOnNonImgRegex.test(result)) {
|
|
202
|
+
result = result.replace(
|
|
203
|
+
/(<(?:a|iframe|div|span|button|section)\s[^>]*?)\s+alt=(?:\{[^}]*\}|"[^"]*"|'[^']*')/g,
|
|
204
|
+
"$1",
|
|
205
|
+
);
|
|
206
|
+
changed = true;
|
|
207
|
+
notes.push("Removed invalid alt prop from non-img elements");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Remove `type` prop from non-form elements (<span>, <div>, etc.)
|
|
211
|
+
// In React, `type` is only valid on <input>, <button>, <select>, <textarea>, <script>, <style>, <link>
|
|
212
|
+
const typeOnInvalidRegex = /(<(?:span|div|p|section|header|footer|nav|main|article|aside)\s[^>]*?)\s+type=(?:\{[^}]*\}|"[^"]*"|'[^']*')/g;
|
|
213
|
+
if (typeOnInvalidRegex.test(result)) {
|
|
214
|
+
result = result.replace(
|
|
215
|
+
/(<(?:span|div|p|section|header|footer|nav|main|article|aside)\s[^>]*?)\s+type=(?:\{[^}]*\}|"[^"]*"|'[^']*')/g,
|
|
216
|
+
"$1",
|
|
217
|
+
);
|
|
218
|
+
changed = true;
|
|
219
|
+
notes.push("Removed invalid type prop from non-form elements");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// setTimeout/setInterval return type: use window.setTimeout for correct typing
|
|
223
|
+
// In Node/CF Workers, setTimeout returns Timeout object, not number
|
|
224
|
+
// window.setTimeout always returns number
|
|
225
|
+
if (/\bsetTimeout\b/.test(result) && /:\s*number/.test(result)) {
|
|
226
|
+
// Only replace bare setTimeout when it's assigned to a typed variable
|
|
227
|
+
result = result.replace(
|
|
228
|
+
/\b(?<!window\.)setTimeout\(/g,
|
|
229
|
+
"window.setTimeout(",
|
|
230
|
+
);
|
|
231
|
+
result = result.replace(
|
|
232
|
+
/\b(?<!window\.)setInterval\(/g,
|
|
233
|
+
"window.setInterval(",
|
|
234
|
+
);
|
|
235
|
+
result = result.replace(
|
|
236
|
+
/\b(?<!window\.)clearTimeout\(/g,
|
|
237
|
+
"window.clearTimeout(",
|
|
238
|
+
);
|
|
239
|
+
result = result.replace(
|
|
240
|
+
/\b(?<!window\.)clearInterval\(/g,
|
|
241
|
+
"window.clearInterval(",
|
|
242
|
+
);
|
|
243
|
+
changed = true;
|
|
244
|
+
notes.push("Prefixed setTimeout/setInterval with window. for correct typing");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Strip data-fresh-disable-lock (Fresh-specific, not needed in React/TanStack)
|
|
248
|
+
if (result.includes("data-fresh-disable-lock")) {
|
|
249
|
+
result = result.replace(/\s*data-fresh-disable-lock=\{[^}]*\}/g, "");
|
|
250
|
+
result = result.replace(/\s*data-fresh-disable-lock="[^"]*"/g, "");
|
|
251
|
+
result = result.replace(/\s*data-fresh-disable-lock/g, "");
|
|
252
|
+
changed = true;
|
|
253
|
+
notes.push("Removed data-fresh-disable-lock attribute (Fresh-specific)");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Replace <x.Component {...x.props} /> with <SectionRenderer section={x} />
|
|
257
|
+
// In TanStack Start, nested sections have Component as a string key, not a function.
|
|
258
|
+
// SectionRenderer from @decocms/start/hooks handles the lazy registry lookup.
|
|
259
|
+
//
|
|
260
|
+
// Gate on ANY variant of the .Component/.props pattern (simple, extra props, or multi-line).
|
|
261
|
+
const sectionPatternGate = /\.\s*Component[\s\n]+\{\.\.\.(\w+)\.props\}/;
|
|
262
|
+
if (sectionPatternGate.test(result)) {
|
|
263
|
+
// 1. Simple: <x.Component {...x.props} />
|
|
264
|
+
result = result.replace(
|
|
265
|
+
/<(\w+)\.Component\s+\{\.\.\.(\w+)\.props\}\s*\/>/g,
|
|
266
|
+
(_, v1) => `<SectionRenderer section={${v1}} />`,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// 2. Extra props: <x.Component {...x.props} page={page} />
|
|
270
|
+
// Convert JSX attrs to object entries: name={expr} → name: expr
|
|
271
|
+
result = result.replace(
|
|
272
|
+
/<(\w+)\.Component\s+\{\.\.\.(\w+)\.props\}\s+([^/]+?)\s*\/>/g,
|
|
273
|
+
(_, varName, _v2, extraJsx) => {
|
|
274
|
+
const objEntries = jsxAttrsToObjectEntries(extraJsx.trim());
|
|
275
|
+
return `<SectionRenderer section={{ ...${varName}, props: { ...${varName}.props, ${objEntries} } }} />`;
|
|
276
|
+
},
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// 3. Multi-line: <x.Component\n {...x.props}\n/>
|
|
280
|
+
result = result.replace(
|
|
281
|
+
/<(\w+)\.Component\s*\n\s*\{\.\.\.(\w+)\.props\}\s*\n\s*\/>/g,
|
|
282
|
+
(_, v1) => `<SectionRenderer section={${v1}} />`,
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// 4. Multi-line with extra props: <x.Component\n {...x.props}\n extra={val}\n/>
|
|
286
|
+
result = result.replace(
|
|
287
|
+
/<(\w+)\.Component\s*\n\s*\{\.\.\.(\w+)\.props\}\s*\n\s*([^/]+?)\s*\n\s*\/>/g,
|
|
288
|
+
(_, varName, _v2, extraJsx) => {
|
|
289
|
+
const objEntries = jsxAttrsToObjectEntries(extraJsx.trim());
|
|
290
|
+
return `<SectionRenderer section={{ ...${varName}, props: { ...${varName}.props, ${objEntries} } }} />`;
|
|
291
|
+
},
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
if (!result.match(/^import\s.*SectionRenderer/m)) {
|
|
295
|
+
const hooksImportRe = /^import\s+\{([^}]*)\}\s+from\s+["']@decocms\/start\/hooks["'];?$/m;
|
|
296
|
+
const hooksMatch = result.match(hooksImportRe);
|
|
297
|
+
if (hooksMatch) {
|
|
298
|
+
result = result.replace(hooksImportRe, `import { ${hooksMatch[1].trim()}, SectionRenderer } from "@decocms/start/hooks";`);
|
|
299
|
+
} else {
|
|
300
|
+
result = `import { SectionRenderer } from "@decocms/start/hooks";\n${result}`;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
changed = true;
|
|
304
|
+
notes.push("Replaced .Component/.props section pattern with SectionRenderer");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Ensure React import exists if we introduced React.* references
|
|
308
|
+
if (
|
|
309
|
+
(result.includes("React.") || result.includes("React,")) &&
|
|
310
|
+
!result.match(/^import\s.*React/m)
|
|
311
|
+
) {
|
|
312
|
+
result = `import React from "react";\n${result}`;
|
|
313
|
+
changed = true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return { content: result, changed, notes };
|
|
317
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SectionConventionSets,
|
|
3
|
+
} from "../config";
|
|
4
|
+
import { resolveSectionConventions } from "../config";
|
|
5
|
+
import type { SectionMeta, TransformResult } from "../types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Adds section convention exports (sync, eager, layout, cache)
|
|
9
|
+
* to section files based on metadata extracted during analysis.
|
|
10
|
+
*
|
|
11
|
+
* These exports are read by generate-sections.ts in @decocms/start
|
|
12
|
+
* to build the sections.gen.ts registry.
|
|
13
|
+
*
|
|
14
|
+
* The set of section *names* that get hints applied is configurable
|
|
15
|
+
* via `.deco-migrate.config.json` (see `migrate/config.ts`). The exported
|
|
16
|
+
* `transformSectionConventions` keeps a back-compat signature using the
|
|
17
|
+
* baked-in defaults; new callers should prefer
|
|
18
|
+
* `createSectionConventionsTransform(sets)` so config can drive the lists.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
function getSectionBasename(filePath: string): string {
|
|
22
|
+
return filePath.split("/").pop()?.replace(/\.\w+$/, "") || "";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Build a `transformSectionConventions` closure bound to the given
|
|
27
|
+
* resolved sets. This is the preferred entry point — the caller (usually
|
|
28
|
+
* `phase-transform`) loads config once and passes the sets in.
|
|
29
|
+
*/
|
|
30
|
+
export function createSectionConventionsTransform(
|
|
31
|
+
sets: SectionConventionSets,
|
|
32
|
+
) {
|
|
33
|
+
return (
|
|
34
|
+
content: string,
|
|
35
|
+
sectionMeta: SectionMeta | undefined,
|
|
36
|
+
): TransformResult => transformWithSets(content, sectionMeta, sets);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Default-configured transform. Uses the baked-in defaults from `config.ts`. */
|
|
40
|
+
const defaultSets = resolveSectionConventions(null);
|
|
41
|
+
|
|
42
|
+
export function transformSectionConventions(
|
|
43
|
+
content: string,
|
|
44
|
+
sectionMeta: SectionMeta | undefined,
|
|
45
|
+
): TransformResult {
|
|
46
|
+
return transformWithSets(content, sectionMeta, defaultSets);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function transformWithSets(
|
|
50
|
+
content: string,
|
|
51
|
+
sectionMeta: SectionMeta | undefined,
|
|
52
|
+
sets: SectionConventionSets,
|
|
53
|
+
): TransformResult {
|
|
54
|
+
if (!sectionMeta) {
|
|
55
|
+
return { content, changed: false, notes: [] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const notes: string[] = [];
|
|
59
|
+
let result = content;
|
|
60
|
+
let changed = false;
|
|
61
|
+
const basename = getSectionBasename(sectionMeta.path);
|
|
62
|
+
|
|
63
|
+
// Header, footer, theme → eager + sync + layout
|
|
64
|
+
if (sectionMeta.isHeader || sectionMeta.isFooter || sectionMeta.isTheme) {
|
|
65
|
+
if (!result.includes("export const eager")) {
|
|
66
|
+
result += "\nexport const eager = true;\n";
|
|
67
|
+
notes.push("Added: export const eager = true");
|
|
68
|
+
changed = true;
|
|
69
|
+
}
|
|
70
|
+
if (!result.includes("export const sync")) {
|
|
71
|
+
result += "export const sync = true;\n";
|
|
72
|
+
notes.push("Added: export const sync = true");
|
|
73
|
+
changed = true;
|
|
74
|
+
}
|
|
75
|
+
// Header in golden does NOT have layout=true; only footer+theme do
|
|
76
|
+
if (
|
|
77
|
+
(sectionMeta.isFooter || sectionMeta.isTheme) &&
|
|
78
|
+
!result.includes("export const layout")
|
|
79
|
+
) {
|
|
80
|
+
result += "export const layout = true;\n";
|
|
81
|
+
notes.push("Added: export const layout = true");
|
|
82
|
+
changed = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Known eager+sync sections (non-layout)
|
|
87
|
+
if (sets.eagerSync.has(basename)) {
|
|
88
|
+
if (!result.includes("export const eager")) {
|
|
89
|
+
result += "\nexport const eager = true;\n";
|
|
90
|
+
notes.push(`Added: export const eager = true (${basename})`);
|
|
91
|
+
changed = true;
|
|
92
|
+
}
|
|
93
|
+
if (!result.includes("export const sync")) {
|
|
94
|
+
result += "export const sync = true;\n";
|
|
95
|
+
notes.push(`Added: export const sync = true (${basename})`);
|
|
96
|
+
changed = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Known sync-only sections
|
|
101
|
+
if (sets.sync.has(basename) && !result.includes("export const sync")) {
|
|
102
|
+
result += "\nexport const sync = true;\n";
|
|
103
|
+
notes.push(`Added: export const sync = true (${basename})`);
|
|
104
|
+
changed = true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Listing cache sections
|
|
108
|
+
if (
|
|
109
|
+
sets.listingCache.has(basename) &&
|
|
110
|
+
!result.includes("export const cache")
|
|
111
|
+
) {
|
|
112
|
+
result += '\nexport const cache = "listing";\n';
|
|
113
|
+
notes.push(`Added: export const cache = "listing" (${basename})`);
|
|
114
|
+
changed = true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Static cache sections
|
|
118
|
+
if (
|
|
119
|
+
sets.staticCache.has(basename) &&
|
|
120
|
+
!result.includes("export const cache")
|
|
121
|
+
) {
|
|
122
|
+
result += '\nexport const cache = "static";\n';
|
|
123
|
+
notes.push(`Added: export const cache = "static" (${basename})`);
|
|
124
|
+
changed = true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Generic: listing sections not already matched above
|
|
128
|
+
if (sectionMeta.isListing && !result.includes("export const cache")) {
|
|
129
|
+
result += '\nexport const cache = "listing";\n';
|
|
130
|
+
notes.push('Added: export const cache = "listing"');
|
|
131
|
+
changed = true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Sections with loaders that use device → add sync (needs SSR device detection)
|
|
135
|
+
if (
|
|
136
|
+
sectionMeta.hasLoader &&
|
|
137
|
+
sectionMeta.loaderUsesDevice &&
|
|
138
|
+
!result.includes("export const sync")
|
|
139
|
+
) {
|
|
140
|
+
result += "\nexport const sync = true;\n";
|
|
141
|
+
notes.push("Added: export const sync = true (loader uses device)");
|
|
142
|
+
changed = true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Sections that render nested Section children need sync so they're in
|
|
146
|
+
// the syncComponents registry (SectionRenderer resolves the string key).
|
|
147
|
+
const hasNestedSections =
|
|
148
|
+
/children:\s*Section\b/.test(result) ||
|
|
149
|
+
/fallback:\s*Section\b/.test(result);
|
|
150
|
+
if (hasNestedSections && !result.includes("export const sync")) {
|
|
151
|
+
result += "\nexport const sync = true;\n";
|
|
152
|
+
notes.push(
|
|
153
|
+
"Added: export const sync = true (renders nested Section children)",
|
|
154
|
+
);
|
|
155
|
+
changed = true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Re-export sections that wrap PDP/nested content need sync too.
|
|
159
|
+
// Detect: file is a re-export AND the target component renders nested Sections
|
|
160
|
+
const isReExport = /^export\s+\{[^}]*default[^}]*\}\s+from\s+/.test(
|
|
161
|
+
result.trim(),
|
|
162
|
+
);
|
|
163
|
+
if (
|
|
164
|
+
isReExport &&
|
|
165
|
+
(basename === "MountedPDP" || basename === "NotFoundChallenge")
|
|
166
|
+
) {
|
|
167
|
+
if (!result.includes("export const sync")) {
|
|
168
|
+
result += "\nexport const sync = true;\n";
|
|
169
|
+
notes.push(`Added: export const sync = true (re-export: ${basename})`);
|
|
170
|
+
changed = true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Don't add LoadingFallback re-exports to thin section files —
|
|
175
|
+
// we can't guarantee the target component exports it.
|
|
176
|
+
// Instead, if it's a listing section, a generic skeleton will be added below.
|
|
177
|
+
|
|
178
|
+
// Generate a basic LoadingFallback if the section doesn't have one
|
|
179
|
+
// and it's a listing section (visible skeleton improvement)
|
|
180
|
+
if (
|
|
181
|
+
sectionMeta.isListing &&
|
|
182
|
+
!sectionMeta.hasLoadingFallback &&
|
|
183
|
+
!result.includes("LoadingFallback")
|
|
184
|
+
) {
|
|
185
|
+
result += `
|
|
186
|
+
export function LoadingFallback() {
|
|
187
|
+
return (
|
|
188
|
+
<div className="w-full py-8">
|
|
189
|
+
<div className="container mx-auto px-4">
|
|
190
|
+
<div className="h-6 w-48 bg-base-200 animate-pulse rounded mb-4" />
|
|
191
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
192
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
193
|
+
<div key={i} className="flex flex-col gap-2">
|
|
194
|
+
<div className="aspect-square bg-base-200 animate-pulse rounded" />
|
|
195
|
+
<div className="h-4 bg-base-200 animate-pulse rounded w-3/4" />
|
|
196
|
+
<div className="h-4 bg-base-200 animate-pulse rounded w-1/2" />
|
|
197
|
+
</div>
|
|
198
|
+
))}
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
`;
|
|
205
|
+
notes.push("Added: LoadingFallback skeleton for listing section");
|
|
206
|
+
changed = true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { content: result, changed, notes };
|
|
210
|
+
}
|