@kernlang/core 3.1.7 → 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.
@@ -0,0 +1,1135 @@
1
+ /**
2
+ * TypeScript → .kern Importer
3
+ *
4
+ * Reads TypeScript source and produces .kern output by recognizing structural patterns:
5
+ * type aliases, interfaces, functions, classes (→ service/error), constants, imports.
6
+ * Function/method bodies become <<<>>> handler blocks.
7
+ * JSDoc comments become doc nodes.
8
+ *
9
+ * Uses the TypeScript compiler API (already a dependency) — no ts-morph needed.
10
+ */
11
+ import ts from 'typescript';
12
+ // ── Helpers ──────────────────────────────────────────────────────────────
13
+ function isExported(node) {
14
+ const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
15
+ return modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
16
+ }
17
+ function isAsync(node) {
18
+ const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
19
+ return modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false;
20
+ }
21
+ function isStatic(node) {
22
+ const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
23
+ return modifiers?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword) ?? false;
24
+ }
25
+ function isPrivate(node) {
26
+ const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
27
+ return modifiers?.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword) ?? false;
28
+ }
29
+ function isReadonly(node) {
30
+ const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
31
+ return modifiers?.some((m) => m.kind === ts.SyntaxKind.ReadonlyKeyword) ?? false;
32
+ }
33
+ function _isDefault(node) {
34
+ const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
35
+ return modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword) ?? false;
36
+ }
37
+ function typeToString(typeNode, source) {
38
+ if (!typeNode)
39
+ return '';
40
+ return typeNode.getText(source);
41
+ }
42
+ function getJSDoc(node, source) {
43
+ const jsDocs = node.jsDoc;
44
+ if (!jsDocs || jsDocs.length === 0)
45
+ return undefined;
46
+ const doc = jsDocs[0];
47
+ const text = doc.comment;
48
+ if (typeof text === 'string')
49
+ return text.trim();
50
+ if (Array.isArray(text)) {
51
+ return text
52
+ .map((part) => (typeof part === 'string' ? part : part.getText(source)))
53
+ .join('')
54
+ .trim();
55
+ }
56
+ return undefined;
57
+ }
58
+ function _indent(lines, depth) {
59
+ const prefix = ' '.repeat(depth);
60
+ return lines.map((l) => `${prefix}${l}`);
61
+ }
62
+ function escapeKernString(s) {
63
+ return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
64
+ }
65
+ function formatParams(params, source) {
66
+ return params
67
+ .map((p) => {
68
+ const name = p.name.getText(source);
69
+ const type = typeToString(p.type, source);
70
+ const optional = p.questionToken ? '?' : '';
71
+ const defaultVal = p.initializer ? `=${p.initializer.getText(source)}` : '';
72
+ return type ? `${name}${optional}:${type}${defaultVal}` : `${name}${optional}${defaultVal}`;
73
+ })
74
+ .join(',');
75
+ }
76
+ function getBodyText(body, source) {
77
+ if (!body)
78
+ return undefined;
79
+ if (ts.isBlock(body)) {
80
+ const statements = body.statements;
81
+ if (statements.length === 0)
82
+ return undefined;
83
+ // Get the text between the braces
84
+ const fullText = body.getText(source);
85
+ // Strip outer { }
86
+ const inner = fullText.slice(1, -1);
87
+ return dedentBlock(inner);
88
+ }
89
+ // Arrow function expression body
90
+ return body.getText(source);
91
+ }
92
+ function dedentBlock(text) {
93
+ const lines = text.split('\n');
94
+ // Find minimum indentation (ignoring empty lines)
95
+ let minIndent = Infinity;
96
+ for (const line of lines) {
97
+ if (line.trim().length === 0)
98
+ continue;
99
+ const leading = line.match(/^(\s*)/)?.[1].length ?? 0;
100
+ minIndent = Math.min(minIndent, leading);
101
+ }
102
+ if (minIndent === Infinity || minIndent === 0)
103
+ return text.trim();
104
+ return lines
105
+ .map((line) => (line.trim().length === 0 ? '' : line.slice(minIndent)))
106
+ .join('\n')
107
+ .trim();
108
+ }
109
+ // ── Node converters ─────────────────────────────────────────────────────
110
+ function convertImport(node, source) {
111
+ const lines = [];
112
+ const moduleSpec = node.moduleSpecifier.getText(source).replace(/['"]/g, '');
113
+ const clause = node.importClause;
114
+ if (!clause) {
115
+ // Side-effect import: import './setup'
116
+ lines.push(`import from="${moduleSpec}"`);
117
+ return lines;
118
+ }
119
+ const parts = [`import from="${moduleSpec}"`];
120
+ const isTypeOnly = clause.isTypeOnly;
121
+ if (clause.name) {
122
+ // Default import
123
+ parts.push(`default=${clause.name.getText(source)}`);
124
+ }
125
+ let hasTypeOnlySpecifiers = false;
126
+ if (clause.namedBindings) {
127
+ if (ts.isNamedImports(clause.namedBindings)) {
128
+ const names = clause.namedBindings.elements
129
+ .map((e) => {
130
+ // Strip 'type' modifier and 'as Alias' — use the local name only
131
+ if (e.isTypeOnly)
132
+ hasTypeOnlySpecifiers = true;
133
+ return e.name.getText(source);
134
+ })
135
+ .join(',');
136
+ parts.push(`names="${names}"`);
137
+ }
138
+ }
139
+ if (isTypeOnly || hasTypeOnlySpecifiers) {
140
+ parts.push('types=true');
141
+ }
142
+ lines.push(parts.join(' '));
143
+ return lines;
144
+ }
145
+ function convertTypeAlias(node, source) {
146
+ const lines = [];
147
+ const name = node.name.getText(source);
148
+ const exp = isExported(node) ? ' export=true' : '';
149
+ const doc = getJSDoc(node, source);
150
+ if (doc)
151
+ lines.push(`doc text="${escapeKernString(doc)}"`);
152
+ // Check for string literal union: type X = 'a' | 'b' | 'c'
153
+ if (ts.isUnionTypeNode(node.type)) {
154
+ const members = node.type.types;
155
+ const allStringLiterals = members.every((m) => ts.isLiteralTypeNode(m) && m.literal.kind === ts.SyntaxKind.StringLiteral);
156
+ if (allStringLiterals) {
157
+ const values = members.map((m) => m.literal.text).join('|');
158
+ lines.push(`type name=${name} values="${values}"${exp}`);
159
+ return lines;
160
+ }
161
+ }
162
+ // General type alias
163
+ const typeText = typeToString(node.type, source);
164
+ lines.push(`type name=${name} alias="${escapeKernString(typeText)}"${exp}`);
165
+ return lines;
166
+ }
167
+ function convertInterface(node, source) {
168
+ const lines = [];
169
+ const name = node.name.getText(source);
170
+ const exp = isExported(node) ? ' export=true' : '';
171
+ const doc = getJSDoc(node, source);
172
+ if (doc)
173
+ lines.push(`doc text="${escapeKernString(doc)}"`);
174
+ const extends_ = node.heritageClauses
175
+ ?.filter((h) => h.token === ts.SyntaxKind.ExtendsKeyword)
176
+ .flatMap((h) => h.types.map((t) => t.getText(source)));
177
+ const extendsStr = extends_ && extends_.length > 0 ? ` extends=${extends_.join(',')}` : '';
178
+ lines.push(`interface name=${name}${extendsStr}${exp}`);
179
+ for (const member of node.members) {
180
+ if (ts.isPropertySignature(member)) {
181
+ const fieldName = member.name.getText(source);
182
+ const fieldType = typeToString(member.type, source);
183
+ const optional = member.questionToken ? ' optional=true' : '';
184
+ const fieldDoc = getJSDoc(member, source);
185
+ if (fieldDoc)
186
+ lines.push(` doc text="${escapeKernString(fieldDoc)}"`);
187
+ lines.push(` field name=${fieldName}${fieldType ? ` type=${fieldType}` : ''}${optional}`);
188
+ }
189
+ else if (ts.isMethodSignature(member)) {
190
+ // Interface method signatures → field with function type (schema only allows field children)
191
+ const methodName = member.name.getText(source);
192
+ const params = member.parameters ? formatParams(member.parameters, source) : '';
193
+ const returns = typeToString(member.type, source) || 'void';
194
+ const funcType = `(${params}) => ${returns}`;
195
+ const optional = member.questionToken ? ' optional=true' : '';
196
+ lines.push(` field name=${methodName} type="${escapeKernString(funcType)}"${optional}`);
197
+ }
198
+ }
199
+ return lines;
200
+ }
201
+ function convertEnum(node, source) {
202
+ const lines = [];
203
+ const name = node.name.getText(source);
204
+ const exp = isExported(node) ? ' export=true' : '';
205
+ const doc = getJSDoc(node, source);
206
+ if (doc)
207
+ lines.push(`doc text="${escapeKernString(doc)}"`);
208
+ // Check if all members are string literals → type with values
209
+ const allString = node.members.every((m) => m.initializer && ts.isStringLiteral(m.initializer));
210
+ if (allString) {
211
+ const values = node.members.map((m) => m.initializer.text).join('|');
212
+ lines.push(`type name=${name} values="${values}"${exp}`);
213
+ }
214
+ else {
215
+ // Numeric or mixed enum → type alias
216
+ const values = node.members.map((m) => m.name.getText(source)).join('|');
217
+ lines.push(`type name=${name} values="${values}"${exp}`);
218
+ }
219
+ return lines;
220
+ }
221
+ function convertFunction(node, source) {
222
+ const lines = [];
223
+ const name = node.name?.getText(source) ?? 'anonymous';
224
+ const exp = isExported(node) ? ' export=true' : '';
225
+ const doc = getJSDoc(node, source);
226
+ const asyncStr = isAsync(node) ? ' async=true' : '';
227
+ const isGenerator = node.asteriskToken != null;
228
+ const generatorStr = isGenerator ? (isAsync(node) ? ' stream=true' : ' generator=true') : '';
229
+ if (doc)
230
+ lines.push(`doc text="${escapeKernString(doc)}"`);
231
+ const params = formatParams(node.parameters, source);
232
+ const returns = typeToString(node.type, source);
233
+ const paramsStr = params ? ` params="${params}"` : '';
234
+ const returnsStr = returns ? ` returns=${returns}` : '';
235
+ // For async generators, use stream=true instead of async=true + generator=true
236
+ const asyncFinal = isGenerator && isAsync(node) ? '' : asyncStr;
237
+ lines.push(`fn name=${name}${paramsStr}${returnsStr}${asyncFinal}${generatorStr}${exp}`);
238
+ const body = getBodyText(node.body, source);
239
+ if (body) {
240
+ lines.push(' handler <<<');
241
+ for (const bodyLine of body.split('\n')) {
242
+ lines.push(` ${bodyLine}`);
243
+ }
244
+ lines.push(' >>>');
245
+ }
246
+ return lines;
247
+ }
248
+ function convertClass(node, source) {
249
+ const lines = [];
250
+ const name = node.name?.getText(source) ?? 'AnonymousClass';
251
+ const exp = isExported(node) ? ' export=true' : '';
252
+ const doc = getJSDoc(node, source);
253
+ // Check if it extends Error → error node
254
+ const extendsClause = node.heritageClauses?.find((h) => h.token === ts.SyntaxKind.ExtendsKeyword);
255
+ const baseClass = extendsClause?.types[0]?.getText(source);
256
+ const isError = baseClass && (baseClass === 'Error' || baseClass.endsWith('Error'));
257
+ if (doc)
258
+ lines.push(`doc text="${escapeKernString(doc)}"`);
259
+ if (isError) {
260
+ return convertErrorClass(node, source, name, baseClass, exp, lines);
261
+ }
262
+ // Regular class → service
263
+ const implementsClause = node.heritageClauses?.find((h) => h.token === ts.SyntaxKind.ImplementsKeyword);
264
+ const implementsStr = implementsClause
265
+ ? ` implements=${implementsClause.types.map((t) => t.getText(source)).join(',')}`
266
+ : '';
267
+ lines.push(`service name=${name}${implementsStr}${exp}`);
268
+ for (const member of node.members) {
269
+ if (ts.isPropertyDeclaration(member)) {
270
+ const fieldName = member.name.getText(source);
271
+ const fieldType = typeToString(member.type, source);
272
+ const priv = isPrivate(member) ? ' private=true' : '';
273
+ const ro = isReadonly(member) ? ' readonly=true' : '';
274
+ const defaultVal = member.initializer ? ` default=${member.initializer.getText(source)}` : '';
275
+ const memberDoc = getJSDoc(member, source);
276
+ if (memberDoc)
277
+ lines.push(` doc text="${escapeKernString(memberDoc)}"`);
278
+ lines.push(` field name=${fieldName}${fieldType ? ` type=${fieldType}` : ''}${priv}${ro}${defaultVal}`);
279
+ }
280
+ else if (ts.isConstructorDeclaration(member)) {
281
+ lines.push(' constructor');
282
+ const body = getBodyText(member.body, source);
283
+ if (body) {
284
+ lines.push(' handler <<<');
285
+ for (const bodyLine of body.split('\n')) {
286
+ lines.push(` ${bodyLine}`);
287
+ }
288
+ lines.push(' >>>');
289
+ }
290
+ }
291
+ else if (ts.isMethodDeclaration(member)) {
292
+ const methodName = member.name.getText(source);
293
+ const params = formatParams(member.parameters, source);
294
+ const returns = typeToString(member.type, source);
295
+ const asyncStr = isAsync(member) ? ' async=true' : '';
296
+ const staticStr = isStatic(member) ? ' static=true' : '';
297
+ const privStr = isPrivate(member) ? ' private=true' : '';
298
+ const paramsStr = params ? ` params="${params}"` : '';
299
+ const returnsStr = returns ? ` returns=${returns}` : '';
300
+ const memberDoc = getJSDoc(member, source);
301
+ if (memberDoc)
302
+ lines.push(` doc text="${escapeKernString(memberDoc)}"`);
303
+ lines.push(` method name=${methodName}${paramsStr}${returnsStr}${asyncStr}${staticStr}${privStr}`);
304
+ const body = getBodyText(member.body, source);
305
+ if (body) {
306
+ lines.push(' handler <<<');
307
+ for (const bodyLine of body.split('\n')) {
308
+ lines.push(` ${bodyLine}`);
309
+ }
310
+ lines.push(' >>>');
311
+ }
312
+ }
313
+ }
314
+ return lines;
315
+ }
316
+ function convertErrorClass(node, source, name, baseClass, exp, lines) {
317
+ // Find constructor to extract message
318
+ const ctor = node.members.find(ts.isConstructorDeclaration);
319
+ let message = '';
320
+ if (ctor) {
321
+ // Look for super() call to extract message
322
+ const superCall = ctor.body?.statements.find((s) => ts.isExpressionStatement(s) &&
323
+ ts.isCallExpression(s.expression) &&
324
+ s.expression.expression.kind === ts.SyntaxKind.SuperKeyword);
325
+ if (superCall && ts.isExpressionStatement(superCall)) {
326
+ const call = superCall.expression;
327
+ if (call.arguments.length > 0) {
328
+ message = call.arguments[0].getText(source);
329
+ }
330
+ }
331
+ }
332
+ const messageStr = message ? ` message="${escapeKernString(message)}"` : '';
333
+ lines.push(`error name=${name} extends=${baseClass}${messageStr}${exp}`);
334
+ // Add fields (constructor params that are public)
335
+ if (ctor) {
336
+ for (const param of ctor.parameters) {
337
+ const modifiers = ts.canHaveModifiers(param) ? ts.getModifiers(param) : undefined;
338
+ const isPublicOrReadonly = modifiers?.some((m) => m.kind === ts.SyntaxKind.PublicKeyword || m.kind === ts.SyntaxKind.ReadonlyKeyword);
339
+ if (isPublicOrReadonly) {
340
+ const fieldName = param.name.getText(source);
341
+ const fieldType = typeToString(param.type, source);
342
+ lines.push(` field name=${fieldName}${fieldType ? ` type=${fieldType}` : ''}`);
343
+ }
344
+ }
345
+ }
346
+ return lines;
347
+ }
348
+ function convertVariableStatement(node, source) {
349
+ const lines = [];
350
+ const exp = isExported(node) ? ' export=true' : '';
351
+ const doc = getJSDoc(node, source);
352
+ for (const decl of node.declarationList.declarations) {
353
+ const name = decl.name.getText(source);
354
+ const type = typeToString(decl.type, source);
355
+ const typeStr = type ? ` type=${type}` : '';
356
+ if (doc)
357
+ lines.push(`doc text="${escapeKernString(doc)}"`);
358
+ if (decl.initializer) {
359
+ // Check if it's a simple value (number, string, boolean, etc.)
360
+ const initText = decl.initializer.getText(source);
361
+ const isSimple = ts.isNumericLiteral(decl.initializer) ||
362
+ ts.isStringLiteral(decl.initializer) ||
363
+ decl.initializer.kind === ts.SyntaxKind.TrueKeyword ||
364
+ decl.initializer.kind === ts.SyntaxKind.FalseKeyword ||
365
+ decl.initializer.kind === ts.SyntaxKind.NullKeyword;
366
+ if (isSimple) {
367
+ lines.push(`const name=${name}${typeStr} value=${initText}${exp}`);
368
+ }
369
+ else if (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer)) {
370
+ // Arrow function or function expression → fn
371
+ const func = decl.initializer;
372
+ const asyncStr = isAsync(func) ? ' async=true' : '';
373
+ const params = formatParams(func.parameters, source);
374
+ const returns = typeToString(func.type, source);
375
+ const paramsStr = params ? ` params="${params}"` : '';
376
+ const returnsStr = returns ? ` returns=${returns}` : '';
377
+ const isGen = ts.isFunctionExpression(func) && func.asteriskToken != null;
378
+ const genStr = isGen ? (isAsync(func) ? ' stream=true' : ' generator=true') : '';
379
+ const asyncFinal = isGen && isAsync(func) ? '' : asyncStr;
380
+ lines.push(`fn name=${name}${paramsStr}${returnsStr}${asyncFinal}${genStr}${exp}`);
381
+ const body = ts.isArrowFunction(func)
382
+ ? getBodyText(func.body, source)
383
+ : getBodyText(func.body, source);
384
+ if (body) {
385
+ lines.push(' handler <<<');
386
+ for (const bodyLine of body.split('\n')) {
387
+ lines.push(` ${bodyLine}`);
388
+ }
389
+ lines.push(' >>>');
390
+ }
391
+ }
392
+ else {
393
+ // Complex initializer → const with handler
394
+ lines.push(`const name=${name}${typeStr}${exp}`);
395
+ lines.push(' handler <<<');
396
+ for (const initLine of initText.split('\n')) {
397
+ lines.push(` ${initLine}`);
398
+ }
399
+ lines.push(' >>>');
400
+ }
401
+ }
402
+ else {
403
+ lines.push(`const name=${name}${typeStr}${exp}`);
404
+ }
405
+ }
406
+ return lines;
407
+ }
408
+ // ── Tailwind → KERN style reverse mapping ───────────────────────────────
409
+ const TW_TO_KERN_STYLE = {
410
+ // Flexbox
411
+ flex: ['fd', 'row'],
412
+ 'flex-col': ['fd', 'column'],
413
+ 'flex-row': ['fd', 'row'],
414
+ 'items-center': ['ai', 'center'],
415
+ 'items-start': ['ai', 'start'],
416
+ 'items-end': ['ai', 'end'],
417
+ 'items-stretch': ['ai', 'stretch'],
418
+ 'justify-center': ['jc', 'center'],
419
+ 'justify-between': ['jc', 'sb'],
420
+ 'justify-around': ['jc', 'sa'],
421
+ 'justify-evenly': ['jc', 'se'],
422
+ 'justify-start': ['jc', 'start'],
423
+ 'justify-end': ['jc', 'end'],
424
+ // Font
425
+ 'font-bold': ['fw', 'bold'],
426
+ 'font-semibold': ['fw', '600'],
427
+ 'font-medium': ['fw', '500'],
428
+ 'font-normal': ['fw', '400'],
429
+ 'font-light': ['fw', '300'],
430
+ 'text-center': ['ta', 'center'],
431
+ 'text-left': ['ta', 'left'],
432
+ 'text-right': ['ta', 'right'],
433
+ // Width/height
434
+ 'w-full': ['w', 'full'],
435
+ 'h-full': ['h', 'full'],
436
+ };
437
+ /** Match spacing utilities: p-4, px-2, mt-8, gap-4, etc. */
438
+ const TW_SPACING_RE = /^(p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|gap)-(\d+)$/;
439
+ /** Match text size: text-sm, text-lg, text-xl, text-2xl */
440
+ const TW_TEXTSIZE_RE = /^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl)$/;
441
+ /** Match rounded: rounded, rounded-md, rounded-lg, rounded-full */
442
+ const TW_ROUNDED_RE = /^rounded(?:-(sm|md|lg|xl|2xl|full|none))?$/;
443
+ /** Match bg/text colors: bg-blue-500, text-gray-700 */
444
+ const TW_COLOR_RE = /^(bg|text|border)-([a-z]+-\d+|white|black|transparent)$/;
445
+ function parseTailwindClasses(className) {
446
+ const styles = {};
447
+ const remaining = [];
448
+ for (const cls of className.split(/\s+/).filter(Boolean)) {
449
+ const known = TW_TO_KERN_STYLE[cls];
450
+ if (known) {
451
+ styles[known[0]] = known[1];
452
+ continue;
453
+ }
454
+ const spacingMatch = cls.match(TW_SPACING_RE);
455
+ if (spacingMatch) {
456
+ const prop = spacingMatch[1] === 'gap' ? 'gap' : spacingMatch[1];
457
+ styles[prop] = spacingMatch[2];
458
+ continue;
459
+ }
460
+ const textMatch = cls.match(TW_TEXTSIZE_RE);
461
+ if (textMatch) {
462
+ const sizes = {
463
+ xs: '12',
464
+ sm: '14',
465
+ base: '16',
466
+ lg: '18',
467
+ xl: '20',
468
+ '2xl': '24',
469
+ '3xl': '30',
470
+ '4xl': '36',
471
+ '5xl': '48',
472
+ };
473
+ styles.fs = sizes[textMatch[1]] || textMatch[1];
474
+ continue;
475
+ }
476
+ const roundedMatch = cls.match(TW_ROUNDED_RE);
477
+ if (roundedMatch) {
478
+ const vals = {
479
+ sm: '2',
480
+ md: '6',
481
+ lg: '8',
482
+ xl: '12',
483
+ '2xl': '16',
484
+ full: '9999',
485
+ none: '0',
486
+ };
487
+ styles.br = vals[roundedMatch[1] ?? 'md'] ?? '4';
488
+ continue;
489
+ }
490
+ const colorMatch = cls.match(TW_COLOR_RE);
491
+ if (colorMatch) {
492
+ const prop = colorMatch[1] === 'bg' ? 'bg' : colorMatch[1] === 'text' ? 'c' : 'bc';
493
+ styles[prop] = colorMatch[2];
494
+ continue;
495
+ }
496
+ remaining.push(cls);
497
+ }
498
+ return { styles, remaining };
499
+ }
500
+ function formatKernStyles(styles) {
501
+ if (Object.keys(styles).length === 0)
502
+ return '';
503
+ return ` {${Object.entries(styles)
504
+ .map(([k, v]) => `${k}:${v}`)
505
+ .join(', ')}}`;
506
+ }
507
+ // ── JSX → KERN conversion ───────────────────────────────────────────────
508
+ /** Map HTML/React element tags to KERN node types */
509
+ const JSX_TAG_MAP = {
510
+ div: 'row',
511
+ span: 'text',
512
+ p: 'text',
513
+ h1: 'text',
514
+ h2: 'text',
515
+ h3: 'text',
516
+ h4: 'text',
517
+ h5: 'text',
518
+ h6: 'text',
519
+ button: 'button',
520
+ input: 'input',
521
+ textarea: 'textarea',
522
+ img: 'image',
523
+ a: 'link',
524
+ form: 'form',
525
+ section: 'section',
526
+ nav: 'header',
527
+ header: 'header',
528
+ footer: 'section',
529
+ ul: 'list',
530
+ ol: 'list',
531
+ li: 'item',
532
+ table: 'table',
533
+ tr: 'tr',
534
+ th: 'th',
535
+ td: 'td',
536
+ select: 'select',
537
+ option: 'option',
538
+ label: 'text',
539
+ main: 'section',
540
+ article: 'card',
541
+ aside: 'section',
542
+ modal: 'modal',
543
+ };
544
+ function convertJsxElement(node, source, depth) {
545
+ const lines = [];
546
+ const prefix = ' '.repeat(depth);
547
+ if (ts.isJsxElement(node)) {
548
+ const tag = node.openingElement.tagName.getText(source);
549
+ const attrs = node.openingElement.attributes;
550
+ lines.push(...convertJsxTag(tag, attrs, node.children, source, depth));
551
+ }
552
+ else if (ts.isJsxSelfClosingElement(node)) {
553
+ const tag = node.tagName.getText(source);
554
+ const attrs = node.attributes;
555
+ lines.push(...convertJsxTag(tag, attrs, [], source, depth));
556
+ }
557
+ else if (ts.isJsxExpression(node)) {
558
+ if (node.expression) {
559
+ // {variable} → text expression
560
+ // {cond && <el>} → conditional
561
+ // {items.map(i => <el>)} → each
562
+ if (ts.isBinaryExpression(node.expression) &&
563
+ node.expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
564
+ // {show && <Component>} → conditional
565
+ const condition = node.expression.left.getText(source);
566
+ lines.push(`${prefix}conditional expr="${escapeKernString(condition)}"`);
567
+ const right = node.expression.right;
568
+ if (ts.isJsxElement(right) || ts.isJsxSelfClosingElement(right) || ts.isParenthesizedExpression(right)) {
569
+ const inner = ts.isParenthesizedExpression(right) ? right.expression : right;
570
+ lines.push(...convertJsxElement(inner, source, depth + 1));
571
+ }
572
+ }
573
+ else if (ts.isCallExpression(node.expression)) {
574
+ // Check for .map() pattern → each
575
+ const callText = node.expression.getText(source);
576
+ if (ts.isPropertyAccessExpression(node.expression.expression) &&
577
+ node.expression.expression.name.getText(source) === 'map') {
578
+ const collection = node.expression.expression.expression.getText(source);
579
+ const callback = node.expression.arguments[0];
580
+ if (callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) {
581
+ const paramName = callback.parameters[0]?.name.getText(source) ?? 'item';
582
+ const indexParam = callback.parameters[1]?.name.getText(source);
583
+ const indexStr = indexParam ? ` index=${indexParam}` : '';
584
+ lines.push(`${prefix}each name=${paramName} in=${collection}${indexStr}`);
585
+ // Convert the body
586
+ const body = callback.body;
587
+ if (ts.isBlock(body)) {
588
+ // Block body with return
589
+ for (const stmt of body.statements) {
590
+ if (ts.isReturnStatement(stmt) && stmt.expression) {
591
+ if (ts.isParenthesizedExpression(stmt.expression)) {
592
+ lines.push(...convertJsxElement(stmt.expression.expression, source, depth + 1));
593
+ }
594
+ else {
595
+ lines.push(...convertJsxElement(stmt.expression, source, depth + 1));
596
+ }
597
+ }
598
+ }
599
+ }
600
+ else if (ts.isParenthesizedExpression(body)) {
601
+ lines.push(...convertJsxElement(body.expression, source, depth + 1));
602
+ }
603
+ else if (ts.isJsxElement(body) || ts.isJsxSelfClosingElement(body)) {
604
+ lines.push(...convertJsxElement(body, source, depth + 1));
605
+ }
606
+ }
607
+ else {
608
+ lines.push(`${prefix}// {${callText.slice(0, 60)}}`);
609
+ }
610
+ }
611
+ else {
612
+ lines.push(`${prefix}// {${callText.slice(0, 60)}}`);
613
+ }
614
+ }
615
+ else if (ts.isConditionalExpression(node.expression)) {
616
+ // {cond ? <A> : <B>} → branch
617
+ const condition = node.expression.condition.getText(source);
618
+ lines.push(`${prefix}branch name=cond on="${escapeKernString(condition)}"`);
619
+ lines.push(`${prefix} path value=true`);
620
+ const whenTrue = ts.isParenthesizedExpression(node.expression.whenTrue)
621
+ ? node.expression.whenTrue.expression
622
+ : node.expression.whenTrue;
623
+ lines.push(...convertJsxElement(whenTrue, source, depth + 2));
624
+ lines.push(`${prefix} path value=false`);
625
+ const whenFalse = ts.isParenthesizedExpression(node.expression.whenFalse)
626
+ ? node.expression.whenFalse.expression
627
+ : node.expression.whenFalse;
628
+ lines.push(...convertJsxElement(whenFalse, source, depth + 2));
629
+ }
630
+ else {
631
+ // Simple expression: {variable} or {expr}
632
+ const expr = node.expression.getText(source);
633
+ lines.push(`${prefix}text @{${expr}}`);
634
+ }
635
+ }
636
+ }
637
+ else if (ts.isJsxText(node)) {
638
+ const text = node.getText(source).trim();
639
+ if (text) {
640
+ lines.push(`${prefix}text "${escapeKernString(text)}"`);
641
+ }
642
+ }
643
+ else if (ts.isJsxFragment(node)) {
644
+ // <> ... </> → just process children
645
+ for (const child of node.children) {
646
+ lines.push(...convertJsxElement(child, source, depth));
647
+ }
648
+ }
649
+ return lines;
650
+ }
651
+ function convertJsxTag(tag, attrs, children, source, depth) {
652
+ const lines = [];
653
+ const prefix = ' '.repeat(depth);
654
+ const kernTag = JSX_TAG_MAP[tag];
655
+ // Extract props
656
+ let className = '';
657
+ const _styleStr = '';
658
+ const props = [];
659
+ const events = [];
660
+ for (const attr of attrs.properties) {
661
+ if (ts.isJsxAttribute(attr)) {
662
+ const attrName = attr.name.getText(source);
663
+ const attrValue = attr.initializer;
664
+ if (attrName === 'className' || attrName === 'class') {
665
+ if (attrValue && ts.isStringLiteral(attrValue)) {
666
+ className = attrValue.text;
667
+ }
668
+ else if (attrValue && ts.isJsxExpression(attrValue) && attrValue.expression) {
669
+ className = attrValue.expression.getText(source);
670
+ }
671
+ continue;
672
+ }
673
+ if (attrName.startsWith('on') && attrName.length > 2) {
674
+ const eventName = attrName.slice(2).toLowerCase();
675
+ let handlerText = '';
676
+ if (attrValue && ts.isJsxExpression(attrValue) && attrValue.expression) {
677
+ handlerText = attrValue.expression.getText(source);
678
+ }
679
+ events.push({ event: eventName, handler: handlerText });
680
+ continue;
681
+ }
682
+ // Regular props
683
+ if (attrValue) {
684
+ if (ts.isStringLiteral(attrValue)) {
685
+ props.push(`${attrName}="${attrValue.text}"`);
686
+ }
687
+ else if (ts.isJsxExpression(attrValue) && attrValue.expression) {
688
+ props.push(`${attrName}=${attrValue.expression.getText(source)}`);
689
+ }
690
+ }
691
+ else {
692
+ // Boolean prop: <input disabled />
693
+ props.push(`${attrName}=true`);
694
+ }
695
+ }
696
+ }
697
+ // Parse Tailwind classes → KERN styles
698
+ let kernStyles = '';
699
+ let remainingClasses = [];
700
+ if (className && !className.includes('`') && !className.includes('$')) {
701
+ const parsed = parseTailwindClasses(className);
702
+ kernStyles = formatKernStyles(parsed.styles);
703
+ remainingClasses = parsed.remaining;
704
+ }
705
+ if (kernTag) {
706
+ // Known HTML tag → KERN node
707
+ let line = `${prefix}${kernTag}`;
708
+ // Special prop handling per tag
709
+ if (kernTag === 'link' && props.some((p) => p.startsWith('href='))) {
710
+ const href = props.find((p) => p.startsWith('href='));
711
+ if (href)
712
+ line += ` to=${href.slice(5)}`;
713
+ }
714
+ else if (kernTag === 'input') {
715
+ const valueProp = props.find((p) => p.startsWith('value='));
716
+ if (valueProp)
717
+ line += ` bind=${valueProp.slice(6)}`;
718
+ const placeholder = props.find((p) => p.startsWith('placeholder='));
719
+ if (placeholder)
720
+ line += ` ${placeholder}`;
721
+ }
722
+ else if (kernTag === 'image') {
723
+ const src = props.find((p) => p.startsWith('src='));
724
+ if (src)
725
+ line += ` ${src}`;
726
+ const alt = props.find((p) => p.startsWith('alt='));
727
+ if (alt)
728
+ line += ` ${alt}`;
729
+ }
730
+ line += kernStyles;
731
+ if (remainingClasses.length > 0) {
732
+ line += ` // tw: ${remainingClasses.join(' ')}`;
733
+ }
734
+ lines.push(line);
735
+ }
736
+ else if (tag[0] === tag[0].toUpperCase()) {
737
+ // PascalCase → component reference
738
+ let line = `${prefix}component ref=${tag}`;
739
+ const propNames = props.map((p) => p.split('=')[0]);
740
+ if (propNames.length > 0)
741
+ line += ` props="${propNames.join(',')}"`;
742
+ line += kernStyles;
743
+ lines.push(line);
744
+ }
745
+ else {
746
+ // Unknown tag → row with comment
747
+ lines.push(`${prefix}row // <${tag}>${kernStyles}`);
748
+ }
749
+ // Add events
750
+ for (const { event, handler } of events) {
751
+ if (ts.isIdentifier(ts.factory.createIdentifier(handler)) && /^[a-zA-Z_]\w*$/.test(handler)) {
752
+ lines.push(`${prefix} on event=${event} handler=${handler}`);
753
+ }
754
+ else if (handler) {
755
+ lines.push(`${prefix} on event=${event}`);
756
+ lines.push(`${prefix} handler <<<`);
757
+ lines.push(`${prefix} ${handler}`);
758
+ lines.push(`${prefix} >>>`);
759
+ }
760
+ }
761
+ // Process children
762
+ for (const child of children) {
763
+ lines.push(...convertJsxElement(child, source, depth + 1));
764
+ }
765
+ return lines;
766
+ }
767
+ function extractHooks(body, source) {
768
+ const hooks = [];
769
+ const remaining = [];
770
+ for (const stmt of body.statements) {
771
+ const hook = tryExtractHook(stmt, source);
772
+ if (hook) {
773
+ hooks.push(hook);
774
+ }
775
+ else {
776
+ remaining.push(stmt);
777
+ }
778
+ }
779
+ return { hooks, remainingStatements: remaining };
780
+ }
781
+ function tryExtractHook(stmt, source) {
782
+ // useState: const [x, setX] = useState<T>(init)
783
+ if (ts.isVariableStatement(stmt)) {
784
+ for (const decl of stmt.declarationList.declarations) {
785
+ if (decl.initializer && ts.isCallExpression(decl.initializer)) {
786
+ const callName = decl.initializer.expression.getText(source);
787
+ if (callName === 'useState') {
788
+ const init = decl.initializer.arguments[0]?.getText(source) ?? '';
789
+ let name = '';
790
+ if (ts.isArrayBindingPattern(decl.name)) {
791
+ name = decl.name.elements[0]?.getText(source) ?? '';
792
+ }
793
+ else {
794
+ name = decl.name.getText(source);
795
+ }
796
+ const typeArg = decl.initializer.typeArguments?.[0];
797
+ const typeName = typeArg ? typeToString(typeArg, source) : '';
798
+ return { type: 'state', name, init, typeName };
799
+ }
800
+ if (callName === 'useRef') {
801
+ const init = decl.initializer.arguments[0]?.getText(source) ?? '';
802
+ const name = decl.name.getText(source);
803
+ return { type: 'ref', name, init };
804
+ }
805
+ if (callName === 'useMemo') {
806
+ const name = decl.name.getText(source);
807
+ const callback = decl.initializer.arguments[0];
808
+ const depsArg = decl.initializer.arguments[1];
809
+ const deps = depsArg ? depsArg.getText(source).replace(/^\[|\]$/g, '') : '';
810
+ let bodyText = '';
811
+ if (callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) {
812
+ bodyText = getBodyText(callback.body, source) ?? '';
813
+ }
814
+ return { type: 'memo', name, deps, body: bodyText };
815
+ }
816
+ if (callName === 'useCallback') {
817
+ const name = decl.name.getText(source);
818
+ const callback = decl.initializer.arguments[0];
819
+ const depsArg = decl.initializer.arguments[1];
820
+ const deps = depsArg ? depsArg.getText(source).replace(/^\[|\]$/g, '') : '';
821
+ let bodyText = '';
822
+ if (callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) {
823
+ bodyText = getBodyText(callback.body, source) ?? '';
824
+ }
825
+ return { type: 'callback', name, deps, body: bodyText };
826
+ }
827
+ }
828
+ }
829
+ }
830
+ // useEffect: useEffect(() => { ... }, [deps])
831
+ if (ts.isExpressionStatement(stmt) && ts.isCallExpression(stmt.expression)) {
832
+ const callName = stmt.expression.expression.getText(source);
833
+ if (callName === 'useEffect') {
834
+ const callback = stmt.expression.arguments[0];
835
+ const depsArg = stmt.expression.arguments[1];
836
+ const deps = depsArg ? depsArg.getText(source).replace(/^\[|\]$/g, '') : '';
837
+ let bodyText = '';
838
+ let cleanupText = '';
839
+ if (callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) {
840
+ if (ts.isBlock(callback.body)) {
841
+ // Check for cleanup return
842
+ const lastStmt = callback.body.statements[callback.body.statements.length - 1];
843
+ if (lastStmt && ts.isReturnStatement(lastStmt) && lastStmt.expression) {
844
+ // Return of arrow/function = cleanup
845
+ cleanupText = lastStmt.expression.getText(source);
846
+ if (ts.isArrowFunction(lastStmt.expression) || ts.isFunctionExpression(lastStmt.expression)) {
847
+ cleanupText =
848
+ getBodyText(lastStmt.expression.body, source) ?? '';
849
+ }
850
+ // Body is everything except the return
851
+ const bodyStmts = callback.body.statements.slice(0, -1);
852
+ bodyText = bodyStmts.map((s) => s.getText(source)).join('\n');
853
+ }
854
+ else {
855
+ bodyText = getBodyText(callback.body, source) ?? '';
856
+ }
857
+ }
858
+ else {
859
+ bodyText = callback.body.getText(source);
860
+ }
861
+ }
862
+ const once = !!(deps === '' && depsArg);
863
+ return {
864
+ type: 'effect',
865
+ deps: once ? undefined : deps || undefined,
866
+ body: bodyText,
867
+ cleanup: cleanupText || undefined,
868
+ };
869
+ }
870
+ }
871
+ return null;
872
+ }
873
+ function emitHooks(hooks, depth) {
874
+ const lines = [];
875
+ const prefix = ' '.repeat(depth);
876
+ for (const hook of hooks) {
877
+ switch (hook.type) {
878
+ case 'state': {
879
+ let line = `${prefix}// state: ${hook.name}`;
880
+ if (hook.typeName)
881
+ line += ` (${hook.typeName})`;
882
+ if (hook.init)
883
+ line += ` = ${hook.init}`;
884
+ lines.push(line);
885
+ break;
886
+ }
887
+ case 'ref':
888
+ lines.push(`${prefix}ref name=${hook.name}${hook.init ? ` default=${hook.init}` : ''}`);
889
+ break;
890
+ case 'effect': {
891
+ let line = `${prefix}effect`;
892
+ if (hook.deps)
893
+ line += ` deps="${hook.deps}"`;
894
+ if (hook.deps === undefined && !hook.body)
895
+ line += ' once=true';
896
+ lines.push(line);
897
+ if (hook.body) {
898
+ lines.push(`${prefix} handler <<<`);
899
+ for (const l of hook.body.split('\n')) {
900
+ lines.push(`${prefix} ${l}`);
901
+ }
902
+ lines.push(`${prefix} >>>`);
903
+ }
904
+ if (hook.cleanup) {
905
+ lines.push(`${prefix} cleanup <<<`);
906
+ for (const l of hook.cleanup.split('\n')) {
907
+ lines.push(`${prefix} ${l}`);
908
+ }
909
+ lines.push(`${prefix} >>>`);
910
+ }
911
+ break;
912
+ }
913
+ case 'memo':
914
+ lines.push(`${prefix}memo name=${hook.name}${hook.deps ? ` deps="${hook.deps}"` : ''}`);
915
+ if (hook.body) {
916
+ lines.push(`${prefix} handler <<<`);
917
+ for (const l of hook.body.split('\n')) {
918
+ lines.push(`${prefix} ${l}`);
919
+ }
920
+ lines.push(`${prefix} >>>`);
921
+ }
922
+ break;
923
+ case 'callback':
924
+ lines.push(`${prefix}callback name=${hook.name}${hook.deps ? ` deps="${hook.deps}"` : ''}`);
925
+ if (hook.body) {
926
+ lines.push(`${prefix} handler <<<`);
927
+ for (const l of hook.body.split('\n')) {
928
+ lines.push(`${prefix} ${l}`);
929
+ }
930
+ lines.push(`${prefix} >>>`);
931
+ }
932
+ break;
933
+ }
934
+ }
935
+ return lines;
936
+ }
937
+ // ── React component detection & conversion ──────────────────────────────
938
+ function returnsJsx(node, source) {
939
+ if (!node.body)
940
+ return false;
941
+ // Check return type annotation
942
+ const returnType = node.type ? typeToString(node.type, source) : '';
943
+ if (returnType.includes('JSX') || returnType.includes('ReactNode') || returnType.includes('ReactElement'))
944
+ return true;
945
+ // Walk body for JSX returns
946
+ let found = false;
947
+ function visit(n) {
948
+ if (found)
949
+ return;
950
+ if (ts.isJsxElement(n) || ts.isJsxSelfClosingElement(n) || ts.isJsxFragment(n)) {
951
+ found = true;
952
+ return;
953
+ }
954
+ ts.forEachChild(n, visit);
955
+ }
956
+ visit(node.body);
957
+ return found;
958
+ }
959
+ function convertReactComponent(name, _params, body, source, exp, doc, isAsync_) {
960
+ const lines = [];
961
+ if (doc)
962
+ lines.push(`doc text="${escapeKernString(doc)}"`);
963
+ const asyncStr = isAsync_ ? ' async=true' : '';
964
+ const isPage = name.endsWith('Page') ||
965
+ name.endsWith('Layout') ||
966
+ name === 'default' ||
967
+ name === 'Home' ||
968
+ name === 'Dashboard' ||
969
+ name === 'App';
970
+ const nodeType = isPage ? 'page' : 'screen';
971
+ lines.push(`${nodeType} name=${name}${asyncStr}${exp}`);
972
+ // Extract hooks
973
+ const { hooks, remainingStatements } = extractHooks(body, source);
974
+ lines.push(...emitHooks(hooks, 1));
975
+ // Find the return statement with JSX
976
+ for (const stmt of remainingStatements) {
977
+ if (ts.isReturnStatement(stmt) && stmt.expression) {
978
+ let jsxRoot = stmt.expression;
979
+ if (ts.isParenthesizedExpression(jsxRoot))
980
+ jsxRoot = jsxRoot.expression;
981
+ lines.push(...convertJsxElement(jsxRoot, source, 1));
982
+ }
983
+ else {
984
+ // Non-return, non-hook logic → logic block
985
+ const text = stmt.getText(source);
986
+ if (text.trim()) {
987
+ lines.push(` logic <<<`);
988
+ lines.push(` ${text}`);
989
+ lines.push(` >>>`);
990
+ }
991
+ }
992
+ }
993
+ return lines;
994
+ }
995
+ // ── Main entry point ────────────────────────────────────────────────────
996
+ /**
997
+ * Import TypeScript source code and produce .kern output.
998
+ *
999
+ * Recognizes: imports, type aliases, interfaces, enums, functions, classes (→ service/error), constants.
1000
+ * Function/method bodies become <<<>>> handler blocks.
1001
+ * JSDoc comments become doc nodes.
1002
+ *
1003
+ * @param tsSource - TypeScript source code
1004
+ * @param fileName - Optional filename for better error messages
1005
+ */
1006
+ export function importTypeScript(tsSource, fileName = 'input.ts') {
1007
+ const isTsx = fileName.endsWith('.tsx') || tsSource.includes('React') || /<[A-Z]/.test(tsSource);
1008
+ const scriptKind = isTsx ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
1009
+ const sourceFile = ts.createSourceFile(fileName, tsSource, ts.ScriptTarget.Latest, true, scriptKind);
1010
+ const kernLines = [];
1011
+ const unmapped = [];
1012
+ const stats = {
1013
+ types: 0,
1014
+ interfaces: 0,
1015
+ functions: 0,
1016
+ classes: 0,
1017
+ imports: 0,
1018
+ constants: 0,
1019
+ enums: 0,
1020
+ components: 0,
1021
+ };
1022
+ for (const statement of sourceFile.statements) {
1023
+ const converted = convertStatement(statement, sourceFile, unmapped, stats);
1024
+ if (converted.length > 0) {
1025
+ kernLines.push(...converted);
1026
+ kernLines.push(''); // blank line between top-level nodes
1027
+ }
1028
+ }
1029
+ return {
1030
+ kern: `${kernLines.join('\n').trimEnd()}\n`,
1031
+ unmapped,
1032
+ stats,
1033
+ };
1034
+ }
1035
+ function convertStatement(node, source, unmapped, stats) {
1036
+ // Skip 'use client' / 'use server' directives
1037
+ if (ts.isExpressionStatement(node) && ts.isStringLiteral(node.expression)) {
1038
+ return [`// ${node.expression.text}`];
1039
+ }
1040
+ if (ts.isImportDeclaration(node)) {
1041
+ stats.imports++;
1042
+ return convertImport(node, source);
1043
+ }
1044
+ if (ts.isTypeAliasDeclaration(node)) {
1045
+ stats.types++;
1046
+ return convertTypeAlias(node, source);
1047
+ }
1048
+ if (ts.isInterfaceDeclaration(node)) {
1049
+ stats.interfaces++;
1050
+ return convertInterface(node, source);
1051
+ }
1052
+ if (ts.isEnumDeclaration(node)) {
1053
+ stats.enums++;
1054
+ return convertEnum(node, source);
1055
+ }
1056
+ if (ts.isFunctionDeclaration(node)) {
1057
+ // Check if it's a React component (returns JSX)
1058
+ if (node.name && /^[A-Z]/.test(node.name.getText(source)) && returnsJsx(node, source)) {
1059
+ stats.components++;
1060
+ const name = node.name.getText(source);
1061
+ const params = formatParams(node.parameters, source);
1062
+ const exp = isExported(node) ? ' export=true' : '';
1063
+ const doc = getJSDoc(node, source);
1064
+ return convertReactComponent(name, params, node.body, source, exp, doc, isAsync(node));
1065
+ }
1066
+ stats.functions++;
1067
+ return convertFunction(node, source);
1068
+ }
1069
+ if (ts.isClassDeclaration(node)) {
1070
+ stats.classes++;
1071
+ return convertClass(node, source);
1072
+ }
1073
+ if (ts.isVariableStatement(node)) {
1074
+ // Check for arrow function React components: const MyComponent = (props) => { return <div>... }
1075
+ for (const decl of node.declarationList.declarations) {
1076
+ const name = decl.name.getText(source);
1077
+ if (/^[A-Z]/.test(name) &&
1078
+ decl.initializer &&
1079
+ (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer))) {
1080
+ const func = decl.initializer;
1081
+ if (returnsJsx(func, source)) {
1082
+ stats.components++;
1083
+ const exp = isExported(node) ? ' export=true' : '';
1084
+ const doc = getJSDoc(node, source);
1085
+ const arrowBody = func.body;
1086
+ if (ts.isBlock(arrowBody)) {
1087
+ return convertReactComponent(name, formatParams(func.parameters, source), arrowBody, source, exp, doc, isAsync(func));
1088
+ }
1089
+ // Expression-bodied: const Foo = () => <div>...</div>
1090
+ // Wrap in a synthetic block for the component converter
1091
+ const jsxLines = [];
1092
+ if (doc)
1093
+ jsxLines.push(`doc text="${escapeKernString(doc)}"`);
1094
+ const asyncStr = isAsync(func) ? ' async=true' : '';
1095
+ const isPage_ = name.endsWith('Page') ||
1096
+ name.endsWith('Layout') ||
1097
+ name === 'default' ||
1098
+ name === 'Home' ||
1099
+ name === 'Dashboard' ||
1100
+ name === 'App';
1101
+ jsxLines.push(`${isPage_ ? 'page' : 'screen'} name=${name}${asyncStr}${exp}`);
1102
+ jsxLines.push(...convertJsxElement(arrowBody, source, 1));
1103
+ return jsxLines;
1104
+ }
1105
+ }
1106
+ }
1107
+ stats.constants++;
1108
+ return convertVariableStatement(node, source);
1109
+ }
1110
+ // Export default function/class
1111
+ if (ts.isExportAssignment(node)) {
1112
+ const text = node.expression.getText(source);
1113
+ return [`// export default ${text}`];
1114
+ }
1115
+ // Re-exports: export { X } from './y'
1116
+ if (ts.isExportDeclaration(node)) {
1117
+ const moduleSpec = node.moduleSpecifier?.getText(source).replace(/['"]/g, '');
1118
+ if (moduleSpec && node.exportClause && ts.isNamedExports(node.exportClause)) {
1119
+ const names = node.exportClause.elements.map((e) => e.getText(source)).join(',');
1120
+ return [`import from="${moduleSpec}" names="${names}"`];
1121
+ }
1122
+ if (moduleSpec) {
1123
+ return [`// export * from "${moduleSpec}"`];
1124
+ }
1125
+ return [];
1126
+ }
1127
+ // Unmapped
1128
+ const text = node.getText(source).slice(0, 80);
1129
+ unmapped.push(`Line ${getLineNumber(node, source)}: ${text}${text.length >= 80 ? '...' : ''}`);
1130
+ return [`// [unmapped] ${text.split('\n')[0]}`];
1131
+ }
1132
+ function getLineNumber(node, source) {
1133
+ return source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
1134
+ }
1135
+ //# sourceMappingURL=importer.js.map