@kernlang/react 2.0.0 → 3.1.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/dist/codegen-react.d.ts +4 -0
- package/dist/codegen-react.js +46 -15
- package/dist/codegen-react.js.map +1 -1
- package/dist/structure.js +7 -0
- package/dist/structure.js.map +1 -1
- package/dist/transpiler-nextjs.js +400 -28
- package/dist/transpiler-nextjs.js.map +1 -1
- package/dist/transpiler-tailwind.js +144 -38
- package/dist/transpiler-tailwind.js.map +1 -1
- package/dist/transpiler-web.js +0 -3
- package/dist/transpiler-web.js.map +1 -1
- package/package.json +6 -3
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { stylesToTailwind, colorToTw, countTokens, serializeIR,
|
|
1
|
+
import { stylesToTailwind, colorToTw, countTokens, serializeIR, escapeJsxText, escapeJsxAttr, escapeJsString, buildTailwindProfile, buildNextjsProfile, applyTailwindTokenRules, getProps, getStyles } from '@kernlang/core';
|
|
2
2
|
import { planStructure } from './structure.js';
|
|
3
3
|
import { buildStructuredArtifacts } from './artifact-utils.js';
|
|
4
|
-
function
|
|
5
|
-
function getStyles(node) { return getProps(node).styles || {}; }
|
|
4
|
+
function isExpr(v) { return typeof v === 'object' && v !== null && '__expr' in v; }
|
|
6
5
|
// ── Unified import helpers (from Codex) ──────────────────────────────────
|
|
7
6
|
function addDefaultImport(ctx, source, name) {
|
|
8
7
|
const spec = ctx.imports.get(source) || { namedImports: new Set(), typeOnlyImports: new Set() };
|
|
@@ -56,11 +55,53 @@ function emitImports(ctx) {
|
|
|
56
55
|
return lines;
|
|
57
56
|
}
|
|
58
57
|
function twClasses(node, ctx, extra = '') {
|
|
59
|
-
|
|
58
|
+
const styles = getStyles(node);
|
|
59
|
+
// Extract className pass-through (e.g. {className:doc.page} → className={doc.page})
|
|
60
|
+
const classNameRef = styles.className;
|
|
61
|
+
const inlineStyles = {};
|
|
62
|
+
const filteredStyles = {};
|
|
63
|
+
for (const [k, v] of Object.entries(styles)) {
|
|
64
|
+
if (k === 'className')
|
|
65
|
+
continue;
|
|
66
|
+
// CSS custom properties and complex values → inline style
|
|
67
|
+
if (v.includes('var(') || k === 'borderBottom' || k === 'background' || k === 'color' || k === 'fontFamily') {
|
|
68
|
+
inlineStyles[k] = v;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
filteredStyles[k] = v;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
let tw = stylesToTailwind(filteredStyles, ctx.colors);
|
|
60
75
|
if (ctx.twProfile)
|
|
61
76
|
tw = applyTailwindTokenRules(tw, ctx.twProfile);
|
|
62
77
|
const parts = [tw, extra].filter(Boolean);
|
|
63
|
-
|
|
78
|
+
const attrs = [];
|
|
79
|
+
if (classNameRef) {
|
|
80
|
+
// className is a JS expression reference like doc.page
|
|
81
|
+
if (parts.length > 0) {
|
|
82
|
+
attrs.push(` className={\`\${${classNameRef}} ${parts.join(' ')}\`}`);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
attrs.push(` className={${classNameRef}}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else if (parts.length > 0) {
|
|
89
|
+
attrs.push(` className="${parts.join(' ')}"`);
|
|
90
|
+
}
|
|
91
|
+
if (Object.keys(inlineStyles).length > 0) {
|
|
92
|
+
const pairs = Object.entries(inlineStyles).map(([k, v]) => `${k}: '${v}'`);
|
|
93
|
+
attrs.push(` style={{ ${pairs.join(', ')} }}`);
|
|
94
|
+
}
|
|
95
|
+
return attrs.join('');
|
|
96
|
+
}
|
|
97
|
+
// ── Route path helper ────────────────────────────────────────────────────
|
|
98
|
+
function routeToPath(route, segment) {
|
|
99
|
+
// Normalize: strip leading/trailing slashes
|
|
100
|
+
const normalized = route.replace(/^\/+|\/+$/g, '');
|
|
101
|
+
const parts = normalized ? normalized.split('/') : [];
|
|
102
|
+
if (segment)
|
|
103
|
+
parts.push(segment);
|
|
104
|
+
return parts.length > 0 ? parts.join('/') + '/' : '';
|
|
64
105
|
}
|
|
65
106
|
// ── Node renderers ──────────────────────────────────────────────────────
|
|
66
107
|
function renderNode(node, ctx, indent) {
|
|
@@ -114,8 +155,12 @@ function renderNode(node, ctx, indent) {
|
|
|
114
155
|
case 'image':
|
|
115
156
|
renderImage(node, ctx, indent);
|
|
116
157
|
break;
|
|
158
|
+
case 'codeblock':
|
|
159
|
+
renderCodeBlock(node, ctx, indent);
|
|
160
|
+
break;
|
|
117
161
|
case 'input':
|
|
118
|
-
|
|
162
|
+
case 'textarea':
|
|
163
|
+
renderInput(node, ctx, indent);
|
|
119
164
|
break;
|
|
120
165
|
case 'slider':
|
|
121
166
|
renderSlider(node, ctx, indent);
|
|
@@ -157,6 +202,32 @@ function renderNode(node, ctx, indent) {
|
|
|
157
202
|
case 'tab':
|
|
158
203
|
ctx.lines.push(`${indent}<button${twClasses(node, ctx)}>${escapeJsxText(String(p.label || ''))}</button>`);
|
|
159
204
|
break;
|
|
205
|
+
case 'table':
|
|
206
|
+
ctx.lines.push(`${indent}<table${twClasses(node, ctx)}>`);
|
|
207
|
+
renderChildren(node, ctx, indent);
|
|
208
|
+
ctx.lines.push(`${indent}</table>`);
|
|
209
|
+
break;
|
|
210
|
+
case 'thead':
|
|
211
|
+
ctx.lines.push(`${indent}<thead>`);
|
|
212
|
+
renderChildren(node, ctx, indent);
|
|
213
|
+
ctx.lines.push(`${indent}</thead>`);
|
|
214
|
+
break;
|
|
215
|
+
case 'tbody':
|
|
216
|
+
ctx.lines.push(`${indent}<tbody>`);
|
|
217
|
+
renderChildren(node, ctx, indent);
|
|
218
|
+
ctx.lines.push(`${indent}</tbody>`);
|
|
219
|
+
break;
|
|
220
|
+
case 'tr':
|
|
221
|
+
ctx.lines.push(`${indent}<tr${twClasses(node, ctx)}>`);
|
|
222
|
+
renderChildren(node, ctx, indent);
|
|
223
|
+
ctx.lines.push(`${indent}</tr>`);
|
|
224
|
+
break;
|
|
225
|
+
case 'th':
|
|
226
|
+
renderTableCell(node, ctx, indent, 'th');
|
|
227
|
+
break;
|
|
228
|
+
case 'td':
|
|
229
|
+
renderTableCell(node, ctx, indent, 'td');
|
|
230
|
+
break;
|
|
160
231
|
case 'generateMetadata':
|
|
161
232
|
renderGenerateMetadata(node, ctx);
|
|
162
233
|
break;
|
|
@@ -172,6 +243,23 @@ function renderNode(node, ctx, indent) {
|
|
|
172
243
|
case 'fetch':
|
|
173
244
|
renderFetchNode(node, ctx);
|
|
174
245
|
break;
|
|
246
|
+
case 'on':
|
|
247
|
+
renderOnHandler(node, ctx);
|
|
248
|
+
return;
|
|
249
|
+
case 'state':
|
|
250
|
+
ctx.stateDecls.push({ name: String(p.name || ''), initial: String(p.initial ?? '') });
|
|
251
|
+
ctx.isClient = true; // state requires 'use client'
|
|
252
|
+
return;
|
|
253
|
+
case 'logic':
|
|
254
|
+
if (p.code)
|
|
255
|
+
ctx.logicBlocks.push(String(p.code));
|
|
256
|
+
else if (node.children) {
|
|
257
|
+
const handlerChild = node.children.find(c => c.type === 'handler');
|
|
258
|
+
if (handlerChild?.props?.code)
|
|
259
|
+
ctx.logicBlocks.push(String(handlerChild.props.code));
|
|
260
|
+
}
|
|
261
|
+
ctx.isClient = true;
|
|
262
|
+
return;
|
|
175
263
|
case 'theme': break;
|
|
176
264
|
default:
|
|
177
265
|
ctx.lines.push(`${indent}<div${twClasses(node, ctx)}>`);
|
|
@@ -231,12 +319,51 @@ function renderMetadata(node, ctx) {
|
|
|
231
319
|
function renderSection(node, ctx, indent) {
|
|
232
320
|
const p = getProps(node);
|
|
233
321
|
const title = p.title || '';
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
322
|
+
const id = p.id;
|
|
323
|
+
const idAttr = id ? ` id="${id}"` : '';
|
|
324
|
+
const tw = twClasses(node, ctx);
|
|
325
|
+
ctx.lines.push(`${indent}<section${idAttr}${tw}>`);
|
|
326
|
+
if (title) {
|
|
327
|
+
ctx.lines.push(`${indent} <h2 className="text-lg font-semibold mb-4">${escapeJsxText(title)}</h2>`);
|
|
328
|
+
}
|
|
237
329
|
renderChildren(node, ctx, indent);
|
|
238
330
|
ctx.lines.push(`${indent}</section>`);
|
|
239
331
|
}
|
|
332
|
+
function renderCodeBlock(node, ctx, indent) {
|
|
333
|
+
const p = getProps(node);
|
|
334
|
+
const lang = p.lang || '';
|
|
335
|
+
const langClass = lang ? ` language-${lang}` : '';
|
|
336
|
+
const hasCustomStyle = getStyles(node).className || getStyles(node).background;
|
|
337
|
+
const preAttrs = hasCustomStyle ? twClasses(node, ctx) : ` className="bg-zinc-900 rounded-lg p-4 overflow-x-auto"`;
|
|
338
|
+
const codeClass = hasCustomStyle
|
|
339
|
+
? `className="${langClass.trim()}"` + (getStyles(node).fontFamily ? ` style={{ fontFamily: '${getStyles(node).fontFamily}' }}` : '')
|
|
340
|
+
: `className="text-sm font-mono text-zinc-100${langClass}"`;
|
|
341
|
+
// Content: inline value prop or body child node
|
|
342
|
+
const rawValue = p.value;
|
|
343
|
+
if (isExpr(rawValue)) {
|
|
344
|
+
ctx.lines.push(`${indent}<pre${preAttrs}>`);
|
|
345
|
+
ctx.lines.push(`${indent} <code ${codeClass}>{${rawValue.code}}</code>`);
|
|
346
|
+
ctx.lines.push(`${indent}</pre>`);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
let content = rawValue || '';
|
|
350
|
+
if (!content && node.children) {
|
|
351
|
+
const bodyNode = node.children.find(c => c.type === 'body');
|
|
352
|
+
if (bodyNode) {
|
|
353
|
+
const bp = getProps(bodyNode);
|
|
354
|
+
// body value="..." OR body <<<...>>> (multiline block → code prop)
|
|
355
|
+
content = bp.code || bp.value || '';
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Escape for JSX template literal: backslashes, backticks, ${
|
|
359
|
+
const escaped = content
|
|
360
|
+
.replace(/\\/g, '\\\\')
|
|
361
|
+
.replace(/`/g, '\\`')
|
|
362
|
+
.replace(/\$\{/g, '\\${');
|
|
363
|
+
ctx.lines.push(`${indent}<pre${preAttrs}>`);
|
|
364
|
+
ctx.lines.push(`${indent} <code ${codeClass}>{\`${escaped}\`}</code>`);
|
|
365
|
+
ctx.lines.push(`${indent}</pre>`);
|
|
366
|
+
}
|
|
240
367
|
function renderCard(node, ctx, indent) {
|
|
241
368
|
const styles = { ...getStyles(node) };
|
|
242
369
|
const border = styles.border;
|
|
@@ -258,30 +385,67 @@ function renderCard(node, ctx, indent) {
|
|
|
258
385
|
ctx.lines.push(`${indent}</div>`);
|
|
259
386
|
}
|
|
260
387
|
}
|
|
388
|
+
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' };
|
|
261
389
|
function renderText(node, ctx, indent) {
|
|
262
390
|
const p = getProps(node);
|
|
263
|
-
const
|
|
391
|
+
const rawValue = p.value;
|
|
264
392
|
const bind = p.bind;
|
|
265
|
-
const el = p.tag
|
|
393
|
+
const el = TEXT_TAG_MAP[p.tag] || 'span';
|
|
266
394
|
const tw = twClasses(node, ctx);
|
|
267
|
-
if (
|
|
395
|
+
if (isExpr(rawValue))
|
|
396
|
+
ctx.lines.push(`${indent}<${el}${tw}>{${rawValue.code}}</${el}>`);
|
|
397
|
+
else if (bind)
|
|
268
398
|
ctx.lines.push(`${indent}<${el}${tw}>{${bind}}</${el}>`);
|
|
269
|
-
else if (
|
|
270
|
-
ctx.lines.push(`${indent}<${el}${tw}>${escapeJsxText(
|
|
399
|
+
else if (rawValue)
|
|
400
|
+
ctx.lines.push(`${indent}<${el}${tw}>${escapeJsxText(rawValue)}</${el}>`);
|
|
271
401
|
}
|
|
272
402
|
function renderButton(node, ctx, indent) {
|
|
273
403
|
const p = getProps(node);
|
|
274
404
|
const text = p.text || '';
|
|
275
405
|
const to = p.to;
|
|
276
|
-
const
|
|
406
|
+
const rawOnClick = p.onClick;
|
|
407
|
+
const onClick = isExpr(rawOnClick) ? rawOnClick.code : rawOnClick;
|
|
277
408
|
if (to) {
|
|
278
409
|
addDefaultImport(ctx, 'next/link', 'Link');
|
|
279
410
|
ctx.lines.push(`${indent}<Link href="/${to.toLowerCase()}"${twClasses(node, ctx)}>${escapeJsxText(text)}</Link>`);
|
|
280
411
|
}
|
|
281
412
|
else {
|
|
413
|
+
ctx.isClient = true; // onClick requires 'use client'
|
|
282
414
|
ctx.lines.push(`${indent}<button${twClasses(node, ctx)} onClick={${onClick || '() => {}'}}>${escapeJsxText(text)}</button>`);
|
|
283
415
|
}
|
|
284
416
|
}
|
|
417
|
+
function renderInput(node, ctx, indent) {
|
|
418
|
+
const p = getProps(node);
|
|
419
|
+
const isTextarea = node.type === 'textarea' || p.type === 'textarea' || p.multiline;
|
|
420
|
+
const tag = isTextarea ? 'textarea' : 'input';
|
|
421
|
+
const attrs = [];
|
|
422
|
+
const tw = twClasses(node, ctx);
|
|
423
|
+
if (p.bind) {
|
|
424
|
+
const bind = p.bind;
|
|
425
|
+
const setter = `set${bind.charAt(0).toUpperCase() + bind.slice(1)}`;
|
|
426
|
+
attrs.push(`value={${bind}}`);
|
|
427
|
+
ctx.isClient = true; // onChange requires 'use client'
|
|
428
|
+
if (isExpr(p.onChange))
|
|
429
|
+
attrs.push(`onChange={${p.onChange.code}}`);
|
|
430
|
+
else if (p.onChange)
|
|
431
|
+
attrs.push(`onChange={${p.onChange}}`);
|
|
432
|
+
else
|
|
433
|
+
attrs.push(`onChange={(e) => ${setter}(e.target.value)}`);
|
|
434
|
+
}
|
|
435
|
+
if (p.placeholder)
|
|
436
|
+
attrs.push(`placeholder="${p.placeholder}"`);
|
|
437
|
+
if (!isTextarea && p.type && p.type !== 'textarea')
|
|
438
|
+
attrs.push(`type="${p.type}"`);
|
|
439
|
+
if (p.spellcheck === 'false' || p.spellcheck === false)
|
|
440
|
+
attrs.push('spellCheck={false}');
|
|
441
|
+
const attrStr = attrs.length > 0 ? ' ' + attrs.join(' ') : '';
|
|
442
|
+
if (isTextarea) {
|
|
443
|
+
ctx.lines.push(`${indent}<${tag}${tw}${attrStr} rows={4} />`);
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
ctx.lines.push(`${indent}<${tag}${tw}${attrStr} />`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
285
449
|
function renderLink(node, ctx, indent) {
|
|
286
450
|
const p = getProps(node);
|
|
287
451
|
addDefaultImport(ctx, 'next/link', 'Link');
|
|
@@ -293,25 +457,107 @@ function renderImage(node, ctx, indent) {
|
|
|
293
457
|
const p = getProps(node);
|
|
294
458
|
addDefaultImport(ctx, 'next/image', 'Image');
|
|
295
459
|
const tw = twClasses(node, ctx);
|
|
296
|
-
const
|
|
297
|
-
const
|
|
298
|
-
|
|
460
|
+
const rawSrc = p.src || '';
|
|
461
|
+
const src = (rawSrc.startsWith('/') || rawSrc.includes('://') || rawSrc.includes('.')) ? rawSrc : `/${rawSrc}.png`;
|
|
462
|
+
const alt = escapeJsxAttr(String(p.alt || p.src || ''));
|
|
463
|
+
const fill = p.fill === 'true' || p.fill === true;
|
|
464
|
+
const priority = p.priority === 'true' || p.priority === true;
|
|
465
|
+
if (fill) {
|
|
466
|
+
ctx.lines.push(`${indent}<Image src="${src}" alt="${alt}"${priority ? ' priority' : ''} fill${tw} />`);
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
const width = p.width || (getStyles(node).w) || '100';
|
|
470
|
+
const height = p.height || (getStyles(node).h) || '100';
|
|
471
|
+
ctx.lines.push(`${indent}<Image src="${src}" alt="${alt}" width={${width}} height={${height}}${priority ? ' priority' : ''}${tw} />`);
|
|
472
|
+
}
|
|
299
473
|
}
|
|
300
474
|
function renderSlider(node, ctx, indent) {
|
|
301
475
|
const p = getProps(node);
|
|
302
476
|
const bind = p.bind;
|
|
303
477
|
const setter = bind ? `set${bind.charAt(0).toUpperCase() + bind.slice(1)}` : 'setValue';
|
|
478
|
+
ctx.isClient = true; // onChange requires 'use client'
|
|
304
479
|
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" />`);
|
|
305
480
|
}
|
|
306
481
|
function renderToggle(node, ctx, indent) {
|
|
307
482
|
const p = getProps(node);
|
|
308
483
|
const bind = p.bind;
|
|
309
484
|
const setter = bind ? `set${bind.charAt(0).toUpperCase() + bind.slice(1)}` : 'setValue';
|
|
485
|
+
ctx.isClient = true; // onChange requires 'use client'
|
|
310
486
|
ctx.lines.push(`${indent}<label className="relative inline-flex items-center cursor-pointer">`);
|
|
311
487
|
ctx.lines.push(`${indent} <input type="checkbox" className="sr-only peer" checked={${bind || 'value'}} onChange={(e) => ${setter}(e.target.checked)} />`);
|
|
312
488
|
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" />`);
|
|
313
489
|
ctx.lines.push(`${indent}</label>`);
|
|
314
490
|
}
|
|
491
|
+
function renderOnHandler(node, ctx) {
|
|
492
|
+
const p = getProps(node);
|
|
493
|
+
const event = (p.event || p.name);
|
|
494
|
+
const handlerRef = p.handler;
|
|
495
|
+
const key = p.key;
|
|
496
|
+
const isAsync = p.async === 'true' || p.async === true;
|
|
497
|
+
const handlerChild = (node.children || []).find(c => c.type === 'handler');
|
|
498
|
+
const code = handlerChild ? (getProps(handlerChild).code || '') : '';
|
|
499
|
+
if (handlerRef && !code)
|
|
500
|
+
return;
|
|
501
|
+
ctx.isClient = true; // event handlers require 'use client'
|
|
502
|
+
const fnName = handlerRef || `handle${event.charAt(0).toUpperCase() + event.slice(1)}`;
|
|
503
|
+
const asyncKw = isAsync ? 'async ' : '';
|
|
504
|
+
const paramType = event === 'submit' ? 'e: React.FormEvent'
|
|
505
|
+
: event === 'click' ? 'e: React.MouseEvent'
|
|
506
|
+
: event === 'change' ? 'e: React.ChangeEvent'
|
|
507
|
+
: event === 'key' || event === 'keydown' || event === 'keyup' ? 'e: React.KeyboardEvent'
|
|
508
|
+
: event === 'focus' || event === 'blur' ? 'e: React.FocusEvent'
|
|
509
|
+
: event === 'scroll' ? 'e: React.UIEvent'
|
|
510
|
+
: `e: React.SyntheticEvent`;
|
|
511
|
+
const keyGuard = key ? ` if (e.key !== '${key}') return;\n` : '';
|
|
512
|
+
addNamedImport(ctx, 'react', 'useCallback');
|
|
513
|
+
let block = ` const ${fnName} = useCallback(${asyncKw}(${paramType}) => {\n`;
|
|
514
|
+
if (keyGuard)
|
|
515
|
+
block += keyGuard;
|
|
516
|
+
if (code) {
|
|
517
|
+
for (const line of code.split('\n')) {
|
|
518
|
+
block += ` ${line}\n`;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
block += ` }, []);\n`;
|
|
522
|
+
ctx.bodyLines.push(block);
|
|
523
|
+
if (event === 'key' || event === 'keydown' || event === 'keyup') {
|
|
524
|
+
addNamedImport(ctx, 'react', 'useEffect');
|
|
525
|
+
const domEvent = event === 'key' ? 'keydown' : event;
|
|
526
|
+
let effect = ` useEffect(() => {\n`;
|
|
527
|
+
effect += ` const listener = (e: KeyboardEvent) => ${fnName}(e as unknown as React.KeyboardEvent);\n`;
|
|
528
|
+
effect += ` window.addEventListener('${domEvent}', listener);\n`;
|
|
529
|
+
effect += ` return () => window.removeEventListener('${domEvent}', listener);\n`;
|
|
530
|
+
effect += ` }, [${fnName}]);\n`;
|
|
531
|
+
ctx.bodyLines.push(effect);
|
|
532
|
+
}
|
|
533
|
+
if (event === 'resize') {
|
|
534
|
+
addNamedImport(ctx, 'react', 'useEffect');
|
|
535
|
+
let effect = ` useEffect(() => {\n`;
|
|
536
|
+
effect += ` window.addEventListener('resize', ${fnName});\n`;
|
|
537
|
+
effect += ` return () => window.removeEventListener('resize', ${fnName});\n`;
|
|
538
|
+
effect += ` }, [${fnName}]);\n`;
|
|
539
|
+
ctx.bodyLines.push(effect);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function renderTableCell(node, ctx, indent, tag) {
|
|
543
|
+
const p = getProps(node);
|
|
544
|
+
const tw = twClasses(node, ctx);
|
|
545
|
+
const rawValue = p.value;
|
|
546
|
+
if (isExpr(rawValue)) {
|
|
547
|
+
ctx.lines.push(`${indent}<${tag}${tw}>{${rawValue.code}}</${tag}>`);
|
|
548
|
+
}
|
|
549
|
+
else if (rawValue) {
|
|
550
|
+
ctx.lines.push(`${indent}<${tag}${tw}>${escapeJsxText(rawValue)}</${tag}>`);
|
|
551
|
+
}
|
|
552
|
+
else if (node.children && node.children.length > 0) {
|
|
553
|
+
ctx.lines.push(`${indent}<${tag}${tw}>`);
|
|
554
|
+
renderChildren(node, ctx, indent);
|
|
555
|
+
ctx.lines.push(`${indent}</${tag}>`);
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
ctx.lines.push(`${indent}<${tag}${tw} />`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
315
561
|
function renderGrid(node, ctx, indent) {
|
|
316
562
|
const p = getProps(node);
|
|
317
563
|
ctx.lines.push(`${indent}<div className="grid grid-cols-1 md:grid-cols-${p.cols || 1} gap-${Math.round(Number(p.gap || 16) / 4)}">`);
|
|
@@ -328,14 +574,14 @@ function renderConditional(node, ctx, indent) {
|
|
|
328
574
|
}
|
|
329
575
|
function renderComponent(node, ctx, indent) {
|
|
330
576
|
const p = getProps(node);
|
|
331
|
-
const ref = p.ref;
|
|
577
|
+
const ref = (p.ref || p.name);
|
|
332
578
|
if (!ref)
|
|
333
579
|
return;
|
|
334
580
|
ctx.componentImports.add(ref);
|
|
335
581
|
const hasOnChange = 'onChange' in p;
|
|
336
582
|
const attrs = [];
|
|
337
583
|
for (const [k, v] of Object.entries(p)) {
|
|
338
|
-
if (['ref', 'styles', 'pseudoStyles', 'themeRefs'].includes(k))
|
|
584
|
+
if (['ref', 'name', 'styles', 'pseudoStyles', 'themeRefs'].includes(k))
|
|
339
585
|
continue;
|
|
340
586
|
if (k === 'bind') {
|
|
341
587
|
attrs.push(`value={${v}}`);
|
|
@@ -355,7 +601,15 @@ function renderComponent(node, ctx, indent) {
|
|
|
355
601
|
else
|
|
356
602
|
attrs.push(`${k}={${JSON.stringify(v)}}`);
|
|
357
603
|
}
|
|
358
|
-
|
|
604
|
+
const attrStr = attrs.length ? ' ' + attrs.join(' ') : '';
|
|
605
|
+
if (node.children && node.children.length > 0) {
|
|
606
|
+
ctx.lines.push(`${indent}<${ref}${attrStr}>`);
|
|
607
|
+
renderChildren(node, ctx, indent);
|
|
608
|
+
ctx.lines.push(`${indent}</${ref}>`);
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
ctx.lines.push(`${indent}<${ref}${attrStr} />`);
|
|
612
|
+
}
|
|
359
613
|
}
|
|
360
614
|
function renderProgress(node, ctx, indent) {
|
|
361
615
|
const p = getProps(node);
|
|
@@ -446,6 +700,8 @@ function _transpileNextjsInner(root, config) {
|
|
|
446
700
|
generateMetadataInfo: null,
|
|
447
701
|
fetchCalls: [],
|
|
448
702
|
bodyLines: [],
|
|
703
|
+
stateDecls: [],
|
|
704
|
+
logicBlocks: [],
|
|
449
705
|
colors: config?.colors,
|
|
450
706
|
twProfile: config?.frameworkVersions ? buildTailwindProfile(config.frameworkVersions) : undefined,
|
|
451
707
|
njProfile: config?.frameworkVersions ? buildNextjsProfile(config.frameworkVersions) : undefined,
|
|
@@ -461,6 +717,11 @@ function _transpileNextjsInner(root, config) {
|
|
|
461
717
|
// If there are fetch calls, mark page as async
|
|
462
718
|
if (ctx.fetchCalls.length > 0)
|
|
463
719
|
ctx.isAsync = true;
|
|
720
|
+
// Client components cannot be async in Next.js — client wins, drop server-only patterns
|
|
721
|
+
if (ctx.isClient && ctx.isAsync) {
|
|
722
|
+
ctx.isAsync = false;
|
|
723
|
+
ctx.fetchCalls = [];
|
|
724
|
+
}
|
|
464
725
|
const name = rootProps.name || 'Page';
|
|
465
726
|
const isLayout = root.type === 'layout';
|
|
466
727
|
const isLoading = root.type === 'loading';
|
|
@@ -490,6 +751,21 @@ function _transpileNextjsInner(root, config) {
|
|
|
490
751
|
if (ctx.metadata && !ctx.isClient && !ctx.generateMetadataInfo) {
|
|
491
752
|
addNamedImport(ctx, 'next', 'Metadata', true);
|
|
492
753
|
}
|
|
754
|
+
// State requires useState import
|
|
755
|
+
if (ctx.stateDecls.length > 0) {
|
|
756
|
+
addNamedImport(ctx, 'react', 'useState');
|
|
757
|
+
}
|
|
758
|
+
// Detect hook imports from logic blocks before emitting imports
|
|
759
|
+
for (const block of ctx.logicBlocks) {
|
|
760
|
+
if (block.includes('useEffect'))
|
|
761
|
+
addNamedImport(ctx, 'react', 'useEffect');
|
|
762
|
+
if (block.includes('useCallback'))
|
|
763
|
+
addNamedImport(ctx, 'react', 'useCallback');
|
|
764
|
+
if (block.includes('useMemo'))
|
|
765
|
+
addNamedImport(ctx, 'react', 'useMemo');
|
|
766
|
+
if (block.includes('useRef'))
|
|
767
|
+
addNamedImport(ctx, 'react', 'useRef');
|
|
768
|
+
}
|
|
493
769
|
// Emit all imports (unified, sorted)
|
|
494
770
|
code.push(...emitImports(ctx));
|
|
495
771
|
if (code.length > 0 && code[code.length - 1] !== '')
|
|
@@ -560,6 +836,42 @@ function _transpileNextjsInner(root, config) {
|
|
|
560
836
|
code.push(` const ${fc.name} = await fetch('${fc.url}').then(r => r.json());`);
|
|
561
837
|
}
|
|
562
838
|
}
|
|
839
|
+
// Emit useState declarations
|
|
840
|
+
for (const s of ctx.stateDecls) {
|
|
841
|
+
const setter = `set${s.name.charAt(0).toUpperCase() + s.name.slice(1)}`;
|
|
842
|
+
const stateNode = root.children?.find(c => c.type === 'state' && c.props?.name === s.name);
|
|
843
|
+
const initProp = stateNode?.props?.initial;
|
|
844
|
+
const isExprInit = typeof initProp === 'object' && initProp !== null && '__expr' in initProp;
|
|
845
|
+
let initVal;
|
|
846
|
+
if (isExprInit) {
|
|
847
|
+
initVal = initProp.code;
|
|
848
|
+
}
|
|
849
|
+
else if (s.initial === 'true' || s.initial === 'false') {
|
|
850
|
+
initVal = s.initial;
|
|
851
|
+
}
|
|
852
|
+
else if (s.initial === '' || s.initial === "''") {
|
|
853
|
+
initVal = "''";
|
|
854
|
+
}
|
|
855
|
+
else if (!isNaN(Number(s.initial)) && s.initial !== '') {
|
|
856
|
+
initVal = s.initial;
|
|
857
|
+
}
|
|
858
|
+
else {
|
|
859
|
+
initVal = `'${s.initial}'`;
|
|
860
|
+
}
|
|
861
|
+
code.push(` const [${s.name}, ${setter}] = useState(${initVal});`);
|
|
862
|
+
}
|
|
863
|
+
// Emit logic blocks & detect hook imports
|
|
864
|
+
for (const block of ctx.logicBlocks) {
|
|
865
|
+
code.push(` ${block}`);
|
|
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
|
+
}
|
|
563
875
|
// Emit body lines (notFound, redirect calls)
|
|
564
876
|
for (const line of ctx.bodyLines) {
|
|
565
877
|
code.push(line);
|
|
@@ -573,16 +885,19 @@ function _transpileNextjsInner(root, config) {
|
|
|
573
885
|
const irTokenCount = countTokens(irText);
|
|
574
886
|
const tsTokenCount = countTokens(output);
|
|
575
887
|
const tokenReduction = tsTokenCount > 0 ? Math.round((1 - irTokenCount / tsTokenCount) * 100) : 0;
|
|
576
|
-
// Determine output filename convention
|
|
888
|
+
// Determine output filename convention (route-aware)
|
|
889
|
+
const route = rootProps.route;
|
|
890
|
+
const segment = rootProps.segment;
|
|
891
|
+
const routePrefix = route ? routeToPath(route, segment) : (segment ? routeToPath('', segment) : '');
|
|
577
892
|
const files = [];
|
|
578
893
|
if (isLayout)
|
|
579
|
-
files.push({ path:
|
|
894
|
+
files.push({ path: `${routePrefix}layout.tsx`, content: output });
|
|
580
895
|
else if (isLoading)
|
|
581
|
-
files.push({ path:
|
|
896
|
+
files.push({ path: `${routePrefix}loading.tsx`, content: output });
|
|
582
897
|
else if (isError)
|
|
583
|
-
files.push({ path:
|
|
898
|
+
files.push({ path: `${routePrefix}error.tsx`, content: output });
|
|
584
899
|
else
|
|
585
|
-
files.push({ path:
|
|
900
|
+
files.push({ path: `${routePrefix}page.tsx`, content: output });
|
|
586
901
|
return {
|
|
587
902
|
code: output,
|
|
588
903
|
sourceMap: ctx.sourceMap,
|
|
@@ -605,6 +920,8 @@ function _renderNextjsFile(file, config) {
|
|
|
605
920
|
generateMetadataInfo: null,
|
|
606
921
|
fetchCalls: [],
|
|
607
922
|
bodyLines: [],
|
|
923
|
+
stateDecls: [],
|
|
924
|
+
logicBlocks: [],
|
|
608
925
|
colors: config.colors,
|
|
609
926
|
twProfile: config.frameworkVersions ? buildTailwindProfile(config.frameworkVersions) : undefined,
|
|
610
927
|
njProfile: config.frameworkVersions ? buildNextjsProfile(config.frameworkVersions) : undefined,
|
|
@@ -621,8 +938,11 @@ function _renderNextjsFile(file, config) {
|
|
|
621
938
|
const name = file.componentName || rootProps.name || 'Component';
|
|
622
939
|
const isLayout = rootNode.type === 'layout';
|
|
623
940
|
const isError = rootNode.type === 'error';
|
|
624
|
-
//
|
|
625
|
-
|
|
941
|
+
// Client components cannot be async in Next.js — client wins, drop server-only patterns
|
|
942
|
+
if (ctx.isClient && ctx.isAsync) {
|
|
943
|
+
ctx.isAsync = false;
|
|
944
|
+
ctx.fetchCalls = [];
|
|
945
|
+
}
|
|
626
946
|
const code = [];
|
|
627
947
|
if (ctx.isClient) {
|
|
628
948
|
code.push(`'use client';`);
|
|
@@ -648,6 +968,21 @@ function _renderNextjsFile(file, config) {
|
|
|
648
968
|
// If there are fetch calls, mark page as async
|
|
649
969
|
if (ctx.fetchCalls.length > 0)
|
|
650
970
|
ctx.isAsync = true;
|
|
971
|
+
// State requires useState import
|
|
972
|
+
if (ctx.stateDecls.length > 0) {
|
|
973
|
+
addNamedImport(ctx, 'react', 'useState');
|
|
974
|
+
}
|
|
975
|
+
// Detect hook imports from logic blocks before emitting imports
|
|
976
|
+
for (const block of ctx.logicBlocks) {
|
|
977
|
+
if (block.includes('useEffect'))
|
|
978
|
+
addNamedImport(ctx, 'react', 'useEffect');
|
|
979
|
+
if (block.includes('useCallback'))
|
|
980
|
+
addNamedImport(ctx, 'react', 'useCallback');
|
|
981
|
+
if (block.includes('useMemo'))
|
|
982
|
+
addNamedImport(ctx, 'react', 'useMemo');
|
|
983
|
+
if (block.includes('useRef'))
|
|
984
|
+
addNamedImport(ctx, 'react', 'useRef');
|
|
985
|
+
}
|
|
651
986
|
code.push(...emitImports(ctx));
|
|
652
987
|
if (code.length > 0 && code[code.length - 1] !== '')
|
|
653
988
|
code.push('');
|
|
@@ -706,6 +1041,43 @@ function _renderNextjsFile(file, config) {
|
|
|
706
1041
|
else {
|
|
707
1042
|
code.push(`export default function ${name}() {`);
|
|
708
1043
|
}
|
|
1044
|
+
// Emit useState declarations
|
|
1045
|
+
for (const s of ctx.stateDecls) {
|
|
1046
|
+
const setter = `set${s.name.charAt(0).toUpperCase() + s.name.slice(1)}`;
|
|
1047
|
+
const rootNode = file.nodes[0];
|
|
1048
|
+
const stateNode = rootNode?.children?.find((c) => c.type === 'state' && c.props?.name === s.name);
|
|
1049
|
+
const initProp = stateNode?.props?.initial;
|
|
1050
|
+
const isExpr = typeof initProp === 'object' && initProp !== null && '__expr' in initProp;
|
|
1051
|
+
let initVal;
|
|
1052
|
+
if (isExpr) {
|
|
1053
|
+
initVal = initProp.code;
|
|
1054
|
+
}
|
|
1055
|
+
else if (s.initial === 'true' || s.initial === 'false') {
|
|
1056
|
+
initVal = s.initial;
|
|
1057
|
+
}
|
|
1058
|
+
else if (s.initial === '' || s.initial === "''") {
|
|
1059
|
+
initVal = "''";
|
|
1060
|
+
}
|
|
1061
|
+
else if (!isNaN(Number(s.initial)) && s.initial !== '') {
|
|
1062
|
+
initVal = s.initial;
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
initVal = `'${s.initial}'`;
|
|
1066
|
+
}
|
|
1067
|
+
code.push(` const [${s.name}, ${setter}] = useState(${initVal});`);
|
|
1068
|
+
}
|
|
1069
|
+
// Emit logic blocks & detect hook imports
|
|
1070
|
+
for (const block of ctx.logicBlocks) {
|
|
1071
|
+
code.push(` ${block}`);
|
|
1072
|
+
if (block.includes('useEffect'))
|
|
1073
|
+
addNamedImport(ctx, 'react', 'useEffect');
|
|
1074
|
+
if (block.includes('useCallback'))
|
|
1075
|
+
addNamedImport(ctx, 'react', 'useCallback');
|
|
1076
|
+
if (block.includes('useMemo'))
|
|
1077
|
+
addNamedImport(ctx, 'react', 'useMemo');
|
|
1078
|
+
if (block.includes('useRef'))
|
|
1079
|
+
addNamedImport(ctx, 'react', 'useRef');
|
|
1080
|
+
}
|
|
709
1081
|
// Emit fetch calls (inside async function body, before return)
|
|
710
1082
|
for (const fc of ctx.fetchCalls) {
|
|
711
1083
|
if (fc.options) {
|