@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.
- package/dist/codegen/data-layer.js +6 -6
- package/dist/codegen/data-layer.js.map +1 -1
- package/dist/codegen/events.js +4 -4
- package/dist/codegen/events.js.map +1 -1
- package/dist/codegen/functions.js +3 -3
- package/dist/codegen/functions.js.map +1 -1
- package/dist/codegen/helpers.d.ts +2 -0
- package/dist/codegen/helpers.js +24 -0
- package/dist/codegen/helpers.js.map +1 -1
- package/dist/codegen/type-system.js +22 -13
- package/dist/codegen/type-system.js.map +1 -1
- package/dist/codegen-core.js +9 -0
- package/dist/codegen-core.js.map +1 -1
- package/dist/errors.d.ts +3 -1
- package/dist/errors.js +3 -1
- package/dist/errors.js.map +1 -1
- package/dist/importer.d.ts +38 -0
- package/dist/importer.js +1135 -0
- package/dist/importer.js.map +1 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/node-props.d.ts +5 -0
- package/dist/node-props.js.map +1 -1
- package/dist/parser-core.js +56 -1
- package/dist/parser-core.js.map +1 -1
- package/dist/parser-keywords.js +24 -0
- package/dist/parser-keywords.js.map +1 -1
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +1 -1
- package/dist/schema.d.ts +25 -0
- package/dist/schema.js +442 -4
- package/dist/schema.js.map +1 -1
- package/dist/semantic-validator.d.ts +20 -0
- package/dist/semantic-validator.js +82 -0
- package/dist/semantic-validator.js.map +1 -0
- package/dist/spec.d.ts +1 -1
- package/dist/spec.js +1 -0
- package/dist/spec.js.map +1 -1
- package/package.json +1 -1
package/dist/importer.js
ADDED
|
@@ -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
|