@kernlang/react 3.1.6 → 3.1.8
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/dist/artifact-utils.d.ts +1 -1
- package/dist/artifact-utils.js +6 -8
- package/dist/artifact-utils.js.map +1 -1
- package/dist/codegen-react.js +35 -17
- package/dist/codegen-react.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/nextjs-assembler.d.ts +33 -0
- package/dist/nextjs-assembler.js +233 -0
- package/dist/nextjs-assembler.js.map +1 -0
- package/dist/nextjs-imports.d.ts +9 -0
- package/dist/nextjs-imports.js +61 -0
- package/dist/nextjs-imports.js.map +1 -0
- package/dist/nextjs-renderers.d.ts +4 -0
- package/dist/nextjs-renderers.js +628 -0
- package/dist/nextjs-renderers.js.map +1 -0
- package/dist/nextjs-style.d.ts +8 -0
- package/dist/nextjs-style.js +135 -0
- package/dist/nextjs-style.js.map +1 -0
- package/dist/nextjs-types.d.ts +39 -0
- package/dist/nextjs-types.js +2 -0
- package/dist/nextjs-types.js.map +1 -0
- package/dist/structure.d.ts +1 -1
- package/dist/structure.js +56 -22
- package/dist/structure.js.map +1 -1
- package/dist/transpiler-nextjs.d.ts +4 -7
- package/dist/transpiler-nextjs.js +20 -1173
- package/dist/transpiler-nextjs.js.map +1 -1
- package/dist/transpiler-tailwind.d.ts +1 -1
- package/dist/transpiler-tailwind.js +83 -51
- package/dist/transpiler-tailwind.js.map +1 -1
- package/dist/transpiler-web.d.ts +1 -1
- package/dist/transpiler-web.js +33 -19
- package/dist/transpiler-web.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,779 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { planStructure } from './structure.js';
|
|
1
|
+
import { accountNode, buildDiagnostics, countTokens, getProps, serializeIR } from '@kernlang/core';
|
|
3
2
|
import { buildStructuredArtifacts } from './artifact-utils.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const spec = ctx.imports.get(source) || { namedImports: new Set(), typeOnlyImports: new Set() };
|
|
8
|
-
spec.defaultImport = name;
|
|
9
|
-
ctx.imports.set(source, spec);
|
|
10
|
-
}
|
|
11
|
-
function addNamedImport(ctx, source, name, typeOnly) {
|
|
12
|
-
const spec = ctx.imports.get(source) || { namedImports: new Set(), typeOnlyImports: new Set() };
|
|
13
|
-
if (typeOnly) {
|
|
14
|
-
spec.typeOnlyImports.add(name);
|
|
15
|
-
}
|
|
16
|
-
else {
|
|
17
|
-
spec.namedImports.add(name);
|
|
18
|
-
}
|
|
19
|
-
ctx.imports.set(source, spec);
|
|
20
|
-
}
|
|
21
|
-
function exprCode(value, fallback) {
|
|
22
|
-
if (typeof value === 'object' && value !== null && '__expr' in value) {
|
|
23
|
-
return value.code;
|
|
24
|
-
}
|
|
25
|
-
if (typeof value === 'string' && value.length > 0)
|
|
26
|
-
return value;
|
|
27
|
-
return fallback;
|
|
28
|
-
}
|
|
29
|
-
function emitImports(ctx) {
|
|
30
|
-
const lines = [];
|
|
31
|
-
for (const [source, spec] of [...ctx.imports.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
32
|
-
// Separate type-only imports from value imports
|
|
33
|
-
const typeImports = [...spec.typeOnlyImports].filter(n => !spec.namedImports.has(n));
|
|
34
|
-
const valueImports = [...spec.namedImports];
|
|
35
|
-
// Emit type-only import statement if there are type imports and no value imports sharing the source
|
|
36
|
-
if (typeImports.length > 0 && valueImports.length === 0 && !spec.defaultImport) {
|
|
37
|
-
lines.push(`import type { ${typeImports.sort().join(', ')} } from '${source}';`);
|
|
38
|
-
}
|
|
39
|
-
else {
|
|
40
|
-
// Emit value import (with default if present)
|
|
41
|
-
const clauses = [];
|
|
42
|
-
if (spec.defaultImport)
|
|
43
|
-
clauses.push(spec.defaultImport);
|
|
44
|
-
if (valueImports.length > 0)
|
|
45
|
-
clauses.push(`{ ${valueImports.sort().join(', ')} }`);
|
|
46
|
-
if (clauses.length > 0) {
|
|
47
|
-
lines.push(`import ${clauses.join(', ')} from '${source}';`);
|
|
48
|
-
}
|
|
49
|
-
// Emit separate type-only import if both type and value imports exist
|
|
50
|
-
if (typeImports.length > 0) {
|
|
51
|
-
lines.push(`import type { ${typeImports.sort().join(', ')} } from '${source}';`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return lines;
|
|
56
|
-
}
|
|
57
|
-
function twClasses(node, ctx, extra = '') {
|
|
58
|
-
const rawStyles = getStyles(node);
|
|
59
|
-
// Expand semicolon-separated values that the parser merged into one entry
|
|
60
|
-
const styles = {};
|
|
61
|
-
for (const [k, v] of Object.entries(rawStyles)) {
|
|
62
|
-
if (v.includes(';')) {
|
|
63
|
-
const segs = v.split(';').map(s => s.trim()).filter(Boolean);
|
|
64
|
-
styles[k] = segs[0];
|
|
65
|
-
for (let i = 1; i < segs.length; i++) {
|
|
66
|
-
const ci = segs[i].indexOf(':');
|
|
67
|
-
if (ci > 0)
|
|
68
|
-
styles[segs[i].slice(0, ci).trim()] = segs[i].slice(ci + 1).trim();
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
styles[k] = v;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
// Extract className pass-through (e.g. {className:doc.page} → className={doc.page})
|
|
76
|
-
const classNameRef = styles.className;
|
|
77
|
-
const inlineStyles = {};
|
|
78
|
-
const filteredStyles = {};
|
|
79
|
-
for (const [k, v] of Object.entries(styles)) {
|
|
80
|
-
if (k === 'className')
|
|
81
|
-
continue;
|
|
82
|
-
// Vendor-prefixed properties, CSS custom properties, and complex values → inline style
|
|
83
|
-
if (k.startsWith('-') || v.includes('var(') || k === 'borderBottom' || k === 'background' || k === 'color' || k === 'fontFamily') {
|
|
84
|
-
inlineStyles[k] = v;
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
filteredStyles[k] = v;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
let tw = stylesToTailwind(filteredStyles, ctx.colors);
|
|
91
|
-
if (ctx.twProfile)
|
|
92
|
-
tw = applyTailwindTokenRules(tw, ctx.twProfile);
|
|
93
|
-
const parts = [tw, extra].filter(Boolean);
|
|
94
|
-
const attrs = [];
|
|
95
|
-
if (classNameRef) {
|
|
96
|
-
// Detect if className is a JS expression (contains . or [) vs a plain CSS class string
|
|
97
|
-
const isExpr = /[.\[\](]/.test(classNameRef);
|
|
98
|
-
if (isExpr) {
|
|
99
|
-
if (parts.length > 0) {
|
|
100
|
-
attrs.push(` className={\`\${${classNameRef}} ${parts.join(' ')}\`}`);
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
attrs.push(` className={${classNameRef}}`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
// Plain CSS class name(s) — quote them
|
|
108
|
-
if (parts.length > 0) {
|
|
109
|
-
attrs.push(` className="${classNameRef} ${parts.join(' ')}"`);
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
attrs.push(` className="${classNameRef}"`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
else if (parts.length > 0) {
|
|
117
|
-
attrs.push(` className="${parts.join(' ')}"`);
|
|
118
|
-
}
|
|
119
|
-
if (Object.keys(inlineStyles).length > 0) {
|
|
120
|
-
const pairs = Object.entries(inlineStyles).map(([k, v]) => {
|
|
121
|
-
let jsKey;
|
|
122
|
-
if (k.startsWith('-')) {
|
|
123
|
-
// Vendor prefix: -webkit-background-clip → WebkitBackgroundClip
|
|
124
|
-
jsKey = k.slice(1).replace(/-([a-z])/g, (_, c) => c.toUpperCase())
|
|
125
|
-
.replace(/^[a-z]/, c => c.toUpperCase());
|
|
126
|
-
}
|
|
127
|
-
else {
|
|
128
|
-
jsKey = k.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
129
|
-
}
|
|
130
|
-
return `${jsKey}: '${v}'`;
|
|
131
|
-
});
|
|
132
|
-
attrs.push(` style={{ ${pairs.join(', ')} }}`);
|
|
133
|
-
}
|
|
134
|
-
return attrs.join('');
|
|
135
|
-
}
|
|
136
|
-
// ── Route path helper ────────────────────────────────────────────────────
|
|
137
|
-
function routeToPath(route, segment) {
|
|
138
|
-
// Normalize: strip leading/trailing slashes
|
|
139
|
-
const normalized = route.replace(/^\/+|\/+$/g, '');
|
|
140
|
-
const parts = normalized ? normalized.split('/') : [];
|
|
141
|
-
if (segment)
|
|
142
|
-
parts.push(segment);
|
|
143
|
-
return parts.length > 0 ? parts.join('/') + '/' : '';
|
|
144
|
-
}
|
|
145
|
-
// ── Node renderers ──────────────────────────────────────────────────────
|
|
146
|
-
function renderNode(node, ctx, indent) {
|
|
147
|
-
const p = getProps(node);
|
|
148
|
-
ctx.sourceMap.push({ irLine: node.loc?.line || 0, irCol: node.loc?.col || 1, outLine: ctx.lines.length + 1, outCol: 1 });
|
|
149
|
-
switch (node.type) {
|
|
150
|
-
case 'page':
|
|
151
|
-
case 'screen':
|
|
152
|
-
renderPage(node, ctx, indent);
|
|
153
|
-
break;
|
|
154
|
-
case 'layout':
|
|
155
|
-
renderLayout(node, ctx, indent);
|
|
156
|
-
break;
|
|
157
|
-
case 'loading':
|
|
158
|
-
renderLoading(node, ctx, indent);
|
|
159
|
-
break;
|
|
160
|
-
case 'error':
|
|
161
|
-
renderError(node, ctx, indent);
|
|
162
|
-
break;
|
|
163
|
-
case 'metadata':
|
|
164
|
-
renderMetadata(node, ctx);
|
|
165
|
-
break;
|
|
166
|
-
case 'section':
|
|
167
|
-
renderSection(node, ctx, indent);
|
|
168
|
-
break;
|
|
169
|
-
case 'card':
|
|
170
|
-
renderCard(node, ctx, indent);
|
|
171
|
-
break;
|
|
172
|
-
case 'row':
|
|
173
|
-
ctx.lines.push(`${indent}<div${twClasses(node, ctx, 'flex')}>`);
|
|
174
|
-
renderChildren(node, ctx, indent);
|
|
175
|
-
ctx.lines.push(`${indent}</div>`);
|
|
176
|
-
break;
|
|
177
|
-
case 'col':
|
|
178
|
-
ctx.lines.push(`${indent}<div${twClasses(node, ctx, 'flex flex-col')}>`);
|
|
179
|
-
renderChildren(node, ctx, indent);
|
|
180
|
-
ctx.lines.push(`${indent}</div>`);
|
|
181
|
-
break;
|
|
182
|
-
case 'text':
|
|
183
|
-
renderText(node, ctx, indent);
|
|
184
|
-
break;
|
|
185
|
-
case 'divider':
|
|
186
|
-
ctx.lines.push(`${indent}<div${twClasses(node, ctx, 'h-px')} />`);
|
|
187
|
-
break;
|
|
188
|
-
case 'button':
|
|
189
|
-
renderButton(node, ctx, indent);
|
|
190
|
-
break;
|
|
191
|
-
case 'link':
|
|
192
|
-
renderLink(node, ctx, indent);
|
|
193
|
-
break;
|
|
194
|
-
case 'image':
|
|
195
|
-
renderImage(node, ctx, indent);
|
|
196
|
-
break;
|
|
197
|
-
case 'codeblock':
|
|
198
|
-
renderCodeBlock(node, ctx, indent);
|
|
199
|
-
break;
|
|
200
|
-
case 'input':
|
|
201
|
-
case 'textarea':
|
|
202
|
-
renderInput(node, ctx, indent);
|
|
203
|
-
break;
|
|
204
|
-
case 'slider':
|
|
205
|
-
renderSlider(node, ctx, indent);
|
|
206
|
-
break;
|
|
207
|
-
case 'toggle':
|
|
208
|
-
renderToggle(node, ctx, indent);
|
|
209
|
-
break;
|
|
210
|
-
case 'grid':
|
|
211
|
-
renderGrid(node, ctx, indent);
|
|
212
|
-
break;
|
|
213
|
-
case 'conditional':
|
|
214
|
-
renderConditional(node, ctx, indent);
|
|
215
|
-
break;
|
|
216
|
-
case 'component':
|
|
217
|
-
renderComponent(node, ctx, indent);
|
|
218
|
-
break;
|
|
219
|
-
case 'icon':
|
|
220
|
-
ctx.componentImports.add('Icon');
|
|
221
|
-
ctx.lines.push(`${indent}<Icon name="${p.name}" size="sm"${twClasses(node, ctx)} />`);
|
|
222
|
-
break;
|
|
223
|
-
case 'svg':
|
|
224
|
-
renderSvg(node, ctx, indent);
|
|
225
|
-
break;
|
|
226
|
-
case 'form':
|
|
227
|
-
ctx.lines.push(`${indent}<form${twClasses(node, ctx)}>`);
|
|
228
|
-
renderChildren(node, ctx, indent);
|
|
229
|
-
ctx.lines.push(`${indent}</form>`);
|
|
230
|
-
break;
|
|
231
|
-
case 'list':
|
|
232
|
-
ctx.lines.push(`${indent}<div${twClasses(node, ctx, 'space-y-2')}>`);
|
|
233
|
-
renderChildren(node, ctx, indent);
|
|
234
|
-
ctx.lines.push(`${indent}</div>`);
|
|
235
|
-
break;
|
|
236
|
-
case 'item':
|
|
237
|
-
ctx.lines.push(`${indent}<div${twClasses(node, ctx)}>`);
|
|
238
|
-
renderChildren(node, ctx, indent);
|
|
239
|
-
ctx.lines.push(`${indent}</div>`);
|
|
240
|
-
break;
|
|
241
|
-
case 'progress':
|
|
242
|
-
renderProgress(node, ctx, indent);
|
|
243
|
-
break;
|
|
244
|
-
case 'tabs':
|
|
245
|
-
ctx.lines.push(`${indent}<nav${twClasses(node, ctx, 'flex')}>`);
|
|
246
|
-
renderChildren(node, ctx, indent);
|
|
247
|
-
ctx.lines.push(`${indent}</nav>`);
|
|
248
|
-
break;
|
|
249
|
-
case 'tab':
|
|
250
|
-
ctx.lines.push(`${indent}<button${twClasses(node, ctx)}>${escapeJsxText(String(p.label || ''))}</button>`);
|
|
251
|
-
break;
|
|
252
|
-
case 'table':
|
|
253
|
-
ctx.lines.push(`${indent}<table${twClasses(node, ctx)}>`);
|
|
254
|
-
renderChildren(node, ctx, indent);
|
|
255
|
-
ctx.lines.push(`${indent}</table>`);
|
|
256
|
-
break;
|
|
257
|
-
case 'thead':
|
|
258
|
-
ctx.lines.push(`${indent}<thead>`);
|
|
259
|
-
renderChildren(node, ctx, indent);
|
|
260
|
-
ctx.lines.push(`${indent}</thead>`);
|
|
261
|
-
break;
|
|
262
|
-
case 'tbody':
|
|
263
|
-
ctx.lines.push(`${indent}<tbody>`);
|
|
264
|
-
renderChildren(node, ctx, indent);
|
|
265
|
-
ctx.lines.push(`${indent}</tbody>`);
|
|
266
|
-
break;
|
|
267
|
-
case 'tr':
|
|
268
|
-
ctx.lines.push(`${indent}<tr${twClasses(node, ctx)}>`);
|
|
269
|
-
renderChildren(node, ctx, indent);
|
|
270
|
-
ctx.lines.push(`${indent}</tr>`);
|
|
271
|
-
break;
|
|
272
|
-
case 'th':
|
|
273
|
-
renderTableCell(node, ctx, indent, 'th');
|
|
274
|
-
break;
|
|
275
|
-
case 'td':
|
|
276
|
-
renderTableCell(node, ctx, indent, 'td');
|
|
277
|
-
break;
|
|
278
|
-
case 'generateMetadata':
|
|
279
|
-
renderGenerateMetadata(node, ctx);
|
|
280
|
-
break;
|
|
281
|
-
case 'notFound':
|
|
282
|
-
renderNotFound(node, ctx, indent);
|
|
283
|
-
break;
|
|
284
|
-
case 'redirect':
|
|
285
|
-
renderRedirect(node, ctx, indent);
|
|
286
|
-
break;
|
|
287
|
-
case 'import':
|
|
288
|
-
renderImport(node, ctx);
|
|
289
|
-
break;
|
|
290
|
-
case 'fetch':
|
|
291
|
-
renderFetchNode(node, ctx);
|
|
292
|
-
break;
|
|
293
|
-
case 'on':
|
|
294
|
-
renderOnHandler(node, ctx);
|
|
295
|
-
return;
|
|
296
|
-
case 'state':
|
|
297
|
-
ctx.stateDecls.push({ name: String(p.name || ''), initial: String(p.initial ?? '') });
|
|
298
|
-
ctx.isClient = true; // state requires 'use client'
|
|
299
|
-
return;
|
|
300
|
-
case 'logic':
|
|
301
|
-
if (p.code)
|
|
302
|
-
ctx.logicBlocks.push(String(p.code));
|
|
303
|
-
else if (node.children) {
|
|
304
|
-
const handlerChild = node.children.find(c => c.type === 'handler');
|
|
305
|
-
if (handlerChild?.props?.code)
|
|
306
|
-
ctx.logicBlocks.push(String(handlerChild.props.code));
|
|
307
|
-
}
|
|
308
|
-
ctx.isClient = true;
|
|
309
|
-
return;
|
|
310
|
-
case 'theme': break;
|
|
311
|
-
default:
|
|
312
|
-
ctx.lines.push(`${indent}<div${twClasses(node, ctx)}>`);
|
|
313
|
-
renderChildren(node, ctx, indent);
|
|
314
|
-
ctx.lines.push(`${indent}</div>`);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
function renderChildren(node, ctx, indent) {
|
|
318
|
-
if (node.children)
|
|
319
|
-
for (const child of node.children)
|
|
320
|
-
renderNode(child, ctx, indent + ' ');
|
|
321
|
-
}
|
|
322
|
-
function renderPage(node, ctx, indent) {
|
|
323
|
-
const p = getProps(node);
|
|
324
|
-
if (p.client === 'true' || p.client === true)
|
|
325
|
-
ctx.isClient = true;
|
|
326
|
-
if (p.async === 'true' || p.async === true)
|
|
327
|
-
ctx.isAsync = true;
|
|
328
|
-
ctx.lines.push(`${indent}<div${twClasses(node, ctx)}>`);
|
|
329
|
-
renderChildren(node, ctx, indent);
|
|
330
|
-
ctx.lines.push(`${indent}</div>`);
|
|
331
|
-
}
|
|
332
|
-
function renderLayout(node, ctx, indent) {
|
|
333
|
-
const p = getProps(node);
|
|
334
|
-
ctx.lines.push(`${indent}<html lang="${p.lang || 'en'}">`);
|
|
335
|
-
ctx.lines.push(`${indent} <body${twClasses(node, ctx)}>`);
|
|
336
|
-
ctx.lines.push(`${indent} {children}`);
|
|
337
|
-
renderChildren(node, ctx, indent + ' ');
|
|
338
|
-
ctx.lines.push(`${indent} </body>`);
|
|
339
|
-
ctx.lines.push(`${indent}</html>`);
|
|
340
|
-
}
|
|
341
|
-
function renderLoading(node, ctx, indent) {
|
|
342
|
-
ctx.lines.push(`${indent}<div${twClasses(node, ctx, 'animate-pulse')}>`);
|
|
343
|
-
renderChildren(node, ctx, indent);
|
|
344
|
-
ctx.lines.push(`${indent}</div>`);
|
|
345
|
-
}
|
|
346
|
-
function renderError(node, ctx, indent) {
|
|
347
|
-
ctx.isClient = true;
|
|
348
|
-
ctx.lines.push(`${indent}<div${twClasses(node, ctx)}>`);
|
|
349
|
-
ctx.lines.push(`${indent} <h2>Something went wrong!</h2>`);
|
|
350
|
-
ctx.lines.push(`${indent} <button onClick={() => reset()}>Try again</button>`);
|
|
351
|
-
renderChildren(node, ctx, indent);
|
|
352
|
-
ctx.lines.push(`${indent}</div>`);
|
|
353
|
-
}
|
|
354
|
-
function renderMetadata(node, ctx) {
|
|
355
|
-
const p = getProps(node);
|
|
356
|
-
ctx.metadata = {};
|
|
357
|
-
if (p.title)
|
|
358
|
-
ctx.metadata.title = p.title;
|
|
359
|
-
if (p.description)
|
|
360
|
-
ctx.metadata.description = p.description;
|
|
361
|
-
if (p.keywords)
|
|
362
|
-
ctx.metadata.keywords = p.keywords;
|
|
363
|
-
if (p.ogImage)
|
|
364
|
-
ctx.metadata.ogImage = p.ogImage;
|
|
365
|
-
}
|
|
366
|
-
function renderSection(node, ctx, indent) {
|
|
367
|
-
const p = getProps(node);
|
|
368
|
-
const title = p.title || '';
|
|
369
|
-
const id = p.id;
|
|
370
|
-
const idAttr = id ? ` id="${id}"` : '';
|
|
371
|
-
const tw = twClasses(node, ctx);
|
|
372
|
-
ctx.lines.push(`${indent}<section${idAttr}${tw}>`);
|
|
373
|
-
if (title) {
|
|
374
|
-
ctx.lines.push(`${indent} <h2 className="text-lg font-semibold mb-4">${escapeJsxText(title)}</h2>`);
|
|
375
|
-
}
|
|
376
|
-
renderChildren(node, ctx, indent);
|
|
377
|
-
ctx.lines.push(`${indent}</section>`);
|
|
378
|
-
}
|
|
379
|
-
function renderCodeBlock(node, ctx, indent) {
|
|
380
|
-
const p = getProps(node);
|
|
381
|
-
const lang = p.lang || '';
|
|
382
|
-
const langClass = lang ? ` language-${lang}` : '';
|
|
383
|
-
const hasCustomStyle = getStyles(node).className || getStyles(node).background;
|
|
384
|
-
const preAttrs = hasCustomStyle ? twClasses(node, ctx) : ` className="bg-zinc-900 rounded-lg p-4 overflow-x-auto"`;
|
|
385
|
-
const codeClass = hasCustomStyle
|
|
386
|
-
? `className="${langClass.trim()}"` + (getStyles(node).fontFamily ? ` style={{ fontFamily: '${getStyles(node).fontFamily}' }}` : '')
|
|
387
|
-
: `className="text-sm font-mono text-zinc-100${langClass}"`;
|
|
388
|
-
// Content: inline value prop or body child node
|
|
389
|
-
const rawValue = p.value;
|
|
390
|
-
if (isExpr(rawValue)) {
|
|
391
|
-
ctx.lines.push(`${indent}<pre${preAttrs}>`);
|
|
392
|
-
ctx.lines.push(`${indent} <code ${codeClass}>{${rawValue.code}}</code>`);
|
|
393
|
-
ctx.lines.push(`${indent}</pre>`);
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
let content = rawValue || '';
|
|
397
|
-
if (!content && node.children) {
|
|
398
|
-
const bodyNode = node.children.find(c => c.type === 'body');
|
|
399
|
-
if (bodyNode) {
|
|
400
|
-
const bp = getProps(bodyNode);
|
|
401
|
-
// body value="..." OR body <<<...>>> (multiline block → code prop)
|
|
402
|
-
content = bp.code || bp.value || '';
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
// Escape for JSX template literal: backslashes, backticks, ${
|
|
406
|
-
const escaped = content
|
|
407
|
-
.replace(/\\/g, '\\\\')
|
|
408
|
-
.replace(/`/g, '\\`')
|
|
409
|
-
.replace(/\$\{/g, '\\${');
|
|
410
|
-
ctx.lines.push(`${indent}<pre${preAttrs}>`);
|
|
411
|
-
ctx.lines.push(`${indent} <code ${codeClass}>{\`${escaped}\`}</code>`);
|
|
412
|
-
ctx.lines.push(`${indent}</pre>`);
|
|
413
|
-
}
|
|
414
|
-
function renderCard(node, ctx, indent) {
|
|
415
|
-
const styles = { ...getStyles(node) };
|
|
416
|
-
const border = styles.border;
|
|
417
|
-
delete styles.border;
|
|
418
|
-
// Use a shallow-copied styles object to avoid mutating the live IR node
|
|
419
|
-
if (node.props) {
|
|
420
|
-
const origStyles = node.props.styles;
|
|
421
|
-
node.props.styles = styles;
|
|
422
|
-
const extra = border ? `border ${colorToTw('border', border, ctx.colors)}` : '';
|
|
423
|
-
ctx.lines.push(`${indent}<div${twClasses(node, ctx, extra)}>`);
|
|
424
|
-
renderChildren(node, ctx, indent);
|
|
425
|
-
ctx.lines.push(`${indent}</div>`);
|
|
426
|
-
node.props.styles = origStyles;
|
|
427
|
-
}
|
|
428
|
-
else {
|
|
429
|
-
const extra = border ? `border ${colorToTw('border', border, ctx.colors)}` : '';
|
|
430
|
-
ctx.lines.push(`${indent}<div${twClasses(node, ctx, extra)}>`);
|
|
431
|
-
renderChildren(node, ctx, indent);
|
|
432
|
-
ctx.lines.push(`${indent}</div>`);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
const TEXT_TAG_MAP = { p: 'p', h1: 'h1', h2: 'h2', h3: 'h3', h4: 'h4', h5: 'h5', h6: 'h6', label: 'label', span: 'span', pre: 'pre', code: 'code' };
|
|
436
|
-
function renderText(node, ctx, indent) {
|
|
437
|
-
const p = getProps(node);
|
|
438
|
-
const rawValue = p.value;
|
|
439
|
-
const bind = p.bind;
|
|
440
|
-
const el = TEXT_TAG_MAP[p.tag] || 'span';
|
|
441
|
-
const tw = twClasses(node, ctx);
|
|
442
|
-
if (isExpr(rawValue))
|
|
443
|
-
ctx.lines.push(`${indent}<${el}${tw}>{${rawValue.code}}</${el}>`);
|
|
444
|
-
else if (bind)
|
|
445
|
-
ctx.lines.push(`${indent}<${el}${tw}>{${bind}}</${el}>`);
|
|
446
|
-
else if (rawValue)
|
|
447
|
-
ctx.lines.push(`${indent}<${el}${tw}>${escapeJsxText(rawValue)}</${el}>`);
|
|
448
|
-
}
|
|
449
|
-
function renderButton(node, ctx, indent) {
|
|
450
|
-
const p = getProps(node);
|
|
451
|
-
const text = p.text || '';
|
|
452
|
-
const to = p.to;
|
|
453
|
-
const rawOnClick = p.onClick;
|
|
454
|
-
const onClick = isExpr(rawOnClick) ? rawOnClick.code : rawOnClick;
|
|
455
|
-
if (to) {
|
|
456
|
-
addDefaultImport(ctx, 'next/link', 'Link');
|
|
457
|
-
ctx.lines.push(`${indent}<Link href="/${to.toLowerCase()}"${twClasses(node, ctx)}>${escapeJsxText(text)}</Link>`);
|
|
458
|
-
}
|
|
459
|
-
else {
|
|
460
|
-
ctx.isClient = true; // onClick requires 'use client'
|
|
461
|
-
ctx.lines.push(`${indent}<button${twClasses(node, ctx)} onClick={${onClick || '() => {}'}}>${escapeJsxText(text)}</button>`);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
function renderInput(node, ctx, indent) {
|
|
465
|
-
const p = getProps(node);
|
|
466
|
-
const isTextarea = node.type === 'textarea' || p.type === 'textarea' || p.multiline;
|
|
467
|
-
const tag = isTextarea ? 'textarea' : 'input';
|
|
468
|
-
const attrs = [];
|
|
469
|
-
const tw = twClasses(node, ctx);
|
|
470
|
-
if (p.bind) {
|
|
471
|
-
const bind = p.bind;
|
|
472
|
-
const setter = `set${bind.charAt(0).toUpperCase() + bind.slice(1)}`;
|
|
473
|
-
attrs.push(`value={${bind}}`);
|
|
474
|
-
ctx.isClient = true; // onChange requires 'use client'
|
|
475
|
-
if (isExpr(p.onChange))
|
|
476
|
-
attrs.push(`onChange={${p.onChange.code}}`);
|
|
477
|
-
else if (p.onChange)
|
|
478
|
-
attrs.push(`onChange={${p.onChange}}`);
|
|
479
|
-
else
|
|
480
|
-
attrs.push(`onChange={(e) => ${setter}(e.target.value)}`);
|
|
481
|
-
}
|
|
482
|
-
if (p.placeholder)
|
|
483
|
-
attrs.push(`placeholder="${p.placeholder}"`);
|
|
484
|
-
if (!isTextarea && p.type && p.type !== 'textarea')
|
|
485
|
-
attrs.push(`type="${p.type}"`);
|
|
486
|
-
if (p.spellcheck === 'false' || p.spellcheck === false)
|
|
487
|
-
attrs.push('spellCheck={false}');
|
|
488
|
-
const attrStr = attrs.length > 0 ? ' ' + attrs.join(' ') : '';
|
|
489
|
-
if (isTextarea) {
|
|
490
|
-
ctx.lines.push(`${indent}<${tag}${tw}${attrStr} rows={4} />`);
|
|
491
|
-
}
|
|
492
|
-
else {
|
|
493
|
-
ctx.lines.push(`${indent}<${tag}${tw}${attrStr} />`);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
function renderLink(node, ctx, indent) {
|
|
497
|
-
const p = getProps(node);
|
|
498
|
-
addDefaultImport(ctx, 'next/link', 'Link');
|
|
499
|
-
ctx.lines.push(`${indent}<Link href="${p.to || '/'}"${twClasses(node, ctx)}>`);
|
|
500
|
-
renderChildren(node, ctx, indent);
|
|
501
|
-
ctx.lines.push(`${indent}</Link>`);
|
|
502
|
-
}
|
|
503
|
-
function renderImage(node, ctx, indent) {
|
|
504
|
-
const p = getProps(node);
|
|
505
|
-
addDefaultImport(ctx, 'next/image', 'Image');
|
|
506
|
-
const tw = twClasses(node, ctx);
|
|
507
|
-
const rawSrc = p.src || '';
|
|
508
|
-
const src = (rawSrc.startsWith('/') || rawSrc.includes('://') || rawSrc.includes('.')) ? rawSrc : `/${rawSrc}.png`;
|
|
509
|
-
const alt = escapeJsxAttr(String(p.alt || p.src || ''));
|
|
510
|
-
const fill = p.fill === 'true' || p.fill === true;
|
|
511
|
-
const priority = p.priority === 'true' || p.priority === true;
|
|
512
|
-
if (fill) {
|
|
513
|
-
ctx.lines.push(`${indent}<Image src="${src}" alt="${alt}"${priority ? ' priority' : ''} fill${tw} />`);
|
|
514
|
-
}
|
|
515
|
-
else {
|
|
516
|
-
const width = p.width || (getStyles(node).w) || '100';
|
|
517
|
-
const height = p.height || (getStyles(node).h) || '100';
|
|
518
|
-
ctx.lines.push(`${indent}<Image src="${src}" alt="${alt}" width={${width}} height={${height}}${priority ? ' priority' : ''}${tw} />`);
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
function renderSlider(node, ctx, indent) {
|
|
522
|
-
const p = getProps(node);
|
|
523
|
-
const bind = p.bind;
|
|
524
|
-
const setter = bind ? `set${bind.charAt(0).toUpperCase() + bind.slice(1)}` : 'setValue';
|
|
525
|
-
ctx.isClient = true; // onChange requires 'use client'
|
|
526
|
-
ctx.lines.push(`${indent}<input type="range" min={${p.min || 0}} max={${p.max || 100}} step={${p.step || 1}} value={${bind || 'value'}} onChange={(e) => ${setter}(parseFloat(e.target.value))} className="w-full h-2 bg-zinc-700 rounded-lg appearance-none cursor-pointer accent-orange-500" />`);
|
|
527
|
-
}
|
|
528
|
-
function renderToggle(node, ctx, indent) {
|
|
529
|
-
const p = getProps(node);
|
|
530
|
-
const bind = p.bind;
|
|
531
|
-
const setter = bind ? `set${bind.charAt(0).toUpperCase() + bind.slice(1)}` : 'setValue';
|
|
532
|
-
ctx.isClient = true; // onChange requires 'use client'
|
|
533
|
-
ctx.lines.push(`${indent}<label className="relative inline-flex items-center cursor-pointer">`);
|
|
534
|
-
ctx.lines.push(`${indent} <input type="checkbox" className="sr-only peer" checked={${bind || 'value'}} onChange={(e) => ${setter}(e.target.checked)} />`);
|
|
535
|
-
ctx.lines.push(`${indent} <div className="w-11 h-6 bg-zinc-700 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-orange-600" />`);
|
|
536
|
-
ctx.lines.push(`${indent}</label>`);
|
|
537
|
-
}
|
|
538
|
-
function renderOnHandler(node, ctx) {
|
|
539
|
-
const p = getProps(node);
|
|
540
|
-
const event = (p.event || p.name);
|
|
541
|
-
const handlerRef = p.handler;
|
|
542
|
-
const key = p.key;
|
|
543
|
-
const isAsync = p.async === 'true' || p.async === true;
|
|
544
|
-
const handlerChild = (node.children || []).find(c => c.type === 'handler');
|
|
545
|
-
const code = handlerChild ? (getProps(handlerChild).code || '') : '';
|
|
546
|
-
if (handlerRef && !code)
|
|
547
|
-
return;
|
|
548
|
-
ctx.isClient = true; // event handlers require 'use client'
|
|
549
|
-
const fnName = handlerRef || `handle${event.charAt(0).toUpperCase() + event.slice(1)}`;
|
|
550
|
-
const asyncKw = isAsync ? 'async ' : '';
|
|
551
|
-
const paramType = event === 'submit' ? 'e: React.FormEvent'
|
|
552
|
-
: event === 'click' ? 'e: React.MouseEvent'
|
|
553
|
-
: event === 'change' ? 'e: React.ChangeEvent'
|
|
554
|
-
: event === 'key' || event === 'keydown' || event === 'keyup' ? 'e: React.KeyboardEvent'
|
|
555
|
-
: event === 'focus' || event === 'blur' ? 'e: React.FocusEvent'
|
|
556
|
-
: event === 'scroll' ? 'e: React.UIEvent'
|
|
557
|
-
: `e: React.SyntheticEvent`;
|
|
558
|
-
const keyGuard = key ? ` if (e.key !== '${key}') return;\n` : '';
|
|
559
|
-
addNamedImport(ctx, 'react', 'useCallback');
|
|
560
|
-
let block = ` const ${fnName} = useCallback(${asyncKw}(${paramType}) => {\n`;
|
|
561
|
-
if (keyGuard)
|
|
562
|
-
block += keyGuard;
|
|
563
|
-
if (code) {
|
|
564
|
-
for (const line of code.split('\n')) {
|
|
565
|
-
block += ` ${line}\n`;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
block += ` }, []);\n`;
|
|
569
|
-
ctx.bodyLines.push(block);
|
|
570
|
-
if (event === 'key' || event === 'keydown' || event === 'keyup') {
|
|
571
|
-
addNamedImport(ctx, 'react', 'useEffect');
|
|
572
|
-
const domEvent = event === 'key' ? 'keydown' : event;
|
|
573
|
-
let effect = ` useEffect(() => {\n`;
|
|
574
|
-
effect += ` const listener = (e: KeyboardEvent) => ${fnName}(e as unknown as React.KeyboardEvent);\n`;
|
|
575
|
-
effect += ` window.addEventListener('${domEvent}', listener);\n`;
|
|
576
|
-
effect += ` return () => window.removeEventListener('${domEvent}', listener);\n`;
|
|
577
|
-
effect += ` }, [${fnName}]);\n`;
|
|
578
|
-
ctx.bodyLines.push(effect);
|
|
579
|
-
}
|
|
580
|
-
if (event === 'resize') {
|
|
581
|
-
addNamedImport(ctx, 'react', 'useEffect');
|
|
582
|
-
let effect = ` useEffect(() => {\n`;
|
|
583
|
-
effect += ` window.addEventListener('resize', ${fnName});\n`;
|
|
584
|
-
effect += ` return () => window.removeEventListener('resize', ${fnName});\n`;
|
|
585
|
-
effect += ` }, [${fnName}]);\n`;
|
|
586
|
-
ctx.bodyLines.push(effect);
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
function renderTableCell(node, ctx, indent, tag) {
|
|
590
|
-
const p = getProps(node);
|
|
591
|
-
const tw = twClasses(node, ctx);
|
|
592
|
-
const rawValue = p.value;
|
|
593
|
-
if (isExpr(rawValue)) {
|
|
594
|
-
ctx.lines.push(`${indent}<${tag}${tw}>{${rawValue.code}}</${tag}>`);
|
|
595
|
-
}
|
|
596
|
-
else if (rawValue) {
|
|
597
|
-
ctx.lines.push(`${indent}<${tag}${tw}>${escapeJsxText(rawValue)}</${tag}>`);
|
|
598
|
-
}
|
|
599
|
-
else if (node.children && node.children.length > 0) {
|
|
600
|
-
ctx.lines.push(`${indent}<${tag}${tw}>`);
|
|
601
|
-
renderChildren(node, ctx, indent);
|
|
602
|
-
ctx.lines.push(`${indent}</${tag}>`);
|
|
603
|
-
}
|
|
604
|
-
else {
|
|
605
|
-
ctx.lines.push(`${indent}<${tag}${tw} />`);
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
function renderGrid(node, ctx, indent) {
|
|
609
|
-
const p = getProps(node);
|
|
610
|
-
const cols = parseInt(String(p.cols || 1), 10) || 1;
|
|
611
|
-
const gap = parseInt(String(p.gap || 16), 10) || 16;
|
|
612
|
-
ctx.lines.push(`${indent}<div className="grid grid-cols-1 md:grid-cols-${cols} gap-${Math.round(gap / 4)}">`);
|
|
613
|
-
renderChildren(node, ctx, indent);
|
|
614
|
-
ctx.lines.push(`${indent}</div>`);
|
|
615
|
-
}
|
|
616
|
-
const SVG_ICONS = {
|
|
617
|
-
home: '<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>',
|
|
618
|
-
plus: '<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/>',
|
|
619
|
-
chart: '<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>',
|
|
620
|
-
search: '<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>',
|
|
621
|
-
settings: '<circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>',
|
|
622
|
-
heart: '<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>',
|
|
623
|
-
profile: '<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
|
|
624
|
-
check: '<polyline points="20 6 9 17 4 12"/>',
|
|
625
|
-
x: '<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>',
|
|
626
|
-
arrow: '<line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/>',
|
|
627
|
-
};
|
|
628
|
-
/** Convert raw HTML-style SVG attributes to JSX-safe syntax. */
|
|
629
|
-
function htmlAttrsToJsx(html) {
|
|
630
|
-
// Convert unquoted numeric attrs: width=52 → width={52}
|
|
631
|
-
// Convert unquoted string attrs: fill=#E63946 → fill="#E63946"
|
|
632
|
-
return html.replace(/(\w+)=([^\s"'{/>]+)/g, (_match, key, val) => {
|
|
633
|
-
if (/^[\d.]+$/.test(val))
|
|
634
|
-
return `${key}={${val}}`;
|
|
635
|
-
return `${key}="${val}"`;
|
|
636
|
-
});
|
|
637
|
-
}
|
|
638
|
-
function renderSvg(node, ctx, indent) {
|
|
639
|
-
const p = getProps(node);
|
|
640
|
-
const icon = p.icon;
|
|
641
|
-
const size = parseInt(String(p.size || 24), 10) || 24;
|
|
642
|
-
if (icon) {
|
|
643
|
-
const inner = SVG_ICONS[icon] || `<circle cx="12" cy="12" r="4"/>`;
|
|
644
|
-
ctx.lines.push(`${indent}<svg xmlns="http://www.w3.org/2000/svg" width={${size}} height={${size}} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"${twClasses(node, ctx)}>${inner}</svg>`);
|
|
645
|
-
}
|
|
646
|
-
else {
|
|
647
|
-
// Custom SVG — only emit attributes the user explicitly set (no Feather defaults)
|
|
648
|
-
const viewBox = p.viewBox || '0 0 24 24';
|
|
649
|
-
const width = parseInt(String(p.width || size), 10) || size;
|
|
650
|
-
const height = parseInt(String(p.height || size), 10) || size;
|
|
651
|
-
const content = htmlAttrsToJsx(p.content || '');
|
|
652
|
-
const optAttrs = [];
|
|
653
|
-
if (p.fill)
|
|
654
|
-
optAttrs.push(`fill="${p.fill}"`);
|
|
655
|
-
if (p.stroke)
|
|
656
|
-
optAttrs.push(`stroke="${p.stroke}"`);
|
|
657
|
-
const extra = optAttrs.length ? ' ' + optAttrs.join(' ') : '';
|
|
658
|
-
ctx.lines.push(`${indent}<svg xmlns="http://www.w3.org/2000/svg" width={${width}} height={${height}} viewBox="${viewBox}"${extra}${twClasses(node, ctx)}>${content}</svg>`);
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
function renderConditional(node, ctx, indent) {
|
|
662
|
-
const cond = (getProps(node).if || 'true').replace(/&/g, ' && ').replace(/([a-zA-Z_]+)=([a-zA-Z_]+)/g, "$1 === '$2'");
|
|
663
|
-
ctx.lines.push(`${indent}{${cond} && (`);
|
|
664
|
-
ctx.lines.push(`${indent} <>`);
|
|
665
|
-
renderChildren(node, ctx, indent + ' ');
|
|
666
|
-
ctx.lines.push(`${indent} </>`);
|
|
667
|
-
ctx.lines.push(`${indent})}`);
|
|
668
|
-
}
|
|
669
|
-
function renderComponent(node, ctx, indent) {
|
|
670
|
-
const p = getProps(node);
|
|
671
|
-
const ref = (p.ref || p.name);
|
|
672
|
-
if (!ref)
|
|
673
|
-
return;
|
|
674
|
-
ctx.componentImports.add(ref);
|
|
675
|
-
const hasOnChange = 'onChange' in p;
|
|
676
|
-
const attrs = [];
|
|
677
|
-
for (const [k, v] of Object.entries(p)) {
|
|
678
|
-
if (['ref', 'name', 'styles', 'pseudoStyles', 'themeRefs'].includes(k))
|
|
679
|
-
continue;
|
|
680
|
-
if (k === 'bind') {
|
|
681
|
-
attrs.push(`value={${v}}`);
|
|
682
|
-
if (!hasOnChange)
|
|
683
|
-
attrs.push(`onChange={set${v.charAt(0).toUpperCase() + v.slice(1)}}`);
|
|
684
|
-
}
|
|
685
|
-
else if (k === 'onChange')
|
|
686
|
-
attrs.push(`onChange={${v}}`);
|
|
687
|
-
else if (k === 'props') {
|
|
688
|
-
for (const pn of v.split(','))
|
|
689
|
-
attrs.push(`${pn.trim()}={${pn.trim()}}`);
|
|
690
|
-
}
|
|
691
|
-
else if (k === 'disabled')
|
|
692
|
-
attrs.push(`disabled={${v.replace(/&/g, ' && ').replace(/([a-zA-Z_]+)=([a-zA-Z_]+)/g, "$1 === '$2'")}}`);
|
|
693
|
-
else if (k === 'default')
|
|
694
|
-
attrs.push(`defaultValue={${JSON.stringify(v)}}`);
|
|
695
|
-
else
|
|
696
|
-
attrs.push(`${k}={${JSON.stringify(v)}}`);
|
|
697
|
-
}
|
|
698
|
-
const attrStr = attrs.length ? ' ' + attrs.join(' ') : '';
|
|
699
|
-
if (node.children && node.children.length > 0) {
|
|
700
|
-
ctx.lines.push(`${indent}<${ref}${attrStr}>`);
|
|
701
|
-
renderChildren(node, ctx, indent);
|
|
702
|
-
ctx.lines.push(`${indent}</${ref}>`);
|
|
703
|
-
}
|
|
704
|
-
else {
|
|
705
|
-
ctx.lines.push(`${indent}<${ref}${attrStr} />`);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
function renderProgress(node, ctx, indent) {
|
|
709
|
-
const p = getProps(node);
|
|
710
|
-
const current = Number(p.current || 0), target = Number(p.target || 100);
|
|
711
|
-
const pct = Math.round((current / target) * 100);
|
|
712
|
-
ctx.lines.push(`${indent}<div className="mb-3">`);
|
|
713
|
-
ctx.lines.push(`${indent} <div className="flex justify-between text-sm mb-1"><span>${escapeJsxText(String(p.label || ''))}</span><span>${current}/${target} ${escapeJsxText(String(p.unit || ''))}</span></div>`);
|
|
714
|
-
ctx.lines.push(`${indent} <div className="h-2 bg-zinc-700 rounded-full overflow-hidden"><div className="h-full rounded-full bg-[${p.color || '#007AFF'}]" style={{ width: '${pct}%' }} /></div>`);
|
|
715
|
-
ctx.lines.push(`${indent}</div>`);
|
|
716
|
-
}
|
|
717
|
-
// ── Next.js 15 production pattern renderers ─────────────────────────────
|
|
718
|
-
function renderGenerateMetadata(node, ctx) {
|
|
719
|
-
// Collect handler code from children
|
|
720
|
-
let handlerCode = '';
|
|
721
|
-
if (node.children) {
|
|
722
|
-
for (const child of node.children) {
|
|
723
|
-
const cp = getProps(child);
|
|
724
|
-
if (child.type === 'handler' && cp.code) {
|
|
725
|
-
handlerCode = cp.code;
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
// Also check inline code prop
|
|
730
|
-
const p = getProps(node);
|
|
731
|
-
if (p.code)
|
|
732
|
-
handlerCode = p.code;
|
|
733
|
-
ctx.generateMetadataInfo = { handlerCode };
|
|
734
|
-
}
|
|
735
|
-
function renderNotFound(node, ctx, _indent) {
|
|
736
|
-
addNamedImport(ctx, 'next/navigation', 'notFound');
|
|
737
|
-
const p = getProps(node);
|
|
738
|
-
const condition = p.if;
|
|
739
|
-
if (condition) {
|
|
740
|
-
ctx.bodyLines.push(` if (${exprCode(condition, 'true')}) { notFound(); }`);
|
|
741
|
-
}
|
|
742
|
-
else {
|
|
743
|
-
ctx.bodyLines.push(` notFound();`);
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
function renderRedirect(node, ctx, _indent) {
|
|
747
|
-
addNamedImport(ctx, 'next/navigation', 'redirect');
|
|
748
|
-
const p = getProps(node);
|
|
749
|
-
const to = p.to || '/';
|
|
750
|
-
ctx.bodyLines.push(` redirect('${to}');`);
|
|
751
|
-
}
|
|
752
|
-
function renderImport(node, ctx) {
|
|
753
|
-
const p = getProps(node);
|
|
754
|
-
const name = p.name;
|
|
755
|
-
const from = p.from;
|
|
756
|
-
const isDefault = p.default === 'true' || p.default === true;
|
|
757
|
-
if (name && from) {
|
|
758
|
-
if (isDefault) {
|
|
759
|
-
addDefaultImport(ctx, from, name);
|
|
760
|
-
}
|
|
761
|
-
else {
|
|
762
|
-
// Support comma-separated named imports: name=Foo,Bar,Baz
|
|
763
|
-
const names = name.split(',').map(n => n.trim()).filter(Boolean);
|
|
764
|
-
for (const n of names) {
|
|
765
|
-
addNamedImport(ctx, from, n);
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
function renderFetchNode(node, ctx) {
|
|
771
|
-
const p = getProps(node);
|
|
772
|
-
const name = p.name || 'data';
|
|
773
|
-
const url = p.url || '/api/data';
|
|
774
|
-
const options = p.options;
|
|
775
|
-
ctx.fetchCalls.push({ name, url, options });
|
|
776
|
-
}
|
|
3
|
+
import { renderAndAssemble } from './nextjs-assembler.js';
|
|
4
|
+
import { routeToPath } from './nextjs-style.js';
|
|
5
|
+
import { planStructure } from './structure.js';
|
|
777
6
|
// ── Main export ─────────────────────────────────────────────────────────
|
|
778
7
|
export function transpileNextjs(root, config) {
|
|
779
8
|
// Structured output path
|
|
@@ -783,212 +12,14 @@ export function transpileNextjs(root, config) {
|
|
|
783
12
|
return _transpileNextjsStructured(root, config, plan);
|
|
784
13
|
}
|
|
785
14
|
}
|
|
786
|
-
// Flat output path (default
|
|
15
|
+
// Flat output path (default)
|
|
787
16
|
return _transpileNextjsInner(root, config);
|
|
788
17
|
}
|
|
18
|
+
// ── Flat output ─────────────────────────────────────────────────────────
|
|
789
19
|
function _transpileNextjsInner(root, config) {
|
|
790
|
-
const ctx = {
|
|
791
|
-
lines: [],
|
|
792
|
-
sourceMap: [],
|
|
793
|
-
imports: new Map(),
|
|
794
|
-
componentImports: new Set(),
|
|
795
|
-
isClient: false,
|
|
796
|
-
isAsync: false,
|
|
797
|
-
metadata: null,
|
|
798
|
-
generateMetadataInfo: null,
|
|
799
|
-
fetchCalls: [],
|
|
800
|
-
bodyLines: [],
|
|
801
|
-
stateDecls: [],
|
|
802
|
-
logicBlocks: [],
|
|
803
|
-
colors: config?.colors,
|
|
804
|
-
twProfile: config?.frameworkVersions ? buildTailwindProfile(config.frameworkVersions) : undefined,
|
|
805
|
-
njProfile: config?.frameworkVersions ? buildNextjsProfile(config.frameworkVersions) : undefined,
|
|
806
|
-
};
|
|
807
|
-
// Check root for client flag
|
|
808
20
|
const rootProps = getProps(root);
|
|
809
|
-
if (rootProps.client === 'true' || rootProps.client === true)
|
|
810
|
-
ctx.isClient = true;
|
|
811
|
-
if (rootProps.async === 'true' || rootProps.async === true)
|
|
812
|
-
ctx.isAsync = true;
|
|
813
|
-
// renderNode already handles metadata nodes in the switch case
|
|
814
|
-
renderNode(root, ctx, ' ');
|
|
815
|
-
// If there are fetch calls, mark page as async
|
|
816
|
-
if (ctx.fetchCalls.length > 0)
|
|
817
|
-
ctx.isAsync = true;
|
|
818
|
-
// Client components cannot be async in Next.js — client wins, drop server-only patterns
|
|
819
|
-
if (ctx.isClient && ctx.isAsync) {
|
|
820
|
-
ctx.isAsync = false;
|
|
821
|
-
ctx.fetchCalls = [];
|
|
822
|
-
}
|
|
823
21
|
const name = rootProps.name || 'Page';
|
|
824
|
-
const
|
|
825
|
-
const isLoading = root.type === 'loading';
|
|
826
|
-
const isError = root.type === 'error';
|
|
827
|
-
const code = [];
|
|
828
|
-
// 'use client' directive
|
|
829
|
-
if (ctx.isClient) {
|
|
830
|
-
code.push(`'use client';`);
|
|
831
|
-
code.push('');
|
|
832
|
-
}
|
|
833
|
-
// Metadata type import for generateMetadata
|
|
834
|
-
if (ctx.generateMetadataInfo && !ctx.isClient) {
|
|
835
|
-
addNamedImport(ctx, 'next', 'Metadata', true);
|
|
836
|
-
}
|
|
837
|
-
// Component imports → add to unified import map (skip if already imported explicitly)
|
|
838
|
-
const uiLib = config?.components?.uiLibrary ?? '@/components/ui';
|
|
839
|
-
const compRoot = config?.components?.componentRoot ?? '@/components';
|
|
840
|
-
if (ctx.componentImports.size > 0) {
|
|
841
|
-
// Collect names already imported via explicit import nodes
|
|
842
|
-
const alreadyImported = new Set();
|
|
843
|
-
for (const spec of ctx.imports.values()) {
|
|
844
|
-
if (spec.defaultImport)
|
|
845
|
-
alreadyImported.add(spec.defaultImport);
|
|
846
|
-
for (const n of spec.namedImports)
|
|
847
|
-
alreadyImported.add(n);
|
|
848
|
-
}
|
|
849
|
-
const uiImports = [...ctx.componentImports].filter(c => ['Icon', 'Button'].includes(c) && !alreadyImported.has(c));
|
|
850
|
-
const others = [...ctx.componentImports].filter(c => !['Icon', 'Button'].includes(c) && !alreadyImported.has(c));
|
|
851
|
-
for (const name of uiImports)
|
|
852
|
-
addNamedImport(ctx, uiLib, name);
|
|
853
|
-
for (const name of others)
|
|
854
|
-
addDefaultImport(ctx, `${compRoot}/${name}`, name);
|
|
855
|
-
}
|
|
856
|
-
// Static metadata needs Metadata type
|
|
857
|
-
if (ctx.metadata && !ctx.generateMetadataInfo) {
|
|
858
|
-
addNamedImport(ctx, 'next', 'Metadata', true);
|
|
859
|
-
}
|
|
860
|
-
// State requires useState import
|
|
861
|
-
if (ctx.stateDecls.length > 0) {
|
|
862
|
-
addNamedImport(ctx, 'react', 'useState');
|
|
863
|
-
}
|
|
864
|
-
// Detect hook imports from logic blocks before emitting imports
|
|
865
|
-
for (const block of ctx.logicBlocks) {
|
|
866
|
-
if (block.includes('useEffect'))
|
|
867
|
-
addNamedImport(ctx, 'react', 'useEffect');
|
|
868
|
-
if (block.includes('useCallback'))
|
|
869
|
-
addNamedImport(ctx, 'react', 'useCallback');
|
|
870
|
-
if (block.includes('useMemo'))
|
|
871
|
-
addNamedImport(ctx, 'react', 'useMemo');
|
|
872
|
-
if (block.includes('useRef'))
|
|
873
|
-
addNamedImport(ctx, 'react', 'useRef');
|
|
874
|
-
}
|
|
875
|
-
// Emit all imports (unified, sorted)
|
|
876
|
-
code.push(...emitImports(ctx));
|
|
877
|
-
if (code.length > 0 && code[code.length - 1] !== '')
|
|
878
|
-
code.push('');
|
|
879
|
-
// Metadata export — static metadata
|
|
880
|
-
// Note: In Next.js, metadata must be in a server component. If the page is client-side,
|
|
881
|
-
// emit it anyway (user may split into layout.tsx or remove client=true).
|
|
882
|
-
if (ctx.metadata) {
|
|
883
|
-
const useSatisfies = ctx.njProfile?.outputRules.metadataStyle === 'satisfies';
|
|
884
|
-
code.push(useSatisfies ? `export const metadata = {` : `export const metadata: Metadata = {`);
|
|
885
|
-
for (const [k, v] of Object.entries(ctx.metadata)) {
|
|
886
|
-
code.push(` ${k}: '${escapeJsString(v)}',`);
|
|
887
|
-
}
|
|
888
|
-
code.push(useSatisfies ? `} satisfies Metadata;` : `};`);
|
|
889
|
-
code.push('');
|
|
890
|
-
}
|
|
891
|
-
// generateMetadata export (server components only)
|
|
892
|
-
if (ctx.generateMetadataInfo && !ctx.isClient) {
|
|
893
|
-
const usePromiseParams = !ctx.njProfile || ctx.njProfile.major >= 15;
|
|
894
|
-
const paramsType = usePromiseParams
|
|
895
|
-
? '{ params }: { params: Promise<Record<string, string>> }'
|
|
896
|
-
: '{ params }: { params: Record<string, string> }';
|
|
897
|
-
code.push('');
|
|
898
|
-
code.push(`export async function generateMetadata(${paramsType}): Promise<Metadata> {`);
|
|
899
|
-
if (ctx.generateMetadataInfo.handlerCode) {
|
|
900
|
-
// Split handler code by newlines and emit each line as-is
|
|
901
|
-
const lines = ctx.generateMetadataInfo.handlerCode.split('\n').map(s => s.trim()).filter(Boolean);
|
|
902
|
-
for (const line of lines) {
|
|
903
|
-
code.push(` ${line}`);
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
else {
|
|
907
|
-
if (usePromiseParams) {
|
|
908
|
-
code.push(` const resolvedParams = await params;`);
|
|
909
|
-
code.push(` return { title: resolvedParams.slug ?? '' };`);
|
|
910
|
-
}
|
|
911
|
-
else {
|
|
912
|
-
code.push(` return { title: params.slug ?? '' };`);
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
code.push(`}`);
|
|
916
|
-
code.push('');
|
|
917
|
-
}
|
|
918
|
-
// Component
|
|
919
|
-
if (isLayout) {
|
|
920
|
-
code.push(`export default function ${name}({ children }: { children: React.ReactNode }) {`);
|
|
921
|
-
}
|
|
922
|
-
else if (isError) {
|
|
923
|
-
code.push(`export default function ${name}({ error, reset }: { error: Error; reset: () => void }) {`);
|
|
924
|
-
}
|
|
925
|
-
else if (ctx.isAsync) {
|
|
926
|
-
const usePromiseParams = !ctx.njProfile || ctx.njProfile.major >= 15;
|
|
927
|
-
if (usePromiseParams) {
|
|
928
|
-
code.push(`export default async function ${name}(props: { params: Promise<Record<string, string>> }) {`);
|
|
929
|
-
code.push(` const params = await props.params;`);
|
|
930
|
-
}
|
|
931
|
-
else {
|
|
932
|
-
code.push(`export default async function ${name}({ params }: { params: Record<string, string> }) {`);
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
else {
|
|
936
|
-
code.push(`export default function ${name}() {`);
|
|
937
|
-
}
|
|
938
|
-
// Emit fetch calls (inside async function body, before return)
|
|
939
|
-
for (const fc of ctx.fetchCalls) {
|
|
940
|
-
if (fc.options) {
|
|
941
|
-
code.push(` const ${fc.name} = await fetch('${fc.url}', ${fc.options}).then(r => r.json());`);
|
|
942
|
-
}
|
|
943
|
-
else {
|
|
944
|
-
code.push(` const ${fc.name} = await fetch('${fc.url}').then(r => r.json());`);
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
// Emit useState declarations
|
|
948
|
-
for (const s of ctx.stateDecls) {
|
|
949
|
-
const setter = `set${s.name.charAt(0).toUpperCase() + s.name.slice(1)}`;
|
|
950
|
-
const stateNode = root.children?.find(c => c.type === 'state' && c.props?.name === s.name);
|
|
951
|
-
const initProp = stateNode?.props?.initial;
|
|
952
|
-
const isExprInit = typeof initProp === 'object' && initProp !== null && '__expr' in initProp;
|
|
953
|
-
let initVal;
|
|
954
|
-
if (isExprInit) {
|
|
955
|
-
initVal = initProp.code;
|
|
956
|
-
}
|
|
957
|
-
else if (s.initial === 'true' || s.initial === 'false') {
|
|
958
|
-
initVal = s.initial;
|
|
959
|
-
}
|
|
960
|
-
else if (s.initial === '' || s.initial === "''") {
|
|
961
|
-
initVal = "''";
|
|
962
|
-
}
|
|
963
|
-
else if (!isNaN(Number(s.initial)) && s.initial !== '') {
|
|
964
|
-
initVal = s.initial;
|
|
965
|
-
}
|
|
966
|
-
else {
|
|
967
|
-
initVal = `'${s.initial}'`;
|
|
968
|
-
}
|
|
969
|
-
code.push(` const [${s.name}, ${setter}] = useState(${initVal});`);
|
|
970
|
-
}
|
|
971
|
-
// Emit logic blocks & detect hook imports
|
|
972
|
-
for (const block of ctx.logicBlocks) {
|
|
973
|
-
code.push(` ${block}`);
|
|
974
|
-
if (block.includes('useEffect'))
|
|
975
|
-
addNamedImport(ctx, 'react', 'useEffect');
|
|
976
|
-
if (block.includes('useCallback'))
|
|
977
|
-
addNamedImport(ctx, 'react', 'useCallback');
|
|
978
|
-
if (block.includes('useMemo'))
|
|
979
|
-
addNamedImport(ctx, 'react', 'useMemo');
|
|
980
|
-
if (block.includes('useRef'))
|
|
981
|
-
addNamedImport(ctx, 'react', 'useRef');
|
|
982
|
-
}
|
|
983
|
-
// Emit body lines (notFound, redirect calls)
|
|
984
|
-
for (const line of ctx.bodyLines) {
|
|
985
|
-
code.push(line);
|
|
986
|
-
}
|
|
987
|
-
code.push(' return (');
|
|
988
|
-
code.push(...ctx.lines);
|
|
989
|
-
code.push(' );');
|
|
990
|
-
code.push('}');
|
|
991
|
-
const output = code.join('\n');
|
|
22
|
+
const { ctx, code: output } = renderAndAssemble(root, config, name, (stateName) => root.children?.find((c) => c.type === 'state' && c.props?.name === stateName));
|
|
992
23
|
const irText = serializeIR(root);
|
|
993
24
|
const irTokenCount = countTokens(irText);
|
|
994
25
|
const tsTokenCount = countTokens(output);
|
|
@@ -996,7 +27,10 @@ function _transpileNextjsInner(root, config) {
|
|
|
996
27
|
// Determine output filename convention (route-aware)
|
|
997
28
|
const route = rootProps.route;
|
|
998
29
|
const segment = rootProps.segment;
|
|
999
|
-
const routePrefix = route ? routeToPath(route, segment) :
|
|
30
|
+
const routePrefix = route ? routeToPath(route, segment) : segment ? routeToPath('', segment) : '';
|
|
31
|
+
const isLayout = root.type === 'layout';
|
|
32
|
+
const isLoading = root.type === 'loading';
|
|
33
|
+
const isError = root.type === 'error';
|
|
1000
34
|
const files = [];
|
|
1001
35
|
if (isLayout)
|
|
1002
36
|
files.push({ path: `${routePrefix}layout.tsx`, content: output });
|
|
@@ -1011,7 +45,7 @@ function _transpileNextjsInner(root, config) {
|
|
|
1011
45
|
const CONSUMED = new Set(['state', 'logic', 'on', 'theme', 'handler']);
|
|
1012
46
|
for (const child of root.children || []) {
|
|
1013
47
|
if (CONSUMED.has(child.type))
|
|
1014
|
-
accountNode(accounted, child, 'consumed', child.type
|
|
48
|
+
accountNode(accounted, child, 'consumed', `${child.type} pre-pass`, true);
|
|
1015
49
|
}
|
|
1016
50
|
return {
|
|
1017
51
|
code: output,
|
|
@@ -1025,200 +59,13 @@ function _transpileNextjsInner(root, config) {
|
|
|
1025
59
|
}
|
|
1026
60
|
// ── Structured output ────────────────────────────────────────────────────
|
|
1027
61
|
function _renderNextjsFile(file, config) {
|
|
1028
|
-
const ctx = {
|
|
1029
|
-
lines: [],
|
|
1030
|
-
sourceMap: [],
|
|
1031
|
-
imports: new Map(),
|
|
1032
|
-
componentImports: new Set(),
|
|
1033
|
-
isClient: false,
|
|
1034
|
-
isAsync: false,
|
|
1035
|
-
metadata: null,
|
|
1036
|
-
generateMetadataInfo: null,
|
|
1037
|
-
fetchCalls: [],
|
|
1038
|
-
bodyLines: [],
|
|
1039
|
-
stateDecls: [],
|
|
1040
|
-
logicBlocks: [],
|
|
1041
|
-
colors: config.colors,
|
|
1042
|
-
twProfile: config.frameworkVersions ? buildTailwindProfile(config.frameworkVersions) : undefined,
|
|
1043
|
-
njProfile: config.frameworkVersions ? buildNextjsProfile(config.frameworkVersions) : undefined,
|
|
1044
|
-
};
|
|
1045
62
|
const rootNode = file.rootNode;
|
|
1046
|
-
const
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
// renderNode already handles metadata nodes in the switch case
|
|
1053
|
-
renderNode(rootNode, ctx, ' ');
|
|
1054
|
-
const name = file.componentName || rootProps.name || 'Component';
|
|
1055
|
-
const isLayout = rootNode.type === 'layout';
|
|
1056
|
-
const isError = rootNode.type === 'error';
|
|
1057
|
-
// Client components cannot be async in Next.js — client wins, drop server-only patterns
|
|
1058
|
-
if (ctx.isClient && ctx.isAsync) {
|
|
1059
|
-
ctx.isAsync = false;
|
|
1060
|
-
ctx.fetchCalls = [];
|
|
1061
|
-
}
|
|
1062
|
-
const code = [];
|
|
1063
|
-
if (ctx.isClient) {
|
|
1064
|
-
code.push(`'use client';`);
|
|
1065
|
-
code.push('');
|
|
1066
|
-
}
|
|
1067
|
-
const uiLib = config.components?.uiLibrary ?? '@/components/ui';
|
|
1068
|
-
const compRoot = config.components?.componentRoot ?? '@/components';
|
|
1069
|
-
if (ctx.componentImports.size > 0) {
|
|
1070
|
-
const alreadyImported = new Set();
|
|
1071
|
-
for (const spec of ctx.imports.values()) {
|
|
1072
|
-
if (spec.defaultImport)
|
|
1073
|
-
alreadyImported.add(spec.defaultImport);
|
|
1074
|
-
for (const n of spec.namedImports)
|
|
1075
|
-
alreadyImported.add(n);
|
|
1076
|
-
}
|
|
1077
|
-
const uiImports = [...ctx.componentImports].filter(c => ['Icon', 'Button'].includes(c) && !alreadyImported.has(c));
|
|
1078
|
-
const others = [...ctx.componentImports].filter(c => !['Icon', 'Button'].includes(c) && !alreadyImported.has(c));
|
|
1079
|
-
for (const name of uiImports)
|
|
1080
|
-
addNamedImport(ctx, uiLib, name);
|
|
1081
|
-
for (const name of others)
|
|
1082
|
-
addDefaultImport(ctx, `${compRoot}/${name}`, name);
|
|
1083
|
-
}
|
|
1084
|
-
// Metadata type import for generateMetadata
|
|
1085
|
-
if (ctx.generateMetadataInfo && !ctx.isClient) {
|
|
1086
|
-
addNamedImport(ctx, 'next', 'Metadata', true);
|
|
1087
|
-
}
|
|
1088
|
-
if (ctx.metadata && !ctx.generateMetadataInfo) {
|
|
1089
|
-
addNamedImport(ctx, 'next', 'Metadata', true);
|
|
1090
|
-
}
|
|
1091
|
-
// If there are fetch calls, mark page as async
|
|
1092
|
-
if (ctx.fetchCalls.length > 0)
|
|
1093
|
-
ctx.isAsync = true;
|
|
1094
|
-
// State requires useState import
|
|
1095
|
-
if (ctx.stateDecls.length > 0) {
|
|
1096
|
-
addNamedImport(ctx, 'react', 'useState');
|
|
1097
|
-
}
|
|
1098
|
-
// Detect hook imports from logic blocks before emitting imports
|
|
1099
|
-
for (const block of ctx.logicBlocks) {
|
|
1100
|
-
if (block.includes('useEffect'))
|
|
1101
|
-
addNamedImport(ctx, 'react', 'useEffect');
|
|
1102
|
-
if (block.includes('useCallback'))
|
|
1103
|
-
addNamedImport(ctx, 'react', 'useCallback');
|
|
1104
|
-
if (block.includes('useMemo'))
|
|
1105
|
-
addNamedImport(ctx, 'react', 'useMemo');
|
|
1106
|
-
if (block.includes('useRef'))
|
|
1107
|
-
addNamedImport(ctx, 'react', 'useRef');
|
|
1108
|
-
}
|
|
1109
|
-
code.push(...emitImports(ctx));
|
|
1110
|
-
if (code.length > 0 && code[code.length - 1] !== '')
|
|
1111
|
-
code.push('');
|
|
1112
|
-
// Metadata — emit even for client components (user may split into layout.tsx)
|
|
1113
|
-
if (ctx.metadata) {
|
|
1114
|
-
const useSatisfies = ctx.njProfile?.outputRules.metadataStyle === 'satisfies';
|
|
1115
|
-
code.push(useSatisfies ? `export const metadata = {` : `export const metadata: Metadata = {`);
|
|
1116
|
-
for (const [k, v] of Object.entries(ctx.metadata)) {
|
|
1117
|
-
code.push(` ${k}: '${escapeJsString(v)}',`);
|
|
1118
|
-
}
|
|
1119
|
-
code.push(useSatisfies ? `} satisfies Metadata;` : `};`);
|
|
1120
|
-
code.push('');
|
|
1121
|
-
}
|
|
1122
|
-
// generateMetadata export (server components only)
|
|
1123
|
-
if (ctx.generateMetadataInfo && !ctx.isClient) {
|
|
1124
|
-
const usePromiseParams = !ctx.njProfile || ctx.njProfile.major >= 15;
|
|
1125
|
-
const paramsType = usePromiseParams
|
|
1126
|
-
? '{ params }: { params: Promise<Record<string, string>> }'
|
|
1127
|
-
: '{ params }: { params: Record<string, string> }';
|
|
1128
|
-
code.push('');
|
|
1129
|
-
code.push(`export async function generateMetadata(${paramsType}): Promise<Metadata> {`);
|
|
1130
|
-
if (ctx.generateMetadataInfo.handlerCode) {
|
|
1131
|
-
const lines = ctx.generateMetadataInfo.handlerCode.split('\n').map(s => s.trim()).filter(Boolean);
|
|
1132
|
-
for (const line of lines) {
|
|
1133
|
-
code.push(` ${line}`);
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
else {
|
|
1137
|
-
if (usePromiseParams) {
|
|
1138
|
-
code.push(` const resolvedParams = await params;`);
|
|
1139
|
-
code.push(` return { title: resolvedParams.slug ?? '' };`);
|
|
1140
|
-
}
|
|
1141
|
-
else {
|
|
1142
|
-
code.push(` return { title: params.slug ?? '' };`);
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
code.push(`}`);
|
|
1146
|
-
code.push('');
|
|
1147
|
-
}
|
|
1148
|
-
if (isLayout) {
|
|
1149
|
-
code.push(`export default function ${name}({ children }: { children: React.ReactNode }) {`);
|
|
1150
|
-
}
|
|
1151
|
-
else if (isError) {
|
|
1152
|
-
code.push(`export default function ${name}({ error, reset }: { error: Error; reset: () => void }) {`);
|
|
1153
|
-
}
|
|
1154
|
-
else if (ctx.isAsync) {
|
|
1155
|
-
const usePromiseParams = !ctx.njProfile || ctx.njProfile.major >= 15;
|
|
1156
|
-
if (usePromiseParams) {
|
|
1157
|
-
code.push(`export default async function ${name}(props: { params: Promise<Record<string, string>> }) {`);
|
|
1158
|
-
code.push(` const params = await props.params;`);
|
|
1159
|
-
}
|
|
1160
|
-
else {
|
|
1161
|
-
code.push(`export default async function ${name}({ params }: { params: Record<string, string> }) {`);
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
else {
|
|
1165
|
-
code.push(`export default function ${name}() {`);
|
|
1166
|
-
}
|
|
1167
|
-
// Emit useState declarations
|
|
1168
|
-
for (const s of ctx.stateDecls) {
|
|
1169
|
-
const setter = `set${s.name.charAt(0).toUpperCase() + s.name.slice(1)}`;
|
|
1170
|
-
const rootNode = file.nodes[0];
|
|
1171
|
-
const stateNode = rootNode?.children?.find((c) => c.type === 'state' && c.props?.name === s.name);
|
|
1172
|
-
const initProp = stateNode?.props?.initial;
|
|
1173
|
-
const isExpr = typeof initProp === 'object' && initProp !== null && '__expr' in initProp;
|
|
1174
|
-
let initVal;
|
|
1175
|
-
if (isExpr) {
|
|
1176
|
-
initVal = initProp.code;
|
|
1177
|
-
}
|
|
1178
|
-
else if (s.initial === 'true' || s.initial === 'false') {
|
|
1179
|
-
initVal = s.initial;
|
|
1180
|
-
}
|
|
1181
|
-
else if (s.initial === '' || s.initial === "''") {
|
|
1182
|
-
initVal = "''";
|
|
1183
|
-
}
|
|
1184
|
-
else if (!isNaN(Number(s.initial)) && s.initial !== '') {
|
|
1185
|
-
initVal = s.initial;
|
|
1186
|
-
}
|
|
1187
|
-
else {
|
|
1188
|
-
initVal = `'${s.initial}'`;
|
|
1189
|
-
}
|
|
1190
|
-
code.push(` const [${s.name}, ${setter}] = useState(${initVal});`);
|
|
1191
|
-
}
|
|
1192
|
-
// Emit logic blocks & detect hook imports
|
|
1193
|
-
for (const block of ctx.logicBlocks) {
|
|
1194
|
-
code.push(` ${block}`);
|
|
1195
|
-
if (block.includes('useEffect'))
|
|
1196
|
-
addNamedImport(ctx, 'react', 'useEffect');
|
|
1197
|
-
if (block.includes('useCallback'))
|
|
1198
|
-
addNamedImport(ctx, 'react', 'useCallback');
|
|
1199
|
-
if (block.includes('useMemo'))
|
|
1200
|
-
addNamedImport(ctx, 'react', 'useMemo');
|
|
1201
|
-
if (block.includes('useRef'))
|
|
1202
|
-
addNamedImport(ctx, 'react', 'useRef');
|
|
1203
|
-
}
|
|
1204
|
-
// Emit fetch calls (inside async function body, before return)
|
|
1205
|
-
for (const fc of ctx.fetchCalls) {
|
|
1206
|
-
if (fc.options) {
|
|
1207
|
-
code.push(` const ${fc.name} = await fetch('${fc.url}', ${fc.options}).then(r => r.json());`);
|
|
1208
|
-
}
|
|
1209
|
-
else {
|
|
1210
|
-
code.push(` const ${fc.name} = await fetch('${fc.url}').then(r => r.json());`);
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
// Emit body lines (notFound, redirect calls)
|
|
1214
|
-
for (const line of ctx.bodyLines) {
|
|
1215
|
-
code.push(line);
|
|
1216
|
-
}
|
|
1217
|
-
code.push(' return (');
|
|
1218
|
-
code.push(...ctx.lines);
|
|
1219
|
-
code.push(' );');
|
|
1220
|
-
code.push('}');
|
|
1221
|
-
return code.join('\n');
|
|
63
|
+
const name = file.componentName || rootNode.props?.name || 'Component';
|
|
64
|
+
const { code } = renderAndAssemble(rootNode, config, name, (stateName) => {
|
|
65
|
+
const firstNode = file.nodes[0];
|
|
66
|
+
return firstNode?.children?.find((c) => c.type === 'state' && c.props?.name === stateName);
|
|
67
|
+
});
|
|
68
|
+
return code;
|
|
1222
69
|
}
|
|
1223
70
|
function _transpileNextjsStructured(root, config, plan) {
|
|
1224
71
|
const { entryCode, artifacts } = buildStructuredArtifacts(plan, (file, cfg) => _renderNextjsFile(file, cfg), root, config);
|
|
@@ -1228,8 +75,8 @@ function _transpileNextjsStructured(root, config, plan) {
|
|
|
1228
75
|
const tokenReduction = tsTokenCount > 0 ? Math.round((1 - irTokenCount / tsTokenCount) * 100) : 0;
|
|
1229
76
|
// Convert artifacts to NextFile[] for files property
|
|
1230
77
|
const files = artifacts
|
|
1231
|
-
.filter(a => a.path.endsWith('.tsx'))
|
|
1232
|
-
.map(a => ({ path: a.path, content: a.content }));
|
|
78
|
+
.filter((a) => a.path.endsWith('.tsx'))
|
|
79
|
+
.map((a) => ({ path: a.path, content: a.content }));
|
|
1233
80
|
return {
|
|
1234
81
|
code: entryCode,
|
|
1235
82
|
sourceMap: [],
|