naidejs 1.0.0 → 1.2.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/README.md +299 -3
- package/SPEC-X.nx +80 -7
- package/SPEC.naide +117 -9
- package/bin/naide.js +59 -2
- package/examples/fullapp.naide +74 -0
- package/examples/fullapp.nx +74 -0
- package/package.json +13 -5
- package/src/generator.js +493 -19
- package/src/index.js +2 -2
- package/src/parser.js +376 -73
- package/src/preprocess.js +8 -0
- package/src/runtime.js +425 -0
- package/src/tokens.js +24 -0
package/src/generator.js
CHANGED
|
@@ -1,13 +1,43 @@
|
|
|
1
1
|
export class Generator {
|
|
2
|
-
constructor() {
|
|
2
|
+
constructor(options = {}) {
|
|
3
3
|
this.indent = 0;
|
|
4
4
|
this.output = [];
|
|
5
5
|
this.usesExpress = false;
|
|
6
6
|
this.models = new Map();
|
|
7
|
+
this.runtimeImports = new Set();
|
|
8
|
+
this.schemas = new Map();
|
|
9
|
+
this.needsEventBus = false;
|
|
10
|
+
this.runtimePath = options.runtimePath || 'naidejs/runtime';
|
|
11
|
+
this.dbDir = null;
|
|
12
|
+
this.authSecret = null;
|
|
13
|
+
this.hasWs = false;
|
|
14
|
+
this.wsNodes = [];
|
|
7
15
|
}
|
|
8
16
|
|
|
9
17
|
generate(ast) {
|
|
10
18
|
this.visitProgram(ast);
|
|
19
|
+
|
|
20
|
+
const preamble = [];
|
|
21
|
+
|
|
22
|
+
if (this.needsEventBus) {
|
|
23
|
+
this.runtimeImports.add('createEventBus');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (this.runtimeImports.size > 0) {
|
|
27
|
+
const imports = [...this.runtimeImports].join(', ');
|
|
28
|
+
preamble.push(`import { ${imports} } from '${this.runtimePath}';`);
|
|
29
|
+
preamble.push('');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (this.needsEventBus) {
|
|
33
|
+
preamble.push('const __eventBus = createEventBus();');
|
|
34
|
+
preamble.push('');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (preamble.length > 0) {
|
|
38
|
+
this.output.unshift(...preamble);
|
|
39
|
+
}
|
|
40
|
+
|
|
11
41
|
return this.output.join('\n');
|
|
12
42
|
}
|
|
13
43
|
|
|
@@ -32,6 +62,7 @@ export class Generator {
|
|
|
32
62
|
case 'Function': return this.visitFunction(node);
|
|
33
63
|
case 'Return': return this.visitReturn(node);
|
|
34
64
|
case 'ReturnStatus': return this.visitReturnStatus(node);
|
|
65
|
+
case 'ReturnMethod': return this.visitReturnMethod(node);
|
|
35
66
|
case 'TypedVar': return this.visitTypedVar(node);
|
|
36
67
|
case 'If': return this.visitIf(node);
|
|
37
68
|
case 'Each': return this.visitEach(node);
|
|
@@ -50,7 +81,21 @@ export class Generator {
|
|
|
50
81
|
case 'CompoundAssign': return this.visitCompoundAssign(node);
|
|
51
82
|
case 'ExprStatement': this.emit(this.expr(node.expression) + ';'); return;
|
|
52
83
|
case 'DbConnect': return this.visitDbConnect(node);
|
|
84
|
+
case 'DbDir': return this.visitDbDir(node);
|
|
53
85
|
case 'AwaitAll': return this.visitAwaitAllStatement(node);
|
|
86
|
+
case 'SchemaDecl': return this.visitSchema(node);
|
|
87
|
+
case 'CrudDecl': return this.visitCrudTopLevel(node);
|
|
88
|
+
case 'AuthDecl': return this.visitAuthTopLevel(node);
|
|
89
|
+
case 'CorsDecl': return this.visitCorsTopLevel(node);
|
|
90
|
+
case 'LimitDecl': return this.visitLimitTopLevel(node);
|
|
91
|
+
case 'EnvDecl': return this.visitEnv(node);
|
|
92
|
+
case 'EveryDecl': return this.visitEvery(node);
|
|
93
|
+
case 'WatchDecl': return this.visitWatch(node);
|
|
94
|
+
case 'StaticDecl': return this.visitStaticTopLevel(node);
|
|
95
|
+
case 'WsDecl': return this.visitWsTopLevel(node);
|
|
96
|
+
case 'GroupDecl': return this.visitGroupTopLevel(node);
|
|
97
|
+
case 'ErrorHandler': return this.visitErrorHandlerTopLevel(node);
|
|
98
|
+
case 'CookieDecl': return this.visitCookieTopLevel(node);
|
|
54
99
|
default:
|
|
55
100
|
this.emit(`/* unknown: ${node.type} */`);
|
|
56
101
|
}
|
|
@@ -101,6 +146,26 @@ export class Generator {
|
|
|
101
146
|
this.emit(`return res.status(${this.expr(node.statusCode)}).json(${this.expr(node.body)});`);
|
|
102
147
|
}
|
|
103
148
|
|
|
149
|
+
visitReturnMethod(node) {
|
|
150
|
+
const val = this.expr(node.value);
|
|
151
|
+
switch (node.method) {
|
|
152
|
+
case 'redirect':
|
|
153
|
+
this.emit(`return res.redirect(${val});`);
|
|
154
|
+
break;
|
|
155
|
+
case 'html':
|
|
156
|
+
this.emit(`return res.type('html').send(${val});`);
|
|
157
|
+
break;
|
|
158
|
+
case 'text':
|
|
159
|
+
this.emit(`return res.type('text').send(${val});`);
|
|
160
|
+
break;
|
|
161
|
+
case 'file':
|
|
162
|
+
this.emit(`return res.sendFile(${val});`);
|
|
163
|
+
break;
|
|
164
|
+
default:
|
|
165
|
+
this.emit(`return res.${node.method}(${val});`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
104
169
|
visitTypedVar(node) {
|
|
105
170
|
const keyword = node.isMut ? 'let' : 'const';
|
|
106
171
|
const exp = node.isPublic ? 'export ' : '';
|
|
@@ -111,7 +176,6 @@ export class Generator {
|
|
|
111
176
|
const target = this.expr(node.target);
|
|
112
177
|
const value = this.expr(node.value);
|
|
113
178
|
|
|
114
|
-
// Destructuring assignment
|
|
115
179
|
if (node.target.type === 'Array') {
|
|
116
180
|
const names = node.target.elements.map(e => this.expr(e)).join(', ');
|
|
117
181
|
this.emit(`const [${names}] = ${value};`);
|
|
@@ -219,35 +283,72 @@ export class Generator {
|
|
|
219
283
|
}
|
|
220
284
|
|
|
221
285
|
visitServer(node) {
|
|
286
|
+
const hasWs = node.routes.some(r => r.type === 'WsDecl');
|
|
287
|
+
|
|
222
288
|
this.emit(`import express from 'express';`);
|
|
289
|
+
if (hasWs) {
|
|
290
|
+
this.emit(`import { WebSocketServer } from 'ws';`);
|
|
291
|
+
}
|
|
223
292
|
this.emitRaw('');
|
|
224
293
|
this.emit(`const ${node.name} = express();`);
|
|
225
294
|
this.emit(`${node.name}.use(express.json());`);
|
|
295
|
+
this.emit(`${node.name}.use(express.urlencoded({ extended: true }));`);
|
|
226
296
|
this.emitRaw('');
|
|
227
297
|
|
|
228
|
-
// Middleware
|
|
229
298
|
for (const mid of node.middleware) {
|
|
230
299
|
this.emit(`${node.name}.use(${this.generateMiddleware(mid)});`);
|
|
231
300
|
this.emitRaw('');
|
|
232
301
|
}
|
|
233
302
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
303
|
+
const wsNodes = [];
|
|
304
|
+
const errorHandlers = [];
|
|
305
|
+
|
|
306
|
+
for (const child of node.routes) {
|
|
307
|
+
if (child.type === 'Route') {
|
|
308
|
+
this.visitRoute(node.name, child);
|
|
309
|
+
} else if (child.type === 'CrudDecl') {
|
|
310
|
+
this.visitCrud(node.name, child);
|
|
311
|
+
} else if (child.type === 'AuthDecl') {
|
|
312
|
+
this.visitAuth(node.name, child);
|
|
313
|
+
} else if (child.type === 'CorsDecl') {
|
|
314
|
+
this.visitCors(node.name, child);
|
|
315
|
+
} else if (child.type === 'LimitDecl') {
|
|
316
|
+
this.visitLimit(node.name, child);
|
|
317
|
+
} else if (child.type === 'StaticDecl') {
|
|
318
|
+
this.visitStatic(node.name, child);
|
|
319
|
+
} else if (child.type === 'WsDecl') {
|
|
320
|
+
wsNodes.push(child);
|
|
321
|
+
} else if (child.type === 'GroupDecl') {
|
|
322
|
+
this.visitGroup(node.name, child);
|
|
323
|
+
} else if (child.type === 'ErrorHandler') {
|
|
324
|
+
errorHandlers.push(child);
|
|
325
|
+
} else if (child.type === 'CookieDecl') {
|
|
326
|
+
this.visitCookie(node.name, child);
|
|
238
327
|
} else {
|
|
239
|
-
this.visitStatement(
|
|
328
|
+
this.visitStatement(child);
|
|
240
329
|
}
|
|
241
330
|
}
|
|
242
331
|
|
|
243
|
-
|
|
332
|
+
for (const eh of errorHandlers) {
|
|
333
|
+
this.visitErrorHandler(node.name, eh);
|
|
334
|
+
}
|
|
335
|
+
|
|
244
336
|
const port = node.port ? this.expr(node.port) : '3000';
|
|
245
337
|
this.emitRaw('');
|
|
246
|
-
|
|
338
|
+
if (hasWs) {
|
|
339
|
+
this.emit(`const __server = ${node.name}.listen(${port}, () => {`);
|
|
340
|
+
} else {
|
|
341
|
+
this.emit(`${node.name}.listen(${port}, () => {`);
|
|
342
|
+
}
|
|
247
343
|
this.indent++;
|
|
248
344
|
this.emit(`console.log(\`Server running on port \${${port}}\`);`);
|
|
249
345
|
this.indent--;
|
|
250
346
|
this.emit('});');
|
|
347
|
+
|
|
348
|
+
for (const wsNode of wsNodes) {
|
|
349
|
+
this.emitRaw('');
|
|
350
|
+
this.visitWs(wsNode);
|
|
351
|
+
}
|
|
251
352
|
}
|
|
252
353
|
|
|
253
354
|
visitRoute(appName, route) {
|
|
@@ -255,17 +356,14 @@ export class Generator {
|
|
|
255
356
|
const path = this.stringValue(route.path);
|
|
256
357
|
const params = route.params.length > 0 ? route.params.join(', ') : 'req, res';
|
|
257
358
|
|
|
258
|
-
// Check if body uses await
|
|
259
359
|
const needsAsync = this.bodyUsesAwait(route.body);
|
|
260
360
|
const asyncPrefix = needsAsync ? 'async ' : '';
|
|
261
361
|
|
|
262
362
|
this.emit(`${appName}.${method}(${path}, ${asyncPrefix}(${params}) => {`);
|
|
263
363
|
this.indent++;
|
|
264
364
|
|
|
265
|
-
// If params don't include res, inject it
|
|
266
365
|
const hasRes = params.includes('res');
|
|
267
366
|
|
|
268
|
-
// Transform body: last expression with ret becomes res.json
|
|
269
367
|
for (let i = 0; i < route.body.length; i++) {
|
|
270
368
|
const stmt = route.body[i];
|
|
271
369
|
if (stmt.type === 'Return' && stmt.value !== null) {
|
|
@@ -326,7 +424,6 @@ export class Generator {
|
|
|
326
424
|
this.emitRaw('');
|
|
327
425
|
}
|
|
328
426
|
|
|
329
|
-
// Methods
|
|
330
427
|
for (const method of node.methods) {
|
|
331
428
|
const async = method.isAsync ? 'async ' : '';
|
|
332
429
|
const params = method.params.map(p => {
|
|
@@ -353,8 +450,6 @@ export class Generator {
|
|
|
353
450
|
|
|
354
451
|
visitOn(node) {
|
|
355
452
|
const event = this.expr(node.event);
|
|
356
|
-
// Split event into object and event name
|
|
357
|
-
// e.g., process.exit -> process.on('exit', ...)
|
|
358
453
|
if (node.event.type === 'MemberAccess') {
|
|
359
454
|
const obj = this.expr(node.event.object);
|
|
360
455
|
const evt = node.event.property;
|
|
@@ -376,7 +471,6 @@ export class Generator {
|
|
|
376
471
|
|
|
377
472
|
visitThrow(node) {
|
|
378
473
|
const value = this.expr(node.value);
|
|
379
|
-
// If it's a string, wrap in Error
|
|
380
474
|
if (node.value.type === 'String') {
|
|
381
475
|
this.emit(`throw new Error(${value});`);
|
|
382
476
|
} else {
|
|
@@ -388,12 +482,348 @@ export class Generator {
|
|
|
388
482
|
this.emit(`const db = new Database(${this.expr(node.connectionString)});`);
|
|
389
483
|
}
|
|
390
484
|
|
|
485
|
+
visitDbDir(node) {
|
|
486
|
+
const raw = node.path.raw || node.path.parts?.map(p => p.value).join('') || 'data/';
|
|
487
|
+
this.dbDir = raw.endsWith('/') ? raw : raw + '/';
|
|
488
|
+
}
|
|
489
|
+
|
|
391
490
|
visitAwaitAllStatement(node) {
|
|
392
491
|
const exprs = node.expressions.map(e => this.expr(e)).join(', ');
|
|
393
492
|
this.emit(`await Promise.all([${exprs}]);`);
|
|
394
493
|
}
|
|
395
494
|
|
|
396
|
-
//
|
|
495
|
+
// ===== High-level features =====
|
|
496
|
+
|
|
497
|
+
visitSchema(node) {
|
|
498
|
+
this.runtimeImports.add('createSchema');
|
|
499
|
+
if (this.dbDir) {
|
|
500
|
+
this.runtimeImports.add('createFileStore');
|
|
501
|
+
} else {
|
|
502
|
+
this.runtimeImports.add('createStore');
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
this.emit(`const ${node.name}Schema = createSchema('${node.name}', {`);
|
|
506
|
+
this.indent++;
|
|
507
|
+
|
|
508
|
+
for (const field of node.fields) {
|
|
509
|
+
const props = [];
|
|
510
|
+
|
|
511
|
+
switch (field.type) {
|
|
512
|
+
case 'auto':
|
|
513
|
+
props.push("type: 'id'", 'auto: true');
|
|
514
|
+
break;
|
|
515
|
+
case 'timestamp':
|
|
516
|
+
props.push("type: 'timestamp'");
|
|
517
|
+
break;
|
|
518
|
+
case 'str':
|
|
519
|
+
props.push("type: 'string'");
|
|
520
|
+
break;
|
|
521
|
+
case 'int':
|
|
522
|
+
props.push("type: 'integer'");
|
|
523
|
+
break;
|
|
524
|
+
case 'num':
|
|
525
|
+
props.push("type: 'number'");
|
|
526
|
+
break;
|
|
527
|
+
case 'bool':
|
|
528
|
+
props.push("type: 'boolean'");
|
|
529
|
+
break;
|
|
530
|
+
case 'enum':
|
|
531
|
+
if (field.enumValues) {
|
|
532
|
+
const vals = field.enumValues.map(v => this.expr(v)).join(', ');
|
|
533
|
+
props.push("type: 'enum'", `values: [${vals}]`);
|
|
534
|
+
} else {
|
|
535
|
+
props.push("type: 'enum'");
|
|
536
|
+
}
|
|
537
|
+
break;
|
|
538
|
+
default:
|
|
539
|
+
props.push(`type: '${field.type}'`);
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
for (const mod of field.modifiers) {
|
|
544
|
+
switch (mod.name) {
|
|
545
|
+
case 'required': props.push('required: true'); break;
|
|
546
|
+
case 'optional': props.push('required: false'); break;
|
|
547
|
+
case 'unique': props.push('unique: true'); break;
|
|
548
|
+
case 'email': props.push('email: true'); break;
|
|
549
|
+
case 'url': props.push('url: true'); break;
|
|
550
|
+
case 'auto': props.push('auto: true'); break;
|
|
551
|
+
case 'min': props.push(`min: ${this.expr(mod.args[0])}`); break;
|
|
552
|
+
case 'max': props.push(`max: ${this.expr(mod.args[0])}`); break;
|
|
553
|
+
case 'default': props.push(`default: ${this.expr(mod.args[0])}`); break;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
this.emit(`${field.name}: { ${props.join(', ')} },`);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
this.indent--;
|
|
561
|
+
this.emit('});');
|
|
562
|
+
if (this.dbDir) {
|
|
563
|
+
this.emit(`const ${node.name}Store = createFileStore(${node.name}Schema, '${this.dbDir}${node.name}.json');`);
|
|
564
|
+
} else {
|
|
565
|
+
this.emit(`const ${node.name}Store = createStore(${node.name}Schema);`);
|
|
566
|
+
}
|
|
567
|
+
this.emitRaw('');
|
|
568
|
+
|
|
569
|
+
this.schemas.set(node.name, node);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
visitCrud(appName, node) {
|
|
573
|
+
this.runtimeImports.add('registerCrud');
|
|
574
|
+
this.needsEventBus = true;
|
|
575
|
+
|
|
576
|
+
const path = this.stringValue(node.path);
|
|
577
|
+
this.emit(`registerCrud(${appName}, ${path}, ${node.schemaName}Schema, ${node.schemaName}Store, __eventBus);`);
|
|
578
|
+
this.emitRaw('');
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
visitCrudTopLevel(node) {
|
|
582
|
+
this.visitCrud('app', node);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
visitAuth(appName, node) {
|
|
586
|
+
this.runtimeImports.add('jwtAuth');
|
|
587
|
+
|
|
588
|
+
const secret = this.expr(node.secret);
|
|
589
|
+
this.authSecret = secret;
|
|
590
|
+
|
|
591
|
+
this.emit(`const __authSecret = ${secret};`);
|
|
592
|
+
|
|
593
|
+
const options = [];
|
|
594
|
+
if (node.publicPaths.length > 0) {
|
|
595
|
+
const paths = node.publicPaths.map(p => this.stringValue(p)).join(', ');
|
|
596
|
+
options.push(`public: [${paths}]`);
|
|
597
|
+
}
|
|
598
|
+
const optStr = options.length > 0 ? `, { ${options.join(', ')} }` : '';
|
|
599
|
+
|
|
600
|
+
if (node.protectedPaths.length > 0) {
|
|
601
|
+
const path = this.stringValue(node.protectedPaths[0]);
|
|
602
|
+
this.emit(`${appName}.use(${path}, jwtAuth(${secret}${optStr}));`);
|
|
603
|
+
} else {
|
|
604
|
+
this.emit(`${appName}.use(jwtAuth(${secret}${optStr}));`);
|
|
605
|
+
}
|
|
606
|
+
this.emitRaw('');
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
visitAuthTopLevel(node) {
|
|
610
|
+
this.visitAuth('app', node);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
visitCors(appName, node) {
|
|
614
|
+
this.runtimeImports.add('corsMiddleware');
|
|
615
|
+
|
|
616
|
+
const origins = this.expr(node.origins);
|
|
617
|
+
if (node.origins.type === 'String') {
|
|
618
|
+
this.emit(`${appName}.use(corsMiddleware([${origins}]));`);
|
|
619
|
+
} else {
|
|
620
|
+
this.emit(`${appName}.use(corsMiddleware(${origins}));`);
|
|
621
|
+
}
|
|
622
|
+
this.emitRaw('');
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
visitCorsTopLevel(node) {
|
|
626
|
+
this.visitCors('app', node);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
visitLimit(appName, node) {
|
|
630
|
+
this.runtimeImports.add('rateLimit');
|
|
631
|
+
|
|
632
|
+
const path = this.stringValue(node.path);
|
|
633
|
+
const max = this.expr(node.max);
|
|
634
|
+
const window = this.expr(node.window);
|
|
635
|
+
this.emit(`${appName}.use(${path}, rateLimit(${max}, ${window}));`);
|
|
636
|
+
this.emitRaw('');
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
visitLimitTopLevel(node) {
|
|
640
|
+
this.visitLimit('app', node);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
visitStatic(appName, node) {
|
|
644
|
+
let raw = node.path.raw || node.path.parts?.map(p => p.value).join('') || 'public';
|
|
645
|
+
if (raw.startsWith('/')) raw = raw.slice(1);
|
|
646
|
+
this.emit(`${appName}.use(express.static(${JSON.stringify(raw)}));`);
|
|
647
|
+
this.emitRaw('');
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
visitStaticTopLevel(node) {
|
|
651
|
+
this.visitStatic('app', node);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
visitWs(node) {
|
|
655
|
+
const path = this.stringValue(node.path);
|
|
656
|
+
this.emit(`const __wss = new WebSocketServer({ server: __server, path: ${path} });`);
|
|
657
|
+
this.emit(`__wss.on('connection', (__ws) => {`);
|
|
658
|
+
this.indent++;
|
|
659
|
+
this.emit(`const send = (d) => __ws.send(typeof d === 'string' ? d : JSON.stringify(d));`);
|
|
660
|
+
this.emit(`const broadcast = (d) => { const m = typeof d === 'string' ? d : JSON.stringify(d); for (const c of __wss.clients) if (c.readyState === 1) c.send(m); };`);
|
|
661
|
+
|
|
662
|
+
const connectEvents = node.events.filter(e => {
|
|
663
|
+
const name = e.name.raw || e.name.parts?.map(p => p.value).join('');
|
|
664
|
+
return name === 'connect' || name === 'open';
|
|
665
|
+
});
|
|
666
|
+
const otherEvents = node.events.filter(e => {
|
|
667
|
+
const name = e.name.raw || e.name.parts?.map(p => p.value).join('');
|
|
668
|
+
return name !== 'connect' && name !== 'open';
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
for (const evt of connectEvents) {
|
|
672
|
+
this.emitRaw('');
|
|
673
|
+
for (const stmt of evt.body) this.visitStatement(stmt);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
for (const evt of otherEvents) {
|
|
677
|
+
const evtName = evt.name.raw || evt.name.parts?.map(p => p.value).join('');
|
|
678
|
+
this.emitRaw('');
|
|
679
|
+
|
|
680
|
+
if (evtName === 'message') {
|
|
681
|
+
const needsAsync = this.bodyUsesAwait(evt.body);
|
|
682
|
+
const asyncPrefix = needsAsync ? 'async ' : '';
|
|
683
|
+
this.emit(`__ws.on('message', ${asyncPrefix}(__raw) => {`);
|
|
684
|
+
this.indent++;
|
|
685
|
+
const dataParam = evt.params[0] || 'data';
|
|
686
|
+
this.emit(`const ${dataParam} = JSON.parse(__raw);`);
|
|
687
|
+
for (const stmt of evt.body) this.visitStatement(stmt);
|
|
688
|
+
this.indent--;
|
|
689
|
+
this.emit('});');
|
|
690
|
+
} else {
|
|
691
|
+
const params = evt.params.length > 0 ? evt.params.join(', ') : '';
|
|
692
|
+
const needsAsync = this.bodyUsesAwait(evt.body);
|
|
693
|
+
const asyncPrefix = needsAsync ? 'async ' : '';
|
|
694
|
+
this.emit(`__ws.on('${evtName}', ${asyncPrefix}(${params}) => {`);
|
|
695
|
+
this.indent++;
|
|
696
|
+
for (const stmt of evt.body) this.visitStatement(stmt);
|
|
697
|
+
this.indent--;
|
|
698
|
+
this.emit('});');
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
this.indent--;
|
|
703
|
+
this.emit('});');
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
visitWsTopLevel(node) {
|
|
707
|
+
this.visitWs(node);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
visitGroup(appName, node) {
|
|
711
|
+
const prefix = this.stringValue(node.prefix);
|
|
712
|
+
this.groupCounter = (this.groupCounter || 0) + 1;
|
|
713
|
+
const routerName = `__router${this.groupCounter}`;
|
|
714
|
+
this.emit(`const ${routerName} = express.Router();`);
|
|
715
|
+
|
|
716
|
+
for (const child of node.routes) {
|
|
717
|
+
if (child.type === 'Route') {
|
|
718
|
+
this.visitRoute(routerName, child);
|
|
719
|
+
} else if (child.type === 'CrudDecl') {
|
|
720
|
+
this.visitCrud(routerName, child);
|
|
721
|
+
} else if (child.type === 'AuthDecl') {
|
|
722
|
+
this.visitAuth(routerName, child);
|
|
723
|
+
} else if (child.type === 'GroupDecl') {
|
|
724
|
+
this.visitGroup(routerName, child);
|
|
725
|
+
} else {
|
|
726
|
+
this.visitStatement(child);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
this.emit(`${appName}.use(${prefix}, ${routerName});`);
|
|
731
|
+
this.emitRaw('');
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
visitGroupTopLevel(node) {
|
|
735
|
+
this.visitGroup('app', node);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
visitErrorHandler(appName, node) {
|
|
739
|
+
const params = node.params.join(', ');
|
|
740
|
+
this.emit(`${appName}.use((${params}, next) => {`);
|
|
741
|
+
this.indent++;
|
|
742
|
+
for (const stmt of node.body) this.visitStatement(stmt);
|
|
743
|
+
this.indent--;
|
|
744
|
+
this.emit('});');
|
|
745
|
+
this.emitRaw('');
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
visitErrorHandlerTopLevel(node) {
|
|
749
|
+
this.visitErrorHandler('app', node);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
visitCookie(appName, node) {
|
|
753
|
+
this.runtimeImports.add('cookieParser');
|
|
754
|
+
this.emit(`${appName}.use(cookieParser());`);
|
|
755
|
+
this.emitRaw('');
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
visitCookieTopLevel(node) {
|
|
759
|
+
this.visitCookie('app', node);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
visitEnv(node) {
|
|
763
|
+
this.runtimeImports.add('loadEnv');
|
|
764
|
+
|
|
765
|
+
const names = node.vars.map(v => v.name);
|
|
766
|
+
this.emit(`const { ${names.join(', ')} } = loadEnv({`);
|
|
767
|
+
this.indent++;
|
|
768
|
+
|
|
769
|
+
for (const v of node.vars) {
|
|
770
|
+
const props = [];
|
|
771
|
+
|
|
772
|
+
switch (v.type) {
|
|
773
|
+
case 'str': props.push("type: 'string'"); break;
|
|
774
|
+
case 'int': props.push("type: 'integer'"); break;
|
|
775
|
+
case 'num': props.push("type: 'number'"); break;
|
|
776
|
+
case 'bool': props.push("type: 'boolean'"); break;
|
|
777
|
+
default: props.push(`type: '${v.type}'`); break;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
for (const mod of v.modifiers) {
|
|
781
|
+
switch (mod.name) {
|
|
782
|
+
case 'required': props.push('required: true'); break;
|
|
783
|
+
case 'default': props.push(`default: ${this.expr(mod.args[0])}`); break;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
this.emit(`${v.name}: { ${props.join(', ')} },`);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
this.indent--;
|
|
791
|
+
this.emit('});');
|
|
792
|
+
this.emitRaw('');
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
visitEvery(node) {
|
|
796
|
+
this.runtimeImports.add('scheduleEvery');
|
|
797
|
+
|
|
798
|
+
const interval = this.expr(node.interval);
|
|
799
|
+
const needsAsync = this.bodyUsesAwait(node.body);
|
|
800
|
+
const asyncPrefix = needsAsync ? 'async ' : '';
|
|
801
|
+
|
|
802
|
+
this.emit(`scheduleEvery(${interval}, ${asyncPrefix}() => {`);
|
|
803
|
+
this.indent++;
|
|
804
|
+
for (const stmt of node.body) this.visitStatement(stmt);
|
|
805
|
+
this.indent--;
|
|
806
|
+
this.emit('});');
|
|
807
|
+
this.emitRaw('');
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
visitWatch(node) {
|
|
811
|
+
this.needsEventBus = true;
|
|
812
|
+
|
|
813
|
+
const params = node.params.length > 0 ? node.params.join(', ') : 'event';
|
|
814
|
+
const needsAsync = this.bodyUsesAwait(node.body);
|
|
815
|
+
const asyncPrefix = needsAsync ? 'async ' : '';
|
|
816
|
+
|
|
817
|
+
this.emit(`__eventBus.on('${node.eventName}', ${asyncPrefix}(${params}) => {`);
|
|
818
|
+
this.indent++;
|
|
819
|
+
for (const stmt of node.body) this.visitStatement(stmt);
|
|
820
|
+
this.indent--;
|
|
821
|
+
this.emit('});');
|
|
822
|
+
this.emitRaw('');
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// ===== Expression generation =====
|
|
826
|
+
|
|
397
827
|
expr(node) {
|
|
398
828
|
if (!node) return 'undefined';
|
|
399
829
|
|
|
@@ -430,7 +860,7 @@ export class Generator {
|
|
|
430
860
|
return `${this.expr(node.object)}?.${node.property}`;
|
|
431
861
|
|
|
432
862
|
case 'Call':
|
|
433
|
-
return
|
|
863
|
+
return this.generateCall(node);
|
|
434
864
|
|
|
435
865
|
case 'IndexAccess':
|
|
436
866
|
return `${this.expr(node.object)}[${this.expr(node.index)}]`;
|
|
@@ -483,6 +913,50 @@ export class Generator {
|
|
|
483
913
|
}
|
|
484
914
|
}
|
|
485
915
|
|
|
916
|
+
generateCall(node) {
|
|
917
|
+
const AUTO_IMPORT = { 'hash': 'hash', 'verify': 'verify', 'uuid': 'uuid' };
|
|
918
|
+
|
|
919
|
+
if (node.callee.type === 'Identifier' && AUTO_IMPORT[node.callee.name]) {
|
|
920
|
+
const runtimeFn = AUTO_IMPORT[node.callee.name];
|
|
921
|
+
this.runtimeImports.add(runtimeFn);
|
|
922
|
+
return `${runtimeFn}(${node.args.map(a => this.expr(a)).join(', ')})`;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
if (node.callee.type === 'Identifier' && node.callee.name === 'sign') {
|
|
926
|
+
this.runtimeImports.add('jwtSign');
|
|
927
|
+
const args = node.args.map(a => this.expr(a));
|
|
928
|
+
if (args.length === 1 && this.authSecret) {
|
|
929
|
+
return `jwtSign(${args[0]}, __authSecret)`;
|
|
930
|
+
}
|
|
931
|
+
return `jwtSign(${args.join(', ')})`;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
if (node.callee.type === 'MemberAccess' &&
|
|
935
|
+
node.callee.object.type === 'Identifier' &&
|
|
936
|
+
node.callee.object.name === 'api') {
|
|
937
|
+
this.runtimeImports.add('api');
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
if (node.callee.type === 'MemberAccess' &&
|
|
941
|
+
node.callee.object.type === 'Identifier' &&
|
|
942
|
+
node.callee.object.name === 'auth') {
|
|
943
|
+
if (node.callee.property === 'sign') {
|
|
944
|
+
this.runtimeImports.add('jwtSign');
|
|
945
|
+
const args = node.args.map(a => this.expr(a));
|
|
946
|
+
if (args.length === 1 && this.authSecret) {
|
|
947
|
+
return `jwtSign(${args[0]}, __authSecret)`;
|
|
948
|
+
}
|
|
949
|
+
return `jwtSign(${args.join(', ')})`;
|
|
950
|
+
}
|
|
951
|
+
if (node.callee.property === 'verify') {
|
|
952
|
+
this.runtimeImports.add('jwtVerify');
|
|
953
|
+
return `jwtVerify(${node.args.map(a => this.expr(a)).join(', ')})`;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
return `${this.expr(node.callee)}(${node.args.map(a => this.expr(a)).join(', ')})`;
|
|
958
|
+
}
|
|
959
|
+
|
|
486
960
|
generateString(strData) {
|
|
487
961
|
if (!strData || !strData.parts) return '""';
|
|
488
962
|
|
package/src/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { Parser } from './parser.js';
|
|
|
3
3
|
import { Generator } from './generator.js';
|
|
4
4
|
import { preprocess } from './preprocess.js';
|
|
5
5
|
|
|
6
|
-
export function compile(source, { mode = 'naide' } = {}) {
|
|
6
|
+
export function compile(source, { mode = 'naide', runtimePath } = {}) {
|
|
7
7
|
if (mode === 'x') {
|
|
8
8
|
source = preprocess(source);
|
|
9
9
|
}
|
|
@@ -11,7 +11,7 @@ export function compile(source, { mode = 'naide' } = {}) {
|
|
|
11
11
|
const tokens = lexer.tokenize();
|
|
12
12
|
const parser = new Parser(tokens);
|
|
13
13
|
const ast = parser.parse();
|
|
14
|
-
const generator = new Generator();
|
|
14
|
+
const generator = new Generator({ runtimePath });
|
|
15
15
|
const js = generator.generate(ast);
|
|
16
16
|
return { js, ast, tokens, naide: mode === 'x' ? source : null };
|
|
17
17
|
}
|