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