@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.
- package/LICENSE +661 -0
- package/dist/codegen-core.d.ts +30 -0
- package/dist/codegen-core.js +751 -0
- package/dist/codegen-core.js.map +1 -0
- package/dist/config.d.ts +69 -0
- package/dist/config.js +78 -0
- package/dist/config.js.map +1 -0
- package/dist/decompiler.d.ts +2 -0
- package/dist/decompiler.js +44 -0
- package/dist/decompiler.js.map +1 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +40 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/parser.d.ts +4 -0
- package/dist/parser.js +380 -0
- package/dist/parser.js.map +1 -0
- package/dist/scanner.d.ts +40 -0
- package/dist/scanner.js +380 -0
- package/dist/scanner.js.map +1 -0
- package/dist/spec.d.ts +17 -0
- package/dist/spec.js +101 -0
- package/dist/spec.js.map +1 -0
- package/dist/styles-react.d.ts +3 -0
- package/dist/styles-react.js +20 -0
- package/dist/styles-react.js.map +1 -0
- package/dist/styles-tailwind.d.ts +8 -0
- package/dist/styles-tailwind.js +197 -0
- package/dist/styles-tailwind.js.map +1 -0
- package/dist/template-catalog.d.ts +26 -0
- package/dist/template-catalog.js +228 -0
- package/dist/template-catalog.js.map +1 -0
- package/dist/template-engine.d.ts +33 -0
- package/dist/template-engine.js +196 -0
- package/dist/template-engine.js.map +1 -0
- package/dist/types.d.ts +94 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +12 -0
- package/dist/utils.js +62 -0
- package/dist/utils.js.map +1 -0
- package/dist/version-adapters.d.ts +76 -0
- package/dist/version-adapters.js +172 -0
- package/dist/version-adapters.js.map +1 -0
- package/dist/version-detect.d.ts +30 -0
- package/dist/version-detect.js +63 -0
- package/dist/version-detect.js.map +1 -0
- 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
|