@kernlang/core 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +661 -0
  2. package/dist/codegen-core.d.ts +30 -0
  3. package/dist/codegen-core.js +751 -0
  4. package/dist/codegen-core.js.map +1 -0
  5. package/dist/config.d.ts +69 -0
  6. package/dist/config.js +78 -0
  7. package/dist/config.js.map +1 -0
  8. package/dist/decompiler.d.ts +2 -0
  9. package/dist/decompiler.js +44 -0
  10. package/dist/decompiler.js.map +1 -0
  11. package/dist/errors.d.ts +12 -0
  12. package/dist/errors.js +40 -0
  13. package/dist/errors.js.map +1 -0
  14. package/dist/index.d.ts +23 -0
  15. package/dist/index.js +28 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/parser.d.ts +4 -0
  18. package/dist/parser.js +380 -0
  19. package/dist/parser.js.map +1 -0
  20. package/dist/scanner.d.ts +40 -0
  21. package/dist/scanner.js +380 -0
  22. package/dist/scanner.js.map +1 -0
  23. package/dist/spec.d.ts +17 -0
  24. package/dist/spec.js +101 -0
  25. package/dist/spec.js.map +1 -0
  26. package/dist/styles-react.d.ts +3 -0
  27. package/dist/styles-react.js +20 -0
  28. package/dist/styles-react.js.map +1 -0
  29. package/dist/styles-tailwind.d.ts +8 -0
  30. package/dist/styles-tailwind.js +197 -0
  31. package/dist/styles-tailwind.js.map +1 -0
  32. package/dist/template-catalog.d.ts +26 -0
  33. package/dist/template-catalog.js +228 -0
  34. package/dist/template-catalog.js.map +1 -0
  35. package/dist/template-engine.d.ts +33 -0
  36. package/dist/template-engine.js +196 -0
  37. package/dist/template-engine.js.map +1 -0
  38. package/dist/types.d.ts +94 -0
  39. package/dist/types.js +11 -0
  40. package/dist/types.js.map +1 -0
  41. package/dist/utils.d.ts +12 -0
  42. package/dist/utils.js +62 -0
  43. package/dist/utils.js.map +1 -0
  44. package/dist/version-adapters.d.ts +76 -0
  45. package/dist/version-adapters.js +172 -0
  46. package/dist/version-adapters.js.map +1 -0
  47. package/dist/version-detect.d.ts +30 -0
  48. package/dist/version-detect.js +63 -0
  49. package/dist/version-detect.js.map +1 -0
  50. package/package.json +20 -0
@@ -0,0 +1,751 @@
1
+ /**
2
+ * Core Language Codegen — shared TypeScript generation for KERN's type system
3
+ *
4
+ * Handles: type, interface, fn, machine, error, module, config, store, test, event
5
+ * These are target-agnostic — they compile to TypeScript regardless of target.
6
+ *
7
+ * Machine nodes are KERN's killer feature: 12 lines of KERN → 140+ lines of TS.
8
+ */
9
+ import { isTemplateNode, expandTemplateNode } from './template-engine.js';
10
+ // ── Helpers ──────────────────────────────────────────────────────────────
11
+ function p(node) {
12
+ return node.props || {};
13
+ }
14
+ function kids(node, type) {
15
+ const c = node.children || [];
16
+ return type ? c.filter(n => n.type === type) : c;
17
+ }
18
+ function firstChild(node, type) {
19
+ return kids(node, type)[0];
20
+ }
21
+ /** Strip common leading whitespace from multiline handler code. */
22
+ function dedent(code) {
23
+ const lines = code.split('\n');
24
+ const nonEmpty = lines.filter(l => l.trim().length > 0);
25
+ if (nonEmpty.length === 0)
26
+ return code;
27
+ const min = Math.min(...nonEmpty.map(l => l.match(/^(\s*)/)?.[1].length ?? 0));
28
+ return lines.map(l => l.slice(min)).join('\n');
29
+ }
30
+ function handlerCode(node) {
31
+ const handler = firstChild(node, 'handler');
32
+ if (!handler)
33
+ return '';
34
+ const raw = p(handler).code || '';
35
+ return dedent(raw);
36
+ }
37
+ function exportPrefix(node) {
38
+ return p(node).export === 'false' ? '' : 'export ';
39
+ }
40
+ // ── Type Alias ───────────────────────────────────────────────────────────
41
+ // type name=PlanState values="draft|approved|running|paused|completed|failed|cancelled"
42
+ // → export type PlanState = 'draft' | 'approved' | 'running' | ...;
43
+ export function generateType(node) {
44
+ const { name, values, alias } = p(node);
45
+ const exp = exportPrefix(node);
46
+ if (values) {
47
+ const members = values.split('|').map(v => `'${v.trim()}'`).join(' | ');
48
+ return [`${exp}type ${name} = ${members};`];
49
+ }
50
+ if (alias) {
51
+ return [`${exp}type ${name} = ${alias};`];
52
+ }
53
+ return [`${exp}type ${name} = unknown;`];
54
+ }
55
+ // ── Interface ────────────────────────────────────────────────────────────
56
+ // interface name=Plan extends=Base
57
+ // field name=id type=string
58
+ // field name=state type=PlanState
59
+ // field name=steps type="PlanStep[]"
60
+ // field name=engineId type=string optional=true
61
+ export function generateInterface(node) {
62
+ const props = p(node);
63
+ const name = props.name;
64
+ const ext = props.extends ? ` extends ${props.extends}` : '';
65
+ const exp = exportPrefix(node);
66
+ const lines = [];
67
+ lines.push(`${exp}interface ${name}${ext} {`);
68
+ for (const field of kids(node, 'field')) {
69
+ const fp = p(field);
70
+ const opt = fp.optional === 'true' || fp.optional === true ? '?' : '';
71
+ lines.push(` ${fp.name}${opt}: ${fp.type};`);
72
+ }
73
+ lines.push('}');
74
+ return lines;
75
+ }
76
+ // ── Function ─────────────────────────────────────────────────────────────
77
+ // fn name=createPlan params="action:PlanAction,ws:WorkspaceSnapshot" returns=Plan
78
+ // handler <<<
79
+ // return { ... };
80
+ // >>>
81
+ export function generateFunction(node) {
82
+ const props = p(node);
83
+ const name = props.name;
84
+ const params = props.params || '';
85
+ const returns = props.returns;
86
+ const isAsync = props.async === 'true' || props.async === true;
87
+ const exp = exportPrefix(node);
88
+ const lines = [];
89
+ // Parse params: "action:PlanAction,ws:WorkspaceSnapshot" → "action: PlanAction, ws: WorkspaceSnapshot"
90
+ const paramList = params
91
+ ? params.split(',').map(s => {
92
+ const [pname, ...ptype] = s.split(':').map(t => t.trim());
93
+ return ptype.length > 0 ? `${pname}: ${ptype.join(':')}` : pname;
94
+ }).join(', ')
95
+ : '';
96
+ const retClause = returns ? `: ${returns}` : '';
97
+ const asyncKw = isAsync ? 'async ' : '';
98
+ const code = handlerCode(node);
99
+ lines.push(`${exp}${asyncKw}function ${name}(${paramList})${retClause} {`);
100
+ if (code) {
101
+ for (const line of code.split('\n')) {
102
+ lines.push(` ${line}`);
103
+ }
104
+ }
105
+ lines.push('}');
106
+ return lines;
107
+ }
108
+ // ── Error Class ──────────────────────────────────────────────────────────
109
+ // error name=AgonError extends=Error
110
+ // error name=PlanStateError extends=AgonError
111
+ // field name=expected type="string | string[]"
112
+ // field name=actual type=string
113
+ // message "Invalid plan state: expected ${expected}, got ${actual}"
114
+ export function generateError(node) {
115
+ const props = p(node);
116
+ const name = props.name;
117
+ const ext = props.extends || 'Error';
118
+ const message = props.message;
119
+ const exp = exportPrefix(node);
120
+ const fields = kids(node, 'field');
121
+ const lines = [];
122
+ lines.push(`${exp}class ${name} extends ${ext} {`);
123
+ const code = handlerCode(node);
124
+ if (fields.length > 0) {
125
+ lines.push(` constructor(`);
126
+ // Check if first field is 'message' — special case: pass to super
127
+ const hasMessageParam = p(fields[0]).name === 'message';
128
+ for (const field of fields) {
129
+ const fp = p(field);
130
+ const opt = fp.optional === 'true' || fp.optional === true ? '?' : '';
131
+ const isMessage = fp.name === 'message';
132
+ // 'message' param is not readonly — it's passed to super
133
+ if (isMessage) {
134
+ lines.push(` ${fp.name}${opt}: ${fp.type},`);
135
+ }
136
+ else {
137
+ lines.push(` public readonly ${fp.name}${opt}: ${fp.type},`);
138
+ }
139
+ }
140
+ lines.push(` ) {`);
141
+ if (code) {
142
+ // Custom handler body — replaces auto-generated constructor logic
143
+ for (const line of code.split('\n')) {
144
+ lines.push(` ${line}`);
145
+ }
146
+ }
147
+ else if (message) {
148
+ // Check if message references array fields that need formatting
149
+ const arrayFields = fields.filter(f => {
150
+ const ft = p(f).type;
151
+ return ft.includes('[]') || ft.includes('string |') || ft.includes('| string');
152
+ });
153
+ for (const f of arrayFields) {
154
+ const fn = p(f).name;
155
+ lines.push(` const ${fn}Str = Array.isArray(${fn}) ? ${fn}.join(' | ') : ${fn};`);
156
+ }
157
+ lines.push(` super(\`${message}\`);`);
158
+ lines.push(` this.name = '${name}';`);
159
+ }
160
+ else if (hasMessageParam) {
161
+ lines.push(` super(message);`);
162
+ lines.push(` this.name = '${name}';`);
163
+ }
164
+ else {
165
+ lines.push(` super();`);
166
+ lines.push(` this.name = '${name}';`);
167
+ }
168
+ lines.push(` }`);
169
+ }
170
+ else {
171
+ lines.push(` constructor(message: string) {`);
172
+ lines.push(` super(message);`);
173
+ lines.push(` this.name = '${name}';`);
174
+ lines.push(` }`);
175
+ }
176
+ lines.push('}');
177
+ return lines;
178
+ }
179
+ // ── State Machine ────────────────────────────────────────────────────────
180
+ // KERN's killer feature. 12 lines of KERN → 140+ lines of TypeScript.
181
+ //
182
+ // machine name=Plan
183
+ // state name=draft initial=true
184
+ // state name=approved
185
+ // state name=running
186
+ // state name=paused
187
+ // state name=completed
188
+ // state name=failed
189
+ // state name=cancelled
190
+ // transition name=approve from=draft to=approved
191
+ // transition name=start from=approved to=running
192
+ // transition name=cancel from="draft|approved|running|paused|failed" to=cancelled
193
+ // transition name=fail from="running|paused" to=failed
194
+ //
195
+ // Generates:
196
+ // - PlanState type
197
+ // - PlanStateError class
198
+ // - approvePlan(), startPlan(), cancelPlan(), failPlan() functions
199
+ export function generateMachine(node) {
200
+ const props = p(node);
201
+ const name = props.name;
202
+ const exp = exportPrefix(node);
203
+ const lines = [];
204
+ // Collect states
205
+ const states = kids(node, 'state');
206
+ const stateNames = states.map(s => {
207
+ const sp = p(s);
208
+ return (sp.name || sp.value);
209
+ });
210
+ // State type
211
+ const stateType = `${name}State`;
212
+ lines.push(`${exp}type ${stateType} = ${stateNames.map(s => `'${s}'`).join(' | ')};`);
213
+ lines.push('');
214
+ // Error class
215
+ const errorName = `${name}StateError`;
216
+ lines.push(`${exp}class ${errorName} extends Error {`);
217
+ lines.push(` constructor(`);
218
+ lines.push(` public readonly expected: string | string[],`);
219
+ lines.push(` public readonly actual: string,`);
220
+ lines.push(` ) {`);
221
+ lines.push(` const expectedStr = Array.isArray(expected) ? expected.join(' | ') : expected;`);
222
+ lines.push(` super(\`Invalid ${name.toLowerCase()} state: expected \${expectedStr}, got \${actual}\`);`);
223
+ lines.push(` this.name = '${errorName}';`);
224
+ lines.push(` }`);
225
+ lines.push('}');
226
+ lines.push('');
227
+ // Transition functions
228
+ const transitions = kids(node, 'transition');
229
+ for (const t of transitions) {
230
+ const tp = p(t);
231
+ const tname = tp.name;
232
+ const from = tp.from;
233
+ const to = tp.to;
234
+ const fromStates = from.split('|').map(s => s.trim());
235
+ const isMultiFrom = fromStates.length > 1;
236
+ const fnName = `${tname}${name}`;
237
+ const code = handlerCode(t);
238
+ lines.push(`/** ${from} → ${to} */`);
239
+ lines.push(`${exp}function ${fnName}<T extends { state: ${stateType} }>(entity: T): T {`);
240
+ if (isMultiFrom) {
241
+ lines.push(` const validStates: ${stateType}[] = [${fromStates.map(s => `'${s}'`).join(', ')}];`);
242
+ lines.push(` if (!validStates.includes(entity.state)) {`);
243
+ lines.push(` throw new ${errorName}(validStates, entity.state);`);
244
+ lines.push(` }`);
245
+ }
246
+ else {
247
+ lines.push(` if (entity.state !== '${fromStates[0]}') {`);
248
+ lines.push(` throw new ${errorName}('${fromStates[0]}', entity.state);`);
249
+ lines.push(` }`);
250
+ }
251
+ if (code) {
252
+ for (const line of code.split('\n')) {
253
+ lines.push(` ${line}`);
254
+ }
255
+ }
256
+ else {
257
+ lines.push(` return { ...entity, state: '${to}' as ${stateType} };`);
258
+ }
259
+ lines.push('}');
260
+ lines.push('');
261
+ }
262
+ return lines;
263
+ }
264
+ // ── Config ───────────────────────────────────────────────────────────────
265
+ // config name=AgonConfig
266
+ // field name=timeout type=number default=120
267
+ // field name=approvalLevel type=ApprovalLevel default="plan"
268
+ export function generateConfig(node) {
269
+ const props = p(node);
270
+ const name = props.name;
271
+ const exp = exportPrefix(node);
272
+ const fields = kids(node, 'field');
273
+ const lines = [];
274
+ // Interface
275
+ lines.push(`${exp}interface ${name} {`);
276
+ for (const field of fields) {
277
+ const fp = p(field);
278
+ const opt = fp.default !== undefined ? '?' : '';
279
+ lines.push(` ${fp.name}${opt}: ${fp.type};`);
280
+ }
281
+ lines.push('}');
282
+ lines.push('');
283
+ // Defaults object
284
+ lines.push(`${exp}const DEFAULT_${name.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase()}: Required<${name}> = {`);
285
+ for (const field of fields) {
286
+ const fp = p(field);
287
+ const ftype = fp.type;
288
+ let def = fp.default;
289
+ if (def === undefined) {
290
+ if (ftype === 'number')
291
+ def = '0';
292
+ else if (ftype === 'boolean')
293
+ def = 'false';
294
+ else if (ftype.endsWith('[]'))
295
+ def = '[]';
296
+ else
297
+ def = "''";
298
+ }
299
+ else if (ftype === 'string' || (!['number', 'boolean'].includes(ftype) && !ftype.endsWith('[]') && !def.startsWith("'") && !def.startsWith('"'))) {
300
+ def = `'${def}'`;
301
+ }
302
+ lines.push(` ${fp.name}: ${def},`);
303
+ }
304
+ lines.push('};');
305
+ return lines;
306
+ }
307
+ // ── Store ────────────────────────────────────────────────────────────────
308
+ // store name=Plan path="~/.agon/plans" key=id
309
+ // model Plan
310
+ export function generateStore(node) {
311
+ const props = p(node);
312
+ const name = props.name;
313
+ const storePath = props.path || '~/.data';
314
+ const key = props.key || 'id';
315
+ const model = props.model || 'unknown';
316
+ const exp = exportPrefix(node);
317
+ const lines = [];
318
+ const dirConst = `${name.toUpperCase()}_DIR`;
319
+ const resolvedPath = storePath.startsWith('~/')
320
+ ? `join(homedir(), '${storePath.slice(2)}')`
321
+ : `'${storePath}'`;
322
+ lines.push(`import { readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from 'node:fs';`);
323
+ lines.push(`import { join, resolve } from 'node:path';`);
324
+ lines.push(`import { homedir } from 'node:os';`);
325
+ lines.push('');
326
+ lines.push(`const ${dirConst} = ${resolvedPath};`);
327
+ lines.push('');
328
+ lines.push(`function ensure${name}Dir(): void {`);
329
+ lines.push(` mkdirSync(${dirConst}, { recursive: true });`);
330
+ lines.push('}');
331
+ lines.push('');
332
+ lines.push(`function safe${name}Path(id: string): string {`);
333
+ lines.push(` const sanitized = id.replace(/[^a-zA-Z0-9_-]/g, '');`);
334
+ lines.push(` const full = resolve(${dirConst}, \`\${sanitized}.json\`);`);
335
+ lines.push(` if (!full.startsWith(resolve(${dirConst}))) throw new Error(\`Invalid ID: \${id}\`);`);
336
+ lines.push(` return full;`);
337
+ lines.push('}');
338
+ lines.push('');
339
+ lines.push(`${exp}function save${name}(item: ${model}): void {`);
340
+ lines.push(` ensure${name}Dir();`);
341
+ lines.push(` writeFileSync(safe${name}Path((item as any).${key}), JSON.stringify(item, null, 2) + '\\n');`);
342
+ lines.push('}');
343
+ lines.push('');
344
+ lines.push(`${exp}function load${name}(id: string): ${model} | null {`);
345
+ lines.push(` try { return JSON.parse(readFileSync(safe${name}Path(id), 'utf-8')) as ${model}; }`);
346
+ lines.push(` catch { return null; }`);
347
+ lines.push('}');
348
+ lines.push('');
349
+ lines.push(`${exp}function list${name}s(limit = 20): ${model}[] {`);
350
+ lines.push(` ensure${name}Dir();`);
351
+ lines.push(` try {`);
352
+ lines.push(` return readdirSync(${dirConst}).filter(f => f.endsWith('.json'))`);
353
+ lines.push(` .map(f => JSON.parse(readFileSync(join(${dirConst}, f), 'utf-8')) as ${model})`);
354
+ lines.push(` .sort((a: any, b: any) => (b.updatedAt || '').localeCompare(a.updatedAt || ''))`);
355
+ lines.push(` .slice(0, limit);`);
356
+ lines.push(` } catch { return []; }`);
357
+ lines.push('}');
358
+ lines.push('');
359
+ lines.push(`${exp}function delete${name}(id: string): boolean {`);
360
+ lines.push(` try { unlinkSync(safe${name}Path(id)); return true; }`);
361
+ lines.push(` catch { return false; }`);
362
+ lines.push('}');
363
+ return lines;
364
+ }
365
+ // ── Test ─────────────────────────────────────────────────────────────────
366
+ // test name="Plan Transitions"
367
+ // describe name=approvePlan
368
+ // it name="transitions draft → approved"
369
+ // handler <<<
370
+ // expect(approvePlan(makePlan('draft')).state).toBe('approved');
371
+ // >>>
372
+ export function generateTest(node) {
373
+ const props = p(node);
374
+ const name = props.name;
375
+ const lines = [];
376
+ lines.push(`import { describe, it, expect } from 'vitest';`);
377
+ lines.push('');
378
+ // Top-level setup handler
379
+ const setup = handlerCode(node);
380
+ if (setup) {
381
+ for (const line of setup.split('\n'))
382
+ lines.push(line);
383
+ lines.push('');
384
+ }
385
+ lines.push(`describe('${name}', () => {`);
386
+ for (const desc of kids(node, 'describe')) {
387
+ const dname = p(desc).name;
388
+ lines.push(` describe('${dname}', () => {`);
389
+ for (const test of kids(desc, 'it')) {
390
+ const tname = p(test).name;
391
+ const code = handlerCode(test);
392
+ lines.push(` it('${tname}', () => {`);
393
+ if (code) {
394
+ for (const line of code.split('\n'))
395
+ lines.push(` ${line}`);
396
+ }
397
+ lines.push(` });`);
398
+ }
399
+ lines.push(` });`);
400
+ }
401
+ // Top-level it blocks
402
+ for (const test of kids(node, 'it')) {
403
+ const tname = p(test).name;
404
+ const code = handlerCode(test);
405
+ lines.push(` it('${tname}', () => {`);
406
+ if (code) {
407
+ for (const line of code.split('\n'))
408
+ lines.push(` ${line}`);
409
+ }
410
+ lines.push(` });`);
411
+ }
412
+ lines.push('});');
413
+ return lines;
414
+ }
415
+ // ── Event ────────────────────────────────────────────────────────────────
416
+ // event name=ForgeEvent
417
+ // type name="baseline:start"
418
+ // type name="baseline:done" data="{ passes: boolean }"
419
+ // type name="winner:determined" data="{ winner: string, bestScore: number }"
420
+ export function generateEvent(node) {
421
+ const props = p(node);
422
+ const name = props.name;
423
+ const exp = exportPrefix(node);
424
+ const types = kids(node, 'type');
425
+ const lines = [];
426
+ // Event type union
427
+ lines.push(`${exp}type ${name}Type = ${types.map(t => `'${(p(t).name || p(t).value)}'`).join(' | ')};`);
428
+ lines.push('');
429
+ // Event interface
430
+ lines.push(`${exp}interface ${name} {`);
431
+ lines.push(` type: ${name}Type;`);
432
+ lines.push(` engineId?: string;`);
433
+ lines.push(` data?: Record<string, unknown>;`);
434
+ lines.push('}');
435
+ lines.push('');
436
+ // Typed event map
437
+ lines.push(`${exp}interface ${name}Map {`);
438
+ for (const t of types) {
439
+ const tp = p(t);
440
+ const tname = (tp.name || tp.value);
441
+ const data = tp.data || 'Record<string, unknown>';
442
+ lines.push(` '${tname}': ${data};`);
443
+ }
444
+ lines.push('}');
445
+ lines.push('');
446
+ // Callback type
447
+ lines.push(`${exp}type ${name}Callback = (event: ${name}) => void;`);
448
+ return lines;
449
+ }
450
+ // ── Module ───────────────────────────────────────────────────────────────
451
+ // module name=@agon/core
452
+ // export from="./plan.js" names="createPlan,advanceStep"
453
+ export function generateModule(node) {
454
+ const props = p(node);
455
+ const name = props.name;
456
+ const lines = [];
457
+ lines.push(`// ── Module: ${name} ──`);
458
+ lines.push('');
459
+ for (const exp of kids(node, 'export')) {
460
+ const ep = p(exp);
461
+ const from = ep.from;
462
+ const names = ep.names;
463
+ const typeNames = ep.types;
464
+ const star = ep.star === 'true' || ep.star === true;
465
+ if (from && !names && !typeNames && star) {
466
+ lines.push(`export * from '${from}';`);
467
+ }
468
+ if (from && names) {
469
+ lines.push(`export { ${names.split(',').map(s => s.trim()).join(', ')} } from '${from}';`);
470
+ }
471
+ if (from && typeNames) {
472
+ lines.push(`export type { ${typeNames.split(',').map(s => s.trim()).join(', ')} } from '${from}';`);
473
+ }
474
+ }
475
+ // Inline child definitions
476
+ for (const child of kids(node)) {
477
+ if (child.type === 'export')
478
+ continue;
479
+ lines.push(...generateCoreNode(child));
480
+ lines.push('');
481
+ }
482
+ return lines;
483
+ }
484
+ // ── Import ──────────────────────────────────────────────────────────────
485
+ // import from="node:fs" names="readFileSync,writeFileSync"
486
+ // → import { readFileSync, writeFileSync } from 'node:fs';
487
+ //
488
+ // import from="./types.js" names="Plan" types=true
489
+ // → import type { Plan } from './types.js';
490
+ //
491
+ // import from="node:path" default=path
492
+ // → import path from 'node:path';
493
+ //
494
+ // import from="node:fs" default=fs names="readFileSync"
495
+ // → import fs, { readFileSync } from 'node:fs';
496
+ export function generateImport(node) {
497
+ const props = p(node);
498
+ const from = props.from;
499
+ const names = props.names;
500
+ const defaultImport = props.default;
501
+ const isTypeOnly = props.types === 'true' || props.types === true;
502
+ if (!from)
503
+ return [];
504
+ const typeKw = isTypeOnly ? 'type ' : '';
505
+ const namedList = names
506
+ ? names.split(',').map(s => s.trim()).join(', ')
507
+ : '';
508
+ if (defaultImport && namedList) {
509
+ return [`import ${typeKw}${defaultImport}, { ${namedList} } from '${from}';`];
510
+ }
511
+ if (defaultImport) {
512
+ return [`import ${typeKw}${defaultImport} from '${from}';`];
513
+ }
514
+ if (namedList) {
515
+ return [`import ${typeKw}{ ${namedList} } from '${from}';`];
516
+ }
517
+ // Side-effect import
518
+ return [`import '${from}';`];
519
+ }
520
+ // ── Const ───────────────────────────────────────────────────────────────
521
+ // const name=AGON_HOME type=string
522
+ // handler <<<
523
+ // join(homedir(), '.agon')
524
+ // >>>
525
+ // → export const AGON_HOME: string = join(homedir(), '.agon');
526
+ //
527
+ // const name=DEFAULT_WEIGHTS type=ScoreWeights value="{ pass: 50 }"
528
+ // → export const DEFAULT_WEIGHTS: ScoreWeights = { pass: 50 };
529
+ export function generateConst(node) {
530
+ const props = p(node);
531
+ const name = props.name;
532
+ const constType = props.type;
533
+ const value = props.value;
534
+ const exp = exportPrefix(node);
535
+ const code = handlerCode(node);
536
+ const typeAnnotation = constType ? `: ${constType}` : '';
537
+ if (code) {
538
+ return [`${exp}const ${name}${typeAnnotation} = ${code.trim()};`];
539
+ }
540
+ if (value) {
541
+ return [`${exp}const ${name}${typeAnnotation} = ${value};`];
542
+ }
543
+ return [`${exp}const ${name}${typeAnnotation};`];
544
+ }
545
+ // ── Shared Helpers (exported for @kernlang/react) ────────────────────────────
546
+ /** Parse "name:Type,name2:Type2" → "name: Type, name2: Type2" */
547
+ export function parseParamList(params) {
548
+ if (!params)
549
+ return '';
550
+ return params.split(',').map(s => {
551
+ const [pname, ...ptype] = s.split(':').map(t => t.trim());
552
+ return ptype.length > 0 ? `${pname}: ${ptype.join(':')}` : pname;
553
+ }).join(', ');
554
+ }
555
+ export function capitalize(s) {
556
+ return s.charAt(0).toUpperCase() + s.slice(1);
557
+ }
558
+ // ── Hook ─────────────────────────────────────────────────────────────────
559
+ // hook name=useSearch params="initialState:SearchState" returns=UseSearchResult
560
+ // state name=query type=string init="initialState.query"
561
+ // ref name=abortCtrl type=AbortController init="new AbortController()"
562
+ // context name=env type=EnvConfig source=EnvContext
563
+ // handler <<<
564
+ // const { data } = useSWR(cacheKey, fetcher);
565
+ // >>>
566
+ // memo name=cacheKey deps="query,filters"
567
+ // handler <<<
568
+ // return buildCacheKey(query, filters);
569
+ // >>>
570
+ // callback name=handleFilter params="field:string,value:string" deps="query"
571
+ // handler <<<
572
+ // setQuery(prev => updateFilter(prev, field, value));
573
+ // >>>
574
+ // effect deps="query"
575
+ // handler <<<
576
+ // trackSearch(query);
577
+ // >>>
578
+ // returns names="products:data?.products,isLoading,handleFilter,cacheKey"
579
+ export function generateHook(node) {
580
+ const props = p(node);
581
+ const name = props.name;
582
+ const params = props.params || '';
583
+ const returnsType = props.returns;
584
+ const exp = exportPrefix(node);
585
+ const lines = [];
586
+ const reactImports = new Set();
587
+ // Parse params
588
+ const paramList = parseParamList(params);
589
+ const retClause = returnsType ? `: ${returnsType}` : '';
590
+ lines.push(`${exp}function ${name}(${paramList})${retClause} {`);
591
+ // Emit children in source order — returns is always last
592
+ const children = kids(node);
593
+ const returnsNode = children.find(c => c.type === 'returns');
594
+ const ordered = children.filter(c => c.type !== 'returns');
595
+ for (const child of ordered) {
596
+ const cp = p(child);
597
+ switch (child.type) {
598
+ case 'state': {
599
+ reactImports.add('useState');
600
+ const sname = cp.name;
601
+ const stype = cp.type || 'unknown';
602
+ const sinit = cp.init || 'undefined';
603
+ const setter = `set${capitalize(sname)}`;
604
+ lines.push(` const [${sname}, ${setter}] = useState<${stype}>(${sinit});`);
605
+ break;
606
+ }
607
+ case 'ref': {
608
+ reactImports.add('useRef');
609
+ const rname = cp.name;
610
+ const rtype = cp.type || 'unknown';
611
+ const rinit = cp.init || 'null';
612
+ lines.push(` const ${rname} = useRef<${rtype}>(${rinit});`);
613
+ break;
614
+ }
615
+ case 'context': {
616
+ reactImports.add('useContext');
617
+ const cname = cp.name;
618
+ const csource = cp.source;
619
+ lines.push(` const ${cname} = useContext(${csource});`);
620
+ break;
621
+ }
622
+ case 'handler': {
623
+ const code = cp.code || '';
624
+ const dedented = dedent(code);
625
+ for (const line of dedented.split('\n')) {
626
+ lines.push(` ${line}`);
627
+ }
628
+ break;
629
+ }
630
+ case 'memo': {
631
+ reactImports.add('useMemo');
632
+ const mname = cp.name;
633
+ const mdeps = cp.deps || '';
634
+ const mcode = handlerCode(child);
635
+ const depsArr = mdeps ? `[${mdeps}]` : '[]';
636
+ lines.push(` const ${mname} = useMemo(() => {`);
637
+ if (mcode) {
638
+ for (const line of mcode.split('\n')) {
639
+ lines.push(` ${line}`);
640
+ }
641
+ }
642
+ lines.push(` }, ${depsArr});`);
643
+ break;
644
+ }
645
+ case 'callback': {
646
+ reactImports.add('useCallback');
647
+ const cbname = cp.name;
648
+ const cbparams = cp.params || '';
649
+ const cbdeps = cp.deps || '';
650
+ const cbcode = handlerCode(child);
651
+ const cbParamList = parseParamList(cbparams);
652
+ const cbDepsArr = cbdeps ? `[${cbdeps}]` : '[]';
653
+ lines.push(` const ${cbname} = useCallback((${cbParamList}) => {`);
654
+ if (cbcode) {
655
+ for (const line of cbcode.split('\n')) {
656
+ lines.push(` ${line}`);
657
+ }
658
+ }
659
+ lines.push(` }, ${cbDepsArr});`);
660
+ break;
661
+ }
662
+ case 'effect': {
663
+ reactImports.add('useEffect');
664
+ const edeps = cp.deps || '';
665
+ const ecode = handlerCode(child);
666
+ const eDepsArr = edeps ? `[${edeps}]` : '[]';
667
+ lines.push(` useEffect(() => {`);
668
+ if (ecode) {
669
+ for (const line of ecode.split('\n')) {
670
+ lines.push(` ${line}`);
671
+ }
672
+ }
673
+ // Check for cleanup block
674
+ const cleanupNode = firstChild(child, 'cleanup');
675
+ if (cleanupNode) {
676
+ const cleanupCode = p(cleanupNode).code || '';
677
+ const cleanupDedented = dedent(cleanupCode);
678
+ lines.push(` return () => {`);
679
+ for (const line of cleanupDedented.split('\n')) {
680
+ lines.push(` ${line}`);
681
+ }
682
+ lines.push(` };`);
683
+ }
684
+ lines.push(` }, ${eDepsArr});`);
685
+ break;
686
+ }
687
+ // Skip unknown child types silently
688
+ }
689
+ }
690
+ // Returns — always last
691
+ if (returnsNode) {
692
+ const rnames = p(returnsNode).names || '';
693
+ const entries = rnames.split(',').map(e => {
694
+ const [key, ...valueParts] = e.split(':');
695
+ const value = valueParts.join(':').trim();
696
+ return value ? `${key.trim()}: ${value}` : key.trim();
697
+ });
698
+ lines.push(` return { ${entries.join(', ')} };`);
699
+ }
700
+ lines.push('}');
701
+ // Prepend React imports
702
+ if (reactImports.size > 0) {
703
+ const importLine = `import { ${[...reactImports].sort().join(', ')} } from 'react';`;
704
+ lines.unshift('');
705
+ lines.unshift(importLine);
706
+ }
707
+ return lines;
708
+ }
709
+ // ── Dispatcher ───────────────────────────────────────────────────────────
710
+ export const CORE_NODE_TYPES = new Set([
711
+ 'type', 'interface', 'field', 'fn',
712
+ 'machine', 'transition',
713
+ 'error', 'module', 'export',
714
+ 'config', 'store',
715
+ 'test', 'describe', 'it',
716
+ 'event', 'import', 'const',
717
+ 'hook',
718
+ 'template', 'slot', 'body',
719
+ ]);
720
+ /** Check if a node type is a core language construct. */
721
+ export function isCoreNode(type) {
722
+ return CORE_NODE_TYPES.has(type);
723
+ }
724
+ /** Generate TypeScript for any core language node. */
725
+ export function generateCoreNode(node) {
726
+ switch (node.type) {
727
+ case 'type': return generateType(node);
728
+ case 'interface': return generateInterface(node);
729
+ case 'fn': return generateFunction(node);
730
+ case 'machine': return generateMachine(node);
731
+ case 'error': return generateError(node);
732
+ case 'module': return generateModule(node);
733
+ case 'config': return generateConfig(node);
734
+ case 'store': return generateStore(node);
735
+ case 'test': return generateTest(node);
736
+ case 'event': return generateEvent(node);
737
+ case 'import': return generateImport(node);
738
+ case 'const': return generateConst(node);
739
+ case 'hook': return generateHook(node);
740
+ // Template definitions produce no output
741
+ case 'template': return [];
742
+ case 'slot': return [];
743
+ case 'body': return [];
744
+ default:
745
+ // Check if this is a template instance
746
+ if (isTemplateNode(node.type))
747
+ return expandTemplateNode(node);
748
+ return [];
749
+ }
750
+ }
751
+ //# sourceMappingURL=codegen-core.js.map