@kernlang/react 2.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.
@@ -0,0 +1,748 @@
1
+ import { stylesToTailwind, colorToTw, countTokens, serializeIR, camelKey, escapeJsxText, escapeJsString, buildTailwindProfile, buildNextjsProfile, applyTailwindTokenRules } from '@kernlang/core';
2
+ import { planStructure } from './structure.js';
3
+ import { buildStructuredArtifacts } from './artifact-utils.js';
4
+ function getProps(node) { return node.props || {}; }
5
+ function getStyles(node) { return getProps(node).styles || {}; }
6
+ // ── Unified import helpers (from Codex) ──────────────────────────────────
7
+ function addDefaultImport(ctx, source, name) {
8
+ const spec = ctx.imports.get(source) || { namedImports: new Set(), typeOnlyImports: new Set() };
9
+ spec.defaultImport = name;
10
+ ctx.imports.set(source, spec);
11
+ }
12
+ function addNamedImport(ctx, source, name, typeOnly) {
13
+ const spec = ctx.imports.get(source) || { namedImports: new Set(), typeOnlyImports: new Set() };
14
+ if (typeOnly) {
15
+ spec.typeOnlyImports.add(name);
16
+ }
17
+ else {
18
+ spec.namedImports.add(name);
19
+ }
20
+ ctx.imports.set(source, spec);
21
+ }
22
+ function exprCode(value, fallback) {
23
+ if (typeof value === 'object' && value !== null && '__expr' in value) {
24
+ return value.code;
25
+ }
26
+ if (typeof value === 'string' && value.length > 0)
27
+ return value;
28
+ return fallback;
29
+ }
30
+ function emitImports(ctx) {
31
+ const lines = [];
32
+ for (const [source, spec] of [...ctx.imports.entries()].sort(([a], [b]) => a.localeCompare(b))) {
33
+ // Separate type-only imports from value imports
34
+ const typeImports = [...spec.typeOnlyImports].filter(n => !spec.namedImports.has(n));
35
+ const valueImports = [...spec.namedImports];
36
+ // Emit type-only import statement if there are type imports and no value imports sharing the source
37
+ if (typeImports.length > 0 && valueImports.length === 0 && !spec.defaultImport) {
38
+ lines.push(`import type { ${typeImports.sort().join(', ')} } from '${source}';`);
39
+ }
40
+ else {
41
+ // Emit value import (with default if present)
42
+ const clauses = [];
43
+ if (spec.defaultImport)
44
+ clauses.push(spec.defaultImport);
45
+ if (valueImports.length > 0)
46
+ clauses.push(`{ ${valueImports.sort().join(', ')} }`);
47
+ if (clauses.length > 0) {
48
+ lines.push(`import ${clauses.join(', ')} from '${source}';`);
49
+ }
50
+ // Emit separate type-only import if both type and value imports exist
51
+ if (typeImports.length > 0) {
52
+ lines.push(`import type { ${typeImports.sort().join(', ')} } from '${source}';`);
53
+ }
54
+ }
55
+ }
56
+ return lines;
57
+ }
58
+ function twClasses(node, ctx, extra = '') {
59
+ let tw = stylesToTailwind(getStyles(node), ctx.colors);
60
+ if (ctx.twProfile)
61
+ tw = applyTailwindTokenRules(tw, ctx.twProfile);
62
+ const parts = [tw, extra].filter(Boolean);
63
+ return parts.length > 0 ? ` className="${parts.join(' ')}"` : '';
64
+ }
65
+ // ── Node renderers ──────────────────────────────────────────────────────
66
+ function renderNode(node, ctx, indent) {
67
+ const p = getProps(node);
68
+ ctx.sourceMap.push({ irLine: node.loc?.line || 0, irCol: node.loc?.col || 1, outLine: ctx.lines.length + 1, outCol: 1 });
69
+ switch (node.type) {
70
+ case 'page':
71
+ case 'screen':
72
+ renderPage(node, ctx, indent);
73
+ break;
74
+ case 'layout':
75
+ renderLayout(node, ctx, indent);
76
+ break;
77
+ case 'loading':
78
+ renderLoading(node, ctx, indent);
79
+ break;
80
+ case 'error':
81
+ renderError(node, ctx, indent);
82
+ break;
83
+ case 'metadata':
84
+ renderMetadata(node, ctx);
85
+ break;
86
+ case 'section':
87
+ renderSection(node, ctx, indent);
88
+ break;
89
+ case 'card':
90
+ renderCard(node, ctx, indent);
91
+ break;
92
+ case 'row':
93
+ ctx.lines.push(`${indent}<div${twClasses(node, ctx, 'flex')}>`);
94
+ renderChildren(node, ctx, indent);
95
+ ctx.lines.push(`${indent}</div>`);
96
+ break;
97
+ case 'col':
98
+ ctx.lines.push(`${indent}<div${twClasses(node, ctx, 'flex flex-col')}>`);
99
+ renderChildren(node, ctx, indent);
100
+ ctx.lines.push(`${indent}</div>`);
101
+ break;
102
+ case 'text':
103
+ renderText(node, ctx, indent);
104
+ break;
105
+ case 'divider':
106
+ ctx.lines.push(`${indent}<div${twClasses(node, ctx, 'h-px')} />`);
107
+ break;
108
+ case 'button':
109
+ renderButton(node, ctx, indent);
110
+ break;
111
+ case 'link':
112
+ renderLink(node, ctx, indent);
113
+ break;
114
+ case 'image':
115
+ renderImage(node, ctx, indent);
116
+ break;
117
+ case 'input':
118
+ ctx.lines.push(`${indent}<input${twClasses(node, ctx)} />`);
119
+ break;
120
+ case 'slider':
121
+ renderSlider(node, ctx, indent);
122
+ break;
123
+ case 'toggle':
124
+ renderToggle(node, ctx, indent);
125
+ break;
126
+ case 'grid':
127
+ renderGrid(node, ctx, indent);
128
+ break;
129
+ case 'conditional':
130
+ renderConditional(node, ctx, indent);
131
+ break;
132
+ case 'component':
133
+ renderComponent(node, ctx, indent);
134
+ break;
135
+ case 'icon':
136
+ ctx.componentImports.add('Icon');
137
+ ctx.lines.push(`${indent}<Icon name="${p.name}" size="sm"${twClasses(node, ctx)} />`);
138
+ break;
139
+ case 'list':
140
+ ctx.lines.push(`${indent}<div${twClasses(node, ctx, 'space-y-2')}>`);
141
+ renderChildren(node, ctx, indent);
142
+ ctx.lines.push(`${indent}</div>`);
143
+ break;
144
+ case 'item':
145
+ ctx.lines.push(`${indent}<div${twClasses(node, ctx)}>`);
146
+ renderChildren(node, ctx, indent);
147
+ ctx.lines.push(`${indent}</div>`);
148
+ break;
149
+ case 'progress':
150
+ renderProgress(node, ctx, indent);
151
+ break;
152
+ case 'tabs':
153
+ ctx.lines.push(`${indent}<nav${twClasses(node, ctx, 'flex')}>`);
154
+ renderChildren(node, ctx, indent);
155
+ ctx.lines.push(`${indent}</nav>`);
156
+ break;
157
+ case 'tab':
158
+ ctx.lines.push(`${indent}<button${twClasses(node, ctx)}>${escapeJsxText(String(p.label || ''))}</button>`);
159
+ break;
160
+ case 'generateMetadata':
161
+ renderGenerateMetadata(node, ctx);
162
+ break;
163
+ case 'notFound':
164
+ renderNotFound(node, ctx, indent);
165
+ break;
166
+ case 'redirect':
167
+ renderRedirect(node, ctx, indent);
168
+ break;
169
+ case 'import':
170
+ renderImport(node, ctx);
171
+ break;
172
+ case 'fetch':
173
+ renderFetchNode(node, ctx);
174
+ break;
175
+ case 'theme': break;
176
+ default:
177
+ ctx.lines.push(`${indent}<div${twClasses(node, ctx)}>`);
178
+ renderChildren(node, ctx, indent);
179
+ ctx.lines.push(`${indent}</div>`);
180
+ }
181
+ }
182
+ function renderChildren(node, ctx, indent) {
183
+ if (node.children)
184
+ for (const child of node.children)
185
+ renderNode(child, ctx, indent + ' ');
186
+ }
187
+ function renderPage(node, ctx, indent) {
188
+ const p = getProps(node);
189
+ if (p.client === 'true' || p.client === true)
190
+ ctx.isClient = true;
191
+ if (p.async === 'true' || p.async === true)
192
+ ctx.isAsync = true;
193
+ ctx.lines.push(`${indent}<div${twClasses(node, ctx)}>`);
194
+ renderChildren(node, ctx, indent);
195
+ ctx.lines.push(`${indent}</div>`);
196
+ }
197
+ function renderLayout(node, ctx, indent) {
198
+ const p = getProps(node);
199
+ ctx.lines.push(`${indent}<html lang="${p.lang || 'en'}">`);
200
+ ctx.lines.push(`${indent} <body${twClasses(node, ctx)}>`);
201
+ ctx.lines.push(`${indent} {children}`);
202
+ renderChildren(node, ctx, indent + ' ');
203
+ ctx.lines.push(`${indent} </body>`);
204
+ ctx.lines.push(`${indent}</html>`);
205
+ }
206
+ function renderLoading(node, ctx, indent) {
207
+ ctx.lines.push(`${indent}<div${twClasses(node, ctx, 'animate-pulse')}>`);
208
+ renderChildren(node, ctx, indent);
209
+ ctx.lines.push(`${indent}</div>`);
210
+ }
211
+ function renderError(node, ctx, indent) {
212
+ ctx.isClient = true;
213
+ ctx.lines.push(`${indent}<div${twClasses(node, ctx)}>`);
214
+ ctx.lines.push(`${indent} <h2>Something went wrong!</h2>`);
215
+ ctx.lines.push(`${indent} <button onClick={() => reset()}>Try again</button>`);
216
+ renderChildren(node, ctx, indent);
217
+ ctx.lines.push(`${indent}</div>`);
218
+ }
219
+ function renderMetadata(node, ctx) {
220
+ const p = getProps(node);
221
+ ctx.metadata = {};
222
+ if (p.title)
223
+ ctx.metadata.title = p.title;
224
+ if (p.description)
225
+ ctx.metadata.description = p.description;
226
+ if (p.keywords)
227
+ ctx.metadata.keywords = p.keywords;
228
+ if (p.ogImage)
229
+ ctx.metadata.ogImage = p.ogImage;
230
+ }
231
+ function renderSection(node, ctx, indent) {
232
+ const p = getProps(node);
233
+ const title = p.title || '';
234
+ const key = p.key || camelKey(title);
235
+ ctx.lines.push(`${indent}<section>`);
236
+ ctx.lines.push(`${indent} <h2 className="text-lg font-semibold mb-4">${escapeJsxText(title)}</h2>`);
237
+ renderChildren(node, ctx, indent);
238
+ ctx.lines.push(`${indent}</section>`);
239
+ }
240
+ function renderCard(node, ctx, indent) {
241
+ const styles = { ...getStyles(node) };
242
+ const border = styles.border;
243
+ delete styles.border;
244
+ // Use a shallow-copied styles object to avoid mutating the live IR node
245
+ if (node.props) {
246
+ const origStyles = node.props.styles;
247
+ node.props.styles = styles;
248
+ const extra = border ? `border ${colorToTw('border', border, ctx.colors)}` : '';
249
+ ctx.lines.push(`${indent}<div${twClasses(node, ctx, extra)}>`);
250
+ renderChildren(node, ctx, indent);
251
+ ctx.lines.push(`${indent}</div>`);
252
+ node.props.styles = origStyles;
253
+ }
254
+ else {
255
+ const extra = border ? `border ${colorToTw('border', border, ctx.colors)}` : '';
256
+ ctx.lines.push(`${indent}<div${twClasses(node, ctx, extra)}>`);
257
+ renderChildren(node, ctx, indent);
258
+ ctx.lines.push(`${indent}</div>`);
259
+ }
260
+ }
261
+ function renderText(node, ctx, indent) {
262
+ const p = getProps(node);
263
+ const value = p.value;
264
+ const bind = p.bind;
265
+ const el = p.tag === 'p' ? 'p' : p.tag === 'h1' ? 'h1' : p.tag === 'h2' ? 'h2' : p.tag === 'label' ? 'label' : 'span';
266
+ const tw = twClasses(node, ctx);
267
+ if (bind)
268
+ ctx.lines.push(`${indent}<${el}${tw}>{${bind}}</${el}>`);
269
+ else if (value)
270
+ ctx.lines.push(`${indent}<${el}${tw}>${escapeJsxText(value)}</${el}>`);
271
+ }
272
+ function renderButton(node, ctx, indent) {
273
+ const p = getProps(node);
274
+ const text = p.text || '';
275
+ const to = p.to;
276
+ const onClick = p.onClick;
277
+ if (to) {
278
+ addDefaultImport(ctx, 'next/link', 'Link');
279
+ ctx.lines.push(`${indent}<Link href="/${to.toLowerCase()}"${twClasses(node, ctx)}>${escapeJsxText(text)}</Link>`);
280
+ }
281
+ else {
282
+ ctx.lines.push(`${indent}<button${twClasses(node, ctx)} onClick={${onClick || '() => {}'}}>${escapeJsxText(text)}</button>`);
283
+ }
284
+ }
285
+ function renderLink(node, ctx, indent) {
286
+ const p = getProps(node);
287
+ addDefaultImport(ctx, 'next/link', 'Link');
288
+ ctx.lines.push(`${indent}<Link href="${p.to || '/'}"${twClasses(node, ctx)}>`);
289
+ renderChildren(node, ctx, indent);
290
+ ctx.lines.push(`${indent}</Link>`);
291
+ }
292
+ function renderImage(node, ctx, indent) {
293
+ const p = getProps(node);
294
+ addDefaultImport(ctx, 'next/image', 'Image');
295
+ const tw = twClasses(node, ctx);
296
+ const width = p.width || (getStyles(node).w) || '100';
297
+ const height = p.height || (getStyles(node).h) || '100';
298
+ ctx.lines.push(`${indent}<Image src="/${p.src}.png" alt="${p.alt || p.src}" width={${width}} height={${height}}${tw} />`);
299
+ }
300
+ function renderSlider(node, ctx, indent) {
301
+ const p = getProps(node);
302
+ const bind = p.bind;
303
+ const setter = bind ? `set${bind.charAt(0).toUpperCase() + bind.slice(1)}` : 'setValue';
304
+ 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
+ }
306
+ function renderToggle(node, ctx, indent) {
307
+ const p = getProps(node);
308
+ const bind = p.bind;
309
+ const setter = bind ? `set${bind.charAt(0).toUpperCase() + bind.slice(1)}` : 'setValue';
310
+ ctx.lines.push(`${indent}<label className="relative inline-flex items-center cursor-pointer">`);
311
+ ctx.lines.push(`${indent} <input type="checkbox" className="sr-only peer" checked={${bind || 'value'}} onChange={(e) => ${setter}(e.target.checked)} />`);
312
+ 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
+ ctx.lines.push(`${indent}</label>`);
314
+ }
315
+ function renderGrid(node, ctx, indent) {
316
+ const p = getProps(node);
317
+ ctx.lines.push(`${indent}<div className="grid grid-cols-1 md:grid-cols-${p.cols || 1} gap-${Math.round(Number(p.gap || 16) / 4)}">`);
318
+ renderChildren(node, ctx, indent);
319
+ ctx.lines.push(`${indent}</div>`);
320
+ }
321
+ function renderConditional(node, ctx, indent) {
322
+ const cond = (getProps(node).if || 'true').replace(/&/g, ' && ').replace(/([a-zA-Z_]+)=([a-zA-Z_]+)/g, "$1 === '$2'");
323
+ ctx.lines.push(`${indent}{${cond} && (`);
324
+ ctx.lines.push(`${indent} <>`);
325
+ renderChildren(node, ctx, indent + ' ');
326
+ ctx.lines.push(`${indent} </>`);
327
+ ctx.lines.push(`${indent})}`);
328
+ }
329
+ function renderComponent(node, ctx, indent) {
330
+ const p = getProps(node);
331
+ const ref = p.ref;
332
+ if (!ref)
333
+ return;
334
+ ctx.componentImports.add(ref);
335
+ const hasOnChange = 'onChange' in p;
336
+ const attrs = [];
337
+ for (const [k, v] of Object.entries(p)) {
338
+ if (['ref', 'styles', 'pseudoStyles', 'themeRefs'].includes(k))
339
+ continue;
340
+ if (k === 'bind') {
341
+ attrs.push(`value={${v}}`);
342
+ if (!hasOnChange)
343
+ attrs.push(`onChange={set${v.charAt(0).toUpperCase() + v.slice(1)}}`);
344
+ }
345
+ else if (k === 'onChange')
346
+ attrs.push(`onChange={${v}}`);
347
+ else if (k === 'props') {
348
+ for (const pn of v.split(','))
349
+ attrs.push(`${pn.trim()}={${pn.trim()}}`);
350
+ }
351
+ else if (k === 'disabled')
352
+ attrs.push(`disabled={${v.replace(/&/g, ' && ').replace(/([a-zA-Z_]+)=([a-zA-Z_]+)/g, "$1 === '$2'")}}`);
353
+ else if (k === 'default')
354
+ attrs.push(`defaultValue={${JSON.stringify(v)}}`);
355
+ else
356
+ attrs.push(`${k}={${JSON.stringify(v)}}`);
357
+ }
358
+ ctx.lines.push(`${indent}<${ref}${attrs.length ? ' ' + attrs.join(' ') : ''} />`);
359
+ }
360
+ function renderProgress(node, ctx, indent) {
361
+ const p = getProps(node);
362
+ const current = Number(p.current || 0), target = Number(p.target || 100);
363
+ const pct = Math.round((current / target) * 100);
364
+ ctx.lines.push(`${indent}<div className="mb-3">`);
365
+ 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>`);
366
+ 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>`);
367
+ ctx.lines.push(`${indent}</div>`);
368
+ }
369
+ // ── Next.js 15 production pattern renderers ─────────────────────────────
370
+ function renderGenerateMetadata(node, ctx) {
371
+ // Collect handler code from children
372
+ let handlerCode = '';
373
+ if (node.children) {
374
+ for (const child of node.children) {
375
+ const cp = getProps(child);
376
+ if (child.type === 'handler' && cp.code) {
377
+ handlerCode = cp.code;
378
+ }
379
+ }
380
+ }
381
+ // Also check inline code prop
382
+ const p = getProps(node);
383
+ if (p.code)
384
+ handlerCode = p.code;
385
+ ctx.generateMetadataInfo = { handlerCode };
386
+ }
387
+ function renderNotFound(node, ctx, _indent) {
388
+ addNamedImport(ctx, 'next/navigation', 'notFound');
389
+ const p = getProps(node);
390
+ const condition = p.if;
391
+ if (condition) {
392
+ ctx.bodyLines.push(` if (${exprCode(condition, 'true')}) { notFound(); }`);
393
+ }
394
+ else {
395
+ ctx.bodyLines.push(` notFound();`);
396
+ }
397
+ }
398
+ function renderRedirect(node, ctx, _indent) {
399
+ addNamedImport(ctx, 'next/navigation', 'redirect');
400
+ const p = getProps(node);
401
+ const to = p.to || '/';
402
+ ctx.bodyLines.push(` redirect('${to}');`);
403
+ }
404
+ function renderImport(node, ctx) {
405
+ const p = getProps(node);
406
+ const name = p.name;
407
+ const from = p.from;
408
+ const isDefault = p.default === 'true' || p.default === true;
409
+ if (name && from) {
410
+ if (isDefault) {
411
+ addDefaultImport(ctx, from, name);
412
+ }
413
+ else {
414
+ addNamedImport(ctx, from, name);
415
+ }
416
+ }
417
+ }
418
+ function renderFetchNode(node, ctx) {
419
+ const p = getProps(node);
420
+ const name = p.name || 'data';
421
+ const url = p.url || '/api/data';
422
+ const options = p.options;
423
+ ctx.fetchCalls.push({ name, url, options });
424
+ }
425
+ // ── Main export ─────────────────────────────────────────────────────────
426
+ export function transpileNextjs(root, config) {
427
+ // Structured output path
428
+ if (config && config.structure !== 'flat') {
429
+ const plan = planStructure(root, config);
430
+ if (plan) {
431
+ return _transpileNextjsStructured(root, config, plan);
432
+ }
433
+ }
434
+ // Flat output path (default — unchanged)
435
+ return _transpileNextjsInner(root, config);
436
+ }
437
+ function _transpileNextjsInner(root, config) {
438
+ const ctx = {
439
+ lines: [],
440
+ sourceMap: [],
441
+ imports: new Map(),
442
+ componentImports: new Set(),
443
+ isClient: false,
444
+ isAsync: false,
445
+ metadata: null,
446
+ generateMetadataInfo: null,
447
+ fetchCalls: [],
448
+ bodyLines: [],
449
+ colors: config?.colors,
450
+ twProfile: config?.frameworkVersions ? buildTailwindProfile(config.frameworkVersions) : undefined,
451
+ njProfile: config?.frameworkVersions ? buildNextjsProfile(config.frameworkVersions) : undefined,
452
+ };
453
+ // Check root for client flag
454
+ const rootProps = getProps(root);
455
+ if (rootProps.client === 'true' || rootProps.client === true)
456
+ ctx.isClient = true;
457
+ if (rootProps.async === 'true' || rootProps.async === true)
458
+ ctx.isAsync = true;
459
+ // renderNode already handles metadata nodes in the switch case
460
+ renderNode(root, ctx, ' ');
461
+ // If there are fetch calls, mark page as async
462
+ if (ctx.fetchCalls.length > 0)
463
+ ctx.isAsync = true;
464
+ const name = rootProps.name || 'Page';
465
+ const isLayout = root.type === 'layout';
466
+ const isLoading = root.type === 'loading';
467
+ const isError = root.type === 'error';
468
+ const code = [];
469
+ // 'use client' directive
470
+ if (ctx.isClient) {
471
+ code.push(`'use client';`);
472
+ code.push('');
473
+ }
474
+ // Metadata type import for generateMetadata
475
+ if (ctx.generateMetadataInfo && !ctx.isClient) {
476
+ addNamedImport(ctx, 'next', 'Metadata', true);
477
+ }
478
+ // Component imports → add to unified import map
479
+ const uiLib = config?.components?.uiLibrary ?? '@/components/ui';
480
+ const compRoot = config?.components?.componentRoot ?? '@/components';
481
+ if (ctx.componentImports.size > 0) {
482
+ const uiImports = [...ctx.componentImports].filter(c => ['Icon', 'Button'].includes(c));
483
+ const others = [...ctx.componentImports].filter(c => !['Icon', 'Button'].includes(c));
484
+ for (const name of uiImports)
485
+ addNamedImport(ctx, uiLib, name);
486
+ for (const name of others)
487
+ addDefaultImport(ctx, `${compRoot}/${name}`, name);
488
+ }
489
+ // Static metadata needs Metadata type
490
+ if (ctx.metadata && !ctx.isClient && !ctx.generateMetadataInfo) {
491
+ addNamedImport(ctx, 'next', 'Metadata', true);
492
+ }
493
+ // Emit all imports (unified, sorted)
494
+ code.push(...emitImports(ctx));
495
+ if (code.length > 0 && code[code.length - 1] !== '')
496
+ code.push('');
497
+ // Metadata export (server components only) -- static metadata
498
+ if (ctx.metadata && !ctx.isClient) {
499
+ const useSatisfies = ctx.njProfile?.outputRules.metadataStyle === 'satisfies';
500
+ code.push(useSatisfies ? `export const metadata = {` : `export const metadata: Metadata = {`);
501
+ for (const [k, v] of Object.entries(ctx.metadata)) {
502
+ code.push(` ${k}: '${escapeJsString(v)}',`);
503
+ }
504
+ code.push(useSatisfies ? `} satisfies Metadata;` : `};`);
505
+ code.push('');
506
+ }
507
+ // generateMetadata export (server components only)
508
+ if (ctx.generateMetadataInfo && !ctx.isClient) {
509
+ const usePromiseParams = !ctx.njProfile || ctx.njProfile.major >= 15;
510
+ const paramsType = usePromiseParams
511
+ ? '{ params }: { params: Promise<Record<string, string>> }'
512
+ : '{ params }: { params: Record<string, string> }';
513
+ code.push('');
514
+ code.push(`export async function generateMetadata(${paramsType}): Promise<Metadata> {`);
515
+ if (ctx.generateMetadataInfo.handlerCode) {
516
+ // Split handler code by newlines and emit each line as-is
517
+ const lines = ctx.generateMetadataInfo.handlerCode.split('\n').map(s => s.trim()).filter(Boolean);
518
+ for (const line of lines) {
519
+ code.push(` ${line}`);
520
+ }
521
+ }
522
+ else {
523
+ if (usePromiseParams) {
524
+ code.push(` const resolvedParams = await params;`);
525
+ code.push(` return { title: resolvedParams.slug ?? '' };`);
526
+ }
527
+ else {
528
+ code.push(` return { title: params.slug ?? '' };`);
529
+ }
530
+ }
531
+ code.push(`}`);
532
+ code.push('');
533
+ }
534
+ // Component
535
+ if (isLayout) {
536
+ code.push(`export default function ${name}({ children }: { children: React.ReactNode }) {`);
537
+ }
538
+ else if (isError) {
539
+ code.push(`export default function ${name}({ error, reset }: { error: Error; reset: () => void }) {`);
540
+ }
541
+ else if (ctx.isAsync) {
542
+ const usePromiseParams = !ctx.njProfile || ctx.njProfile.major >= 15;
543
+ if (usePromiseParams) {
544
+ code.push(`export default async function ${name}(props: { params: Promise<Record<string, string>> }) {`);
545
+ code.push(` const params = await props.params;`);
546
+ }
547
+ else {
548
+ code.push(`export default async function ${name}({ params }: { params: Record<string, string> }) {`);
549
+ }
550
+ }
551
+ else {
552
+ code.push(`export default function ${name}() {`);
553
+ }
554
+ // Emit fetch calls (inside async function body, before return)
555
+ for (const fc of ctx.fetchCalls) {
556
+ if (fc.options) {
557
+ code.push(` const ${fc.name} = await fetch('${fc.url}', ${fc.options}).then(r => r.json());`);
558
+ }
559
+ else {
560
+ code.push(` const ${fc.name} = await fetch('${fc.url}').then(r => r.json());`);
561
+ }
562
+ }
563
+ // Emit body lines (notFound, redirect calls)
564
+ for (const line of ctx.bodyLines) {
565
+ code.push(line);
566
+ }
567
+ code.push(' return (');
568
+ code.push(...ctx.lines);
569
+ code.push(' );');
570
+ code.push('}');
571
+ const output = code.join('\n');
572
+ const irText = serializeIR(root);
573
+ const irTokenCount = countTokens(irText);
574
+ const tsTokenCount = countTokens(output);
575
+ const tokenReduction = tsTokenCount > 0 ? Math.round((1 - irTokenCount / tsTokenCount) * 100) : 0;
576
+ // Determine output filename convention
577
+ const files = [];
578
+ if (isLayout)
579
+ files.push({ path: 'layout.tsx', content: output });
580
+ else if (isLoading)
581
+ files.push({ path: 'loading.tsx', content: output });
582
+ else if (isError)
583
+ files.push({ path: 'error.tsx', content: output });
584
+ else
585
+ files.push({ path: 'page.tsx', content: output });
586
+ return {
587
+ code: output,
588
+ sourceMap: ctx.sourceMap,
589
+ irTokenCount,
590
+ tsTokenCount,
591
+ tokenReduction,
592
+ files,
593
+ };
594
+ }
595
+ // ── Structured output ────────────────────────────────────────────────────
596
+ function _renderNextjsFile(file, config) {
597
+ const ctx = {
598
+ lines: [],
599
+ sourceMap: [],
600
+ imports: new Map(),
601
+ componentImports: new Set(),
602
+ isClient: false,
603
+ isAsync: false,
604
+ metadata: null,
605
+ generateMetadataInfo: null,
606
+ fetchCalls: [],
607
+ bodyLines: [],
608
+ colors: config.colors,
609
+ twProfile: config.frameworkVersions ? buildTailwindProfile(config.frameworkVersions) : undefined,
610
+ njProfile: config.frameworkVersions ? buildNextjsProfile(config.frameworkVersions) : undefined,
611
+ };
612
+ const rootNode = file.rootNode;
613
+ const rootProps = rootNode.props || {};
614
+ // Check client flag
615
+ if (rootProps.client === 'true' || rootProps.client === true)
616
+ ctx.isClient = true;
617
+ if (rootProps.async === 'true' || rootProps.async === true)
618
+ ctx.isAsync = true;
619
+ // renderNode already handles metadata nodes in the switch case
620
+ renderNode(rootNode, ctx, ' ');
621
+ const name = file.componentName || rootProps.name || 'Component';
622
+ const isLayout = rootNode.type === 'layout';
623
+ const isError = rootNode.type === 'error';
624
+ // 'use client' only when the component actually has client-side interactivity
625
+ // (ctx.isClient is set during rendering by renderPage/renderError/client=true flag)
626
+ const code = [];
627
+ if (ctx.isClient) {
628
+ code.push(`'use client';`);
629
+ code.push('');
630
+ }
631
+ const uiLib = config.components?.uiLibrary ?? '@/components/ui';
632
+ const compRoot = config.components?.componentRoot ?? '@/components';
633
+ if (ctx.componentImports.size > 0) {
634
+ const uiImports = [...ctx.componentImports].filter(c => ['Icon', 'Button'].includes(c));
635
+ const others = [...ctx.componentImports].filter(c => !['Icon', 'Button'].includes(c));
636
+ for (const name of uiImports)
637
+ addNamedImport(ctx, uiLib, name);
638
+ for (const name of others)
639
+ addDefaultImport(ctx, `${compRoot}/${name}`, name);
640
+ }
641
+ // Metadata type import for generateMetadata
642
+ if (ctx.generateMetadataInfo && !ctx.isClient) {
643
+ addNamedImport(ctx, 'next', 'Metadata', true);
644
+ }
645
+ if (ctx.metadata && !ctx.isClient && !ctx.generateMetadataInfo) {
646
+ addNamedImport(ctx, 'next', 'Metadata', true);
647
+ }
648
+ // If there are fetch calls, mark page as async
649
+ if (ctx.fetchCalls.length > 0)
650
+ ctx.isAsync = true;
651
+ code.push(...emitImports(ctx));
652
+ if (code.length > 0 && code[code.length - 1] !== '')
653
+ code.push('');
654
+ // Metadata (server components only)
655
+ if (ctx.metadata && !ctx.isClient) {
656
+ const useSatisfies = ctx.njProfile?.outputRules.metadataStyle === 'satisfies';
657
+ code.push(useSatisfies ? `export const metadata = {` : `export const metadata: Metadata = {`);
658
+ for (const [k, v] of Object.entries(ctx.metadata)) {
659
+ code.push(` ${k}: '${escapeJsString(v)}',`);
660
+ }
661
+ code.push(useSatisfies ? `} satisfies Metadata;` : `};`);
662
+ code.push('');
663
+ }
664
+ // generateMetadata export (server components only)
665
+ if (ctx.generateMetadataInfo && !ctx.isClient) {
666
+ const usePromiseParams = !ctx.njProfile || ctx.njProfile.major >= 15;
667
+ const paramsType = usePromiseParams
668
+ ? '{ params }: { params: Promise<Record<string, string>> }'
669
+ : '{ params }: { params: Record<string, string> }';
670
+ code.push('');
671
+ code.push(`export async function generateMetadata(${paramsType}): Promise<Metadata> {`);
672
+ if (ctx.generateMetadataInfo.handlerCode) {
673
+ const lines = ctx.generateMetadataInfo.handlerCode.split('\n').map(s => s.trim()).filter(Boolean);
674
+ for (const line of lines) {
675
+ code.push(` ${line}`);
676
+ }
677
+ }
678
+ else {
679
+ if (usePromiseParams) {
680
+ code.push(` const resolvedParams = await params;`);
681
+ code.push(` return { title: resolvedParams.slug ?? '' };`);
682
+ }
683
+ else {
684
+ code.push(` return { title: params.slug ?? '' };`);
685
+ }
686
+ }
687
+ code.push(`}`);
688
+ code.push('');
689
+ }
690
+ if (isLayout) {
691
+ code.push(`export default function ${name}({ children }: { children: React.ReactNode }) {`);
692
+ }
693
+ else if (isError) {
694
+ code.push(`export default function ${name}({ error, reset }: { error: Error; reset: () => void }) {`);
695
+ }
696
+ else if (ctx.isAsync) {
697
+ const usePromiseParams = !ctx.njProfile || ctx.njProfile.major >= 15;
698
+ if (usePromiseParams) {
699
+ code.push(`export default async function ${name}(props: { params: Promise<Record<string, string>> }) {`);
700
+ code.push(` const params = await props.params;`);
701
+ }
702
+ else {
703
+ code.push(`export default async function ${name}({ params }: { params: Record<string, string> }) {`);
704
+ }
705
+ }
706
+ else {
707
+ code.push(`export default function ${name}() {`);
708
+ }
709
+ // Emit fetch calls (inside async function body, before return)
710
+ for (const fc of ctx.fetchCalls) {
711
+ if (fc.options) {
712
+ code.push(` const ${fc.name} = await fetch('${fc.url}', ${fc.options}).then(r => r.json());`);
713
+ }
714
+ else {
715
+ code.push(` const ${fc.name} = await fetch('${fc.url}').then(r => r.json());`);
716
+ }
717
+ }
718
+ // Emit body lines (notFound, redirect calls)
719
+ for (const line of ctx.bodyLines) {
720
+ code.push(line);
721
+ }
722
+ code.push(' return (');
723
+ code.push(...ctx.lines);
724
+ code.push(' );');
725
+ code.push('}');
726
+ return code.join('\n');
727
+ }
728
+ function _transpileNextjsStructured(root, config, plan) {
729
+ const { entryCode, artifacts } = buildStructuredArtifacts(plan, (file, cfg) => _renderNextjsFile(file, cfg), root, config);
730
+ const irText = serializeIR(root);
731
+ const irTokenCount = countTokens(irText);
732
+ const tsTokenCount = countTokens(entryCode);
733
+ const tokenReduction = tsTokenCount > 0 ? Math.round((1 - irTokenCount / tsTokenCount) * 100) : 0;
734
+ // Convert artifacts to NextFile[] for files property
735
+ const files = artifacts
736
+ .filter(a => a.path.endsWith('.tsx'))
737
+ .map(a => ({ path: a.path, content: a.content }));
738
+ return {
739
+ code: entryCode,
740
+ sourceMap: [],
741
+ irTokenCount,
742
+ tsTokenCount,
743
+ tokenReduction,
744
+ files,
745
+ artifacts,
746
+ };
747
+ }
748
+ //# sourceMappingURL=transpiler-nextjs.js.map