naidejs 1.1.0 → 1.3.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 +183 -4
- package/SPEC-X.nx +80 -7
- package/SPEC.naide +117 -9
- package/bin/naide.js +55 -1
- package/examples/fullapp.naide +41 -4
- package/examples/fullapp.nx +41 -4
- package/package.json +9 -5
- package/src/generator.js +248 -5
- package/src/index.js +26 -8
- package/src/lexer.js +17 -4
- package/src/parser.js +179 -98
- package/src/preprocess.js +8 -0
- package/src/runtime.js +162 -4
- package/src/tokens.js +8 -0
package/src/parser.js
CHANGED
|
@@ -14,14 +14,11 @@ export class Parser {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
preprocessTokens(tokens) {
|
|
17
|
-
// Remove INDENT/DEDENT pairs around pipe continuation lines
|
|
18
|
-
// Pattern: NEWLINE INDENT PIPE -> NEWLINE PIPE (and remove matching DEDENT)
|
|
19
17
|
const result = [...tokens];
|
|
20
18
|
const indentsToRemove = new Set();
|
|
21
19
|
|
|
22
20
|
for (let i = 0; i < result.length; i++) {
|
|
23
21
|
if (result[i].type !== T.INDENT) continue;
|
|
24
|
-
// Look forward past any newlines for PIPE
|
|
25
22
|
let j = i + 1;
|
|
26
23
|
while (j < result.length && result[j].type === T.NEWLINE) j++;
|
|
27
24
|
if (j < result.length && result[j].type === T.PIPE) {
|
|
@@ -31,7 +28,6 @@ export class Parser {
|
|
|
31
28
|
|
|
32
29
|
if (indentsToRemove.size === 0) return result;
|
|
33
30
|
|
|
34
|
-
// For each INDENT to remove, find matching DEDENT
|
|
35
31
|
const dedentsToRemove = new Set();
|
|
36
32
|
for (const idx of indentsToRemove) {
|
|
37
33
|
let depth = 0;
|
|
@@ -65,7 +61,16 @@ export class Parser {
|
|
|
65
61
|
expect(type) {
|
|
66
62
|
const tok = this.advance();
|
|
67
63
|
if (tok.type !== type) {
|
|
68
|
-
|
|
64
|
+
const hints = {
|
|
65
|
+
COLON: "Missing ':' — blocks (if, fn, server, etc.) need a colon at the end",
|
|
66
|
+
INDENT: "Expected an indented block — check your indentation (use 2 spaces)",
|
|
67
|
+
RPAREN: "Missing closing ')'",
|
|
68
|
+
RBRACKET: "Missing closing ']'",
|
|
69
|
+
RBRACE: "Missing closing '}'",
|
|
70
|
+
IDENT: "Expected an identifier (variable/function name)",
|
|
71
|
+
};
|
|
72
|
+
const hint = hints[type] ? `\n Hint: ${hints[type]}` : '';
|
|
73
|
+
throw this.error(`Expected ${type} but got ${tok.type} ('${tok.value}')${hint}`, tok);
|
|
69
74
|
}
|
|
70
75
|
return tok;
|
|
71
76
|
}
|
|
@@ -85,16 +90,24 @@ export class Parser {
|
|
|
85
90
|
return types.includes(this.peek().type);
|
|
86
91
|
}
|
|
87
92
|
|
|
93
|
+
isIdentLike(type) {
|
|
94
|
+
return type === T.IDENT || TYPE_TOKENS.has(type) ||
|
|
95
|
+
type === T.LOG || type === T.DB || type === T.GET ||
|
|
96
|
+
type === T.POST || type === T.PUT || type === T.DEL ||
|
|
97
|
+
type === T.MATCH || type === T.ON || type === T.NEW ||
|
|
98
|
+
type === T.FROM || type === T.AS || type === T.SELF ||
|
|
99
|
+
type === T.SCHEMA || type === T.CRUD || type === T.AUTH ||
|
|
100
|
+
type === T.CORS || type === T.LIMIT || type === T.ENV ||
|
|
101
|
+
type === T.EVERY || type === T.WATCH || type === T.STATIC ||
|
|
102
|
+
type === T.WS || type === T.GROUP || type === T.COOKIE ||
|
|
103
|
+
type === T.NOT || type === T.AND || type === T.OR ||
|
|
104
|
+
type === T.IN || type === T.BREAK || type === T.CONTINUE ||
|
|
105
|
+
type === T.THROW || type === T.MUT || type === T.PUB;
|
|
106
|
+
}
|
|
107
|
+
|
|
88
108
|
expectPropertyName() {
|
|
89
109
|
const tok = this.advance();
|
|
90
|
-
if (
|
|
91
|
-
tok.type === T.LOG || tok.type === T.DB || tok.type === T.GET ||
|
|
92
|
-
tok.type === T.POST || tok.type === T.PUT || tok.type === T.DEL ||
|
|
93
|
-
tok.type === T.MATCH || tok.type === T.ON || tok.type === T.NEW ||
|
|
94
|
-
tok.type === T.FROM || tok.type === T.AS || tok.type === T.SELF ||
|
|
95
|
-
tok.type === T.SCHEMA || tok.type === T.CRUD || tok.type === T.AUTH ||
|
|
96
|
-
tok.type === T.CORS || tok.type === T.LIMIT || tok.type === T.ENV ||
|
|
97
|
-
tok.type === T.EVERY || tok.type === T.WATCH) {
|
|
110
|
+
if (this.isIdentLike(tok.type)) {
|
|
98
111
|
return tok.value;
|
|
99
112
|
}
|
|
100
113
|
throw this.error(`Expected property name but got ${tok.type} ('${tok.value}')`, tok);
|
|
@@ -148,7 +161,9 @@ export class Parser {
|
|
|
148
161
|
case T.BREAK: this.advance(); return new ASTNode('Break');
|
|
149
162
|
case T.CONTINUE: this.advance(); return new ASTNode('Continue');
|
|
150
163
|
case T.MUT: return this.parseMutVariable();
|
|
151
|
-
case T.DB:
|
|
164
|
+
case T.DB:
|
|
165
|
+
if (this.peek(1).type === T.DOT) return this.parseDbStatement();
|
|
166
|
+
return this.parseDbDir();
|
|
152
167
|
case T.AWAIT: return this.parseAwaitStatement();
|
|
153
168
|
case T.AWAIT_ALL: return this.parseAwaitAll();
|
|
154
169
|
case T.SCHEMA: return this.parseSchema();
|
|
@@ -173,6 +188,19 @@ export class Parser {
|
|
|
173
188
|
case T.WATCH:
|
|
174
189
|
if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
|
|
175
190
|
return this.parseWatch();
|
|
191
|
+
case T.STATIC:
|
|
192
|
+
if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
|
|
193
|
+
return this.parseStatic();
|
|
194
|
+
case T.WS:
|
|
195
|
+
if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
|
|
196
|
+
return this.parseWs();
|
|
197
|
+
case T.GROUP:
|
|
198
|
+
if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
|
|
199
|
+
return this.parseGroup();
|
|
200
|
+
case T.COOKIE:
|
|
201
|
+
if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
|
|
202
|
+
this.advance();
|
|
203
|
+
return new ASTNode('CookieDecl');
|
|
176
204
|
default:
|
|
177
205
|
if (TYPE_TOKENS.has(tok.type)) {
|
|
178
206
|
return this.parseTypedVariable();
|
|
@@ -181,14 +209,10 @@ export class Parser {
|
|
|
181
209
|
}
|
|
182
210
|
}
|
|
183
211
|
|
|
184
|
-
// use express
|
|
185
|
-
// use express from "express"
|
|
186
|
-
// use {readFile, writeFile} from "fs/promises"
|
|
187
212
|
parseUse() {
|
|
188
213
|
this.advance(); // use
|
|
189
214
|
|
|
190
215
|
if (this.at(T.LBRACE)) {
|
|
191
|
-
// use {a, b} from "module"
|
|
192
216
|
this.advance();
|
|
193
217
|
const names = [];
|
|
194
218
|
while (!this.at(T.RBRACE) && !this.at(T.EOF)) {
|
|
@@ -226,8 +250,6 @@ export class Parser {
|
|
|
226
250
|
return tok.value;
|
|
227
251
|
}
|
|
228
252
|
|
|
229
|
-
// fn name(params) -> returnType:
|
|
230
|
-
// body
|
|
231
253
|
parseFunction(isAsync, isPublic) {
|
|
232
254
|
this.advance(); // fn or fn.async
|
|
233
255
|
const name = this.expect(T.IDENT).value;
|
|
@@ -300,7 +322,6 @@ export class Parser {
|
|
|
300
322
|
if (this.at(T.FN_ASYNC)) return this.parseFunction(true, true);
|
|
301
323
|
if (this.at(T.MODEL)) return this.parseModel(true);
|
|
302
324
|
|
|
303
|
-
// pub variable
|
|
304
325
|
if (TYPE_TOKENS.has(this.peek().type)) {
|
|
305
326
|
const node = this.parseTypedVariable();
|
|
306
327
|
node.isPublic = true;
|
|
@@ -310,24 +331,25 @@ export class Parser {
|
|
|
310
331
|
throw this.error('Expected fn, model, or type after pub');
|
|
311
332
|
}
|
|
312
333
|
|
|
313
|
-
// ret expression
|
|
314
334
|
parseReturn() {
|
|
315
335
|
this.advance(); // ret
|
|
316
336
|
if (this.at(T.NEWLINE) || this.at(T.EOF) || this.at(T.DEDENT)) {
|
|
317
337
|
return new ASTNode('Return', { value: null });
|
|
318
338
|
}
|
|
319
|
-
// ret.status 200 {...}
|
|
320
339
|
if (this.match(T.DOT)) {
|
|
321
340
|
const method = this.expect(T.IDENT).value;
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
341
|
+
if (method === 'status') {
|
|
342
|
+
const statusCode = this.parseExpression();
|
|
343
|
+
const body = this.parseExpression();
|
|
344
|
+
return new ASTNode('ReturnStatus', { method, statusCode, body });
|
|
345
|
+
}
|
|
346
|
+
const value = this.parseExpression();
|
|
347
|
+
return new ASTNode('ReturnMethod', { method, value });
|
|
325
348
|
}
|
|
326
349
|
const value = this.parseExpression();
|
|
327
350
|
return new ASTNode('Return', { value });
|
|
328
351
|
}
|
|
329
352
|
|
|
330
|
-
// str name = "hello"
|
|
331
353
|
parseTypedVariable() {
|
|
332
354
|
const varType = this.advance().value;
|
|
333
355
|
const name = this.expect(T.IDENT).value;
|
|
@@ -336,7 +358,6 @@ export class Parser {
|
|
|
336
358
|
return new ASTNode('TypedVar', { varType, name, value, isMut: false, isPublic: false });
|
|
337
359
|
}
|
|
338
360
|
|
|
339
|
-
// mut int counter = 0
|
|
340
361
|
parseMutVariable() {
|
|
341
362
|
this.advance(); // mut
|
|
342
363
|
if (TYPE_TOKENS.has(this.peek().type)) {
|
|
@@ -344,19 +365,12 @@ export class Parser {
|
|
|
344
365
|
node.isMut = true;
|
|
345
366
|
return node;
|
|
346
367
|
}
|
|
347
|
-
// mut name = value (no type)
|
|
348
368
|
const name = this.expect(T.IDENT).value;
|
|
349
369
|
this.expect(T.ASSIGN);
|
|
350
370
|
const value = this.parseExpression();
|
|
351
371
|
return new ASTNode('TypedVar', { varType: null, name, value, isMut: true });
|
|
352
372
|
}
|
|
353
373
|
|
|
354
|
-
// if condition:
|
|
355
|
-
// body
|
|
356
|
-
// elif condition:
|
|
357
|
-
// body
|
|
358
|
-
// else:
|
|
359
|
-
// body
|
|
360
374
|
parseIf() {
|
|
361
375
|
this.advance(); // if
|
|
362
376
|
const condition = this.parseExpression();
|
|
@@ -384,15 +398,12 @@ export class Parser {
|
|
|
384
398
|
return new ASTNode('If', { condition, body, elifs, elseBody });
|
|
385
399
|
}
|
|
386
400
|
|
|
387
|
-
// each item in collection:
|
|
388
|
-
// body
|
|
389
401
|
parseEach() {
|
|
390
402
|
this.advance(); // each
|
|
391
403
|
let key = null;
|
|
392
404
|
const valueName = this.expect(T.IDENT).value;
|
|
393
405
|
if (this.match(T.COMMA)) {
|
|
394
406
|
key = valueName;
|
|
395
|
-
// The next ident is actually the value
|
|
396
407
|
const val = this.expect(T.IDENT).value;
|
|
397
408
|
this.expect(T.IN);
|
|
398
409
|
const collection = this.parseExpression();
|
|
@@ -407,8 +418,6 @@ export class Parser {
|
|
|
407
418
|
return new ASTNode('Each', { key: null, value: valueName, collection, body });
|
|
408
419
|
}
|
|
409
420
|
|
|
410
|
-
// for i in 0..10:
|
|
411
|
-
// body
|
|
412
421
|
parseFor() {
|
|
413
422
|
this.advance(); // for
|
|
414
423
|
const varName = this.expect(T.IDENT).value;
|
|
@@ -421,8 +430,6 @@ export class Parser {
|
|
|
421
430
|
return new ASTNode('For', { varName, start, end, body });
|
|
422
431
|
}
|
|
423
432
|
|
|
424
|
-
// while condition:
|
|
425
|
-
// body
|
|
426
433
|
parseWhile() {
|
|
427
434
|
this.advance(); // while
|
|
428
435
|
const condition = this.parseExpression();
|
|
@@ -431,9 +438,6 @@ export class Parser {
|
|
|
431
438
|
return new ASTNode('While', { condition, body });
|
|
432
439
|
}
|
|
433
440
|
|
|
434
|
-
// match value:
|
|
435
|
-
// pattern: body
|
|
436
|
-
// _: default
|
|
437
441
|
parseMatch() {
|
|
438
442
|
this.advance(); // match
|
|
439
443
|
const value = this.parseExpression();
|
|
@@ -471,10 +475,6 @@ export class Parser {
|
|
|
471
475
|
return new ASTNode('Match', { value, cases });
|
|
472
476
|
}
|
|
473
477
|
|
|
474
|
-
// try:
|
|
475
|
-
// body
|
|
476
|
-
// fail e:
|
|
477
|
-
// handler
|
|
478
478
|
parseTry() {
|
|
479
479
|
this.advance(); // try
|
|
480
480
|
this.expect(T.COLON);
|
|
@@ -495,14 +495,10 @@ export class Parser {
|
|
|
495
495
|
return new ASTNode('Try', { body, catchVar, catchBody });
|
|
496
496
|
}
|
|
497
497
|
|
|
498
|
-
// server app port 3000:
|
|
499
|
-
// get "/path" -> type:
|
|
500
|
-
// body
|
|
501
498
|
parseServer() {
|
|
502
499
|
this.advance(); // server
|
|
503
500
|
const name = this.expect(T.IDENT).value;
|
|
504
501
|
|
|
505
|
-
let portKw = null;
|
|
506
502
|
let port = null;
|
|
507
503
|
if (this.at(T.IDENT) && this.peek().value === 'port') {
|
|
508
504
|
this.advance();
|
|
@@ -530,6 +526,17 @@ export class Parser {
|
|
|
530
526
|
routes.push(this.parseCors());
|
|
531
527
|
} else if (this.at(T.LIMIT)) {
|
|
532
528
|
routes.push(this.parseLimit());
|
|
529
|
+
} else if (this.at(T.STATIC)) {
|
|
530
|
+
routes.push(this.parseStatic());
|
|
531
|
+
} else if (this.at(T.WS)) {
|
|
532
|
+
routes.push(this.parseWs());
|
|
533
|
+
} else if (this.at(T.GROUP)) {
|
|
534
|
+
routes.push(this.parseGroup());
|
|
535
|
+
} else if (this.at(T.COOKIE)) {
|
|
536
|
+
this.advance();
|
|
537
|
+
routes.push(new ASTNode('CookieDecl'));
|
|
538
|
+
} else if (this.at(T.IDENT) && this.peek().value === 'error') {
|
|
539
|
+
routes.push(this.parseErrorHandler());
|
|
533
540
|
} else {
|
|
534
541
|
routes.push(this.parseStatement());
|
|
535
542
|
}
|
|
@@ -579,11 +586,6 @@ export class Parser {
|
|
|
579
586
|
return new ASTNode('Middleware', { name, params, body });
|
|
580
587
|
}
|
|
581
588
|
|
|
582
|
-
// model User:
|
|
583
|
-
// str name
|
|
584
|
-
// int age
|
|
585
|
-
// fn greet() -> str:
|
|
586
|
-
// ret "hi"
|
|
587
589
|
parseModel(isPublic = false) {
|
|
588
590
|
this.advance(); // model
|
|
589
591
|
const name = this.expect(T.IDENT).value;
|
|
@@ -615,7 +617,6 @@ export class Parser {
|
|
|
615
617
|
}
|
|
616
618
|
fields.push({ name: fieldName, type: fieldType, defaultValue });
|
|
617
619
|
} else {
|
|
618
|
-
// skip unknown
|
|
619
620
|
this.advance();
|
|
620
621
|
}
|
|
621
622
|
this.skipNewlines();
|
|
@@ -625,8 +626,6 @@ export class Parser {
|
|
|
625
626
|
return new ASTNode('Model', { name, parent, fields, methods, isPublic });
|
|
626
627
|
}
|
|
627
628
|
|
|
628
|
-
// on event:
|
|
629
|
-
// body
|
|
630
629
|
parseOn() {
|
|
631
630
|
this.advance(); // on
|
|
632
631
|
const event = this.parseExpression();
|
|
@@ -635,8 +634,6 @@ export class Parser {
|
|
|
635
634
|
return new ASTNode('On', { event, body });
|
|
636
635
|
}
|
|
637
636
|
|
|
638
|
-
// log "message"
|
|
639
|
-
// log.error "message"
|
|
640
637
|
parseLog() {
|
|
641
638
|
this.advance(); // log
|
|
642
639
|
let level = 'log';
|
|
@@ -664,17 +661,20 @@ export class Parser {
|
|
|
664
661
|
const connectionString = this.parseExpression();
|
|
665
662
|
return new ASTNode('DbConnect', { connectionString });
|
|
666
663
|
}
|
|
667
|
-
|
|
668
|
-
this.pos -= 3; // rewind to parse as expression
|
|
664
|
+
this.pos -= 3;
|
|
669
665
|
return this.parseExpressionStatement();
|
|
670
666
|
}
|
|
671
667
|
|
|
668
|
+
parseDbDir() {
|
|
669
|
+
this.advance(); // db
|
|
670
|
+
const path = this.parseString();
|
|
671
|
+
return new ASTNode('DbDir', { path });
|
|
672
|
+
}
|
|
673
|
+
|
|
672
674
|
parseAwaitStatement() {
|
|
673
|
-
// could be await expression or standalone await call
|
|
674
675
|
return this.parseExpressionStatement();
|
|
675
676
|
}
|
|
676
677
|
|
|
677
|
-
// [a, b] = await.all [expr1, expr2]
|
|
678
678
|
parseAwaitAll() {
|
|
679
679
|
this.advance(); // await.all
|
|
680
680
|
const exprs = [];
|
|
@@ -696,7 +696,6 @@ export class Parser {
|
|
|
696
696
|
parseExpressionStatement() {
|
|
697
697
|
const expr = this.parseExpression();
|
|
698
698
|
|
|
699
|
-
// Check for assignment: expr = value
|
|
700
699
|
if (this.match(T.ASSIGN)) {
|
|
701
700
|
const value = this.parseExpression();
|
|
702
701
|
return new ASTNode('Assignment', { target: expr, value });
|
|
@@ -713,7 +712,6 @@ export class Parser {
|
|
|
713
712
|
return new ASTNode('ExprStatement', { expression: expr });
|
|
714
713
|
}
|
|
715
714
|
|
|
716
|
-
// Expression parsing with precedence climbing
|
|
717
715
|
parseExpression() {
|
|
718
716
|
return this.parsePipe();
|
|
719
717
|
}
|
|
@@ -730,27 +728,35 @@ export class Parser {
|
|
|
730
728
|
|
|
731
729
|
matchPipeAcrossLines() {
|
|
732
730
|
if (this.match(T.PIPE)) return true;
|
|
733
|
-
// Lookahead across newlines for pipe (INDENT/DEDENT already preprocessed)
|
|
734
731
|
let scanPos = this.pos;
|
|
735
732
|
while (scanPos < this.tokens.length && this.tokens[scanPos].type === T.NEWLINE) {
|
|
736
733
|
scanPos++;
|
|
737
734
|
}
|
|
738
735
|
if (scanPos < this.tokens.length && this.tokens[scanPos].type === T.PIPE) {
|
|
739
736
|
while (this.at(T.NEWLINE)) this.advance();
|
|
740
|
-
this.advance();
|
|
737
|
+
this.advance();
|
|
741
738
|
return true;
|
|
742
739
|
}
|
|
743
740
|
return false;
|
|
744
741
|
}
|
|
745
742
|
|
|
746
743
|
parseTernary() {
|
|
744
|
+
if (this.at(T.IF)) {
|
|
745
|
+
const savedPos = this.pos;
|
|
746
|
+
this.advance();
|
|
747
|
+
const condition = this.parseNullish();
|
|
748
|
+
if (this.match(T.THEN)) {
|
|
749
|
+
const consequent = this.parseNullish();
|
|
750
|
+
this.expect(T.ELSE);
|
|
751
|
+
const alternate = this.parseNullish();
|
|
752
|
+
return new ASTNode('Ternary', { condition, consequent, alternate });
|
|
753
|
+
}
|
|
754
|
+
this.pos = savedPos;
|
|
755
|
+
}
|
|
747
756
|
let expr = this.parseNullish();
|
|
748
|
-
// inline if: value = if cond then a else b
|
|
749
757
|
if (this.at(T.IF)) {
|
|
750
|
-
// Only treat as ternary if we're in an expression context
|
|
751
|
-
// Lookahead: if ... then ... else
|
|
752
758
|
const savedPos = this.pos;
|
|
753
|
-
this.advance();
|
|
759
|
+
this.advance();
|
|
754
760
|
const condition = this.parseNullish();
|
|
755
761
|
if (this.match(T.THEN)) {
|
|
756
762
|
const consequent = this.parseNullish();
|
|
@@ -758,7 +764,6 @@ export class Parser {
|
|
|
758
764
|
const alternate = this.parseNullish();
|
|
759
765
|
return new ASTNode('Ternary', { condition, consequent, alternate });
|
|
760
766
|
}
|
|
761
|
-
// Not a ternary, restore
|
|
762
767
|
this.pos = savedPos;
|
|
763
768
|
}
|
|
764
769
|
return expr;
|
|
@@ -922,12 +927,13 @@ export class Parser {
|
|
|
922
927
|
case T.TYPE_STR: case T.TYPE_INT: case T.TYPE_NUM:
|
|
923
928
|
case T.TYPE_BOOL: case T.TYPE_LIST: case T.TYPE_MAP:
|
|
924
929
|
case T.TYPE_ANY: case T.TYPE_JSON: case T.TYPE_VOID:
|
|
925
|
-
// In expression context, type keywords act as identifiers (e.g., arr.map(), JSON.parse())
|
|
926
930
|
this.advance();
|
|
927
931
|
return new ASTNode('Identifier', { name: tok.value });
|
|
928
932
|
|
|
929
933
|
case T.SCHEMA: case T.CRUD: case T.AUTH: case T.CORS:
|
|
930
934
|
case T.LIMIT: case T.ENV: case T.EVERY: case T.WATCH:
|
|
935
|
+
case T.STATIC: case T.WS: case T.GROUP: case T.COOKIE:
|
|
936
|
+
case T.FROM: case T.AS: case T.IN:
|
|
931
937
|
this.advance();
|
|
932
938
|
return new ASTNode('Identifier', { name: tok.value });
|
|
933
939
|
|
|
@@ -1002,11 +1008,9 @@ export class Parser {
|
|
|
1002
1008
|
}
|
|
1003
1009
|
|
|
1004
1010
|
parseGroupOrArrow() {
|
|
1005
|
-
// Check if this is an arrow function: (params) => body
|
|
1006
1011
|
const savedPos = this.pos;
|
|
1007
1012
|
this.advance(); // (
|
|
1008
1013
|
|
|
1009
|
-
// Try to parse as arrow function params
|
|
1010
1014
|
let isArrow = false;
|
|
1011
1015
|
let depth = 1;
|
|
1012
1016
|
let scanPos = this.pos;
|
|
@@ -1036,7 +1040,6 @@ export class Parser {
|
|
|
1036
1040
|
return new ASTNode('ArrowFn', { params, body });
|
|
1037
1041
|
}
|
|
1038
1042
|
|
|
1039
|
-
// Regular grouping
|
|
1040
1043
|
this.pos = savedPos;
|
|
1041
1044
|
this.advance(); // (
|
|
1042
1045
|
const expr = this.parseExpression();
|
|
@@ -1114,10 +1117,8 @@ export class Parser {
|
|
|
1114
1117
|
return new ASTNode('Lambda', { params, returnType, body, isAsync });
|
|
1115
1118
|
}
|
|
1116
1119
|
|
|
1117
|
-
//
|
|
1118
|
-
|
|
1119
|
-
// name str required min(2) max(50)
|
|
1120
|
-
// email str required email unique
|
|
1120
|
+
// ===== High-level feature parsers =====
|
|
1121
|
+
|
|
1121
1122
|
parseSchema() {
|
|
1122
1123
|
this.advance(); // schema
|
|
1123
1124
|
const name = this.expect(T.IDENT).value;
|
|
@@ -1184,7 +1185,6 @@ export class Parser {
|
|
|
1184
1185
|
return { name, type: fieldType, enumValues, modifiers };
|
|
1185
1186
|
}
|
|
1186
1187
|
|
|
1187
|
-
// crud "/api/users" User
|
|
1188
1188
|
parseCrud() {
|
|
1189
1189
|
this.advance(); // crud
|
|
1190
1190
|
const path = this.parseString();
|
|
@@ -1192,9 +1192,6 @@ export class Parser {
|
|
|
1192
1192
|
return new ASTNode('CrudDecl', { path, schemaName });
|
|
1193
1193
|
}
|
|
1194
1194
|
|
|
1195
|
-
// auth SECRET:
|
|
1196
|
-
// protect "/api/*"
|
|
1197
|
-
// public "/api/auth/*"
|
|
1198
1195
|
parseAuth() {
|
|
1199
1196
|
this.advance(); // auth
|
|
1200
1197
|
const secret = this.parseExpression();
|
|
@@ -1224,15 +1221,12 @@ export class Parser {
|
|
|
1224
1221
|
return new ASTNode('AuthDecl', { secret, protectedPaths, publicPaths });
|
|
1225
1222
|
}
|
|
1226
1223
|
|
|
1227
|
-
// cors "*"
|
|
1228
|
-
// cors ["origin1", "origin2"]
|
|
1229
1224
|
parseCors() {
|
|
1230
1225
|
this.advance(); // cors
|
|
1231
1226
|
const origins = this.parseExpression();
|
|
1232
1227
|
return new ASTNode('CorsDecl', { origins });
|
|
1233
1228
|
}
|
|
1234
1229
|
|
|
1235
|
-
// limit "/api/*" 100 "1m"
|
|
1236
1230
|
parseLimit() {
|
|
1237
1231
|
this.advance(); // limit
|
|
1238
1232
|
const path = this.parseString();
|
|
@@ -1241,9 +1235,6 @@ export class Parser {
|
|
|
1241
1235
|
return new ASTNode('LimitDecl', { path, max, window });
|
|
1242
1236
|
}
|
|
1243
1237
|
|
|
1244
|
-
// env:
|
|
1245
|
-
// PORT int default(3000)
|
|
1246
|
-
// JWT_SECRET str required
|
|
1247
1238
|
parseEnv() {
|
|
1248
1239
|
this.advance(); // env
|
|
1249
1240
|
this.expect(T.COLON);
|
|
@@ -1296,8 +1287,6 @@ export class Parser {
|
|
|
1296
1287
|
return { name, type: fieldType, modifiers };
|
|
1297
1288
|
}
|
|
1298
1289
|
|
|
1299
|
-
// every "5m":
|
|
1300
|
-
// log "tick"
|
|
1301
1290
|
parseEvery() {
|
|
1302
1291
|
this.advance(); // every
|
|
1303
1292
|
const interval = this.parseExpression();
|
|
@@ -1306,8 +1295,6 @@ export class Parser {
|
|
|
1306
1295
|
return new ASTNode('EveryDecl', { interval, body });
|
|
1307
1296
|
}
|
|
1308
1297
|
|
|
1309
|
-
// watch User.create (event):
|
|
1310
|
-
// log event
|
|
1311
1298
|
parseWatch() {
|
|
1312
1299
|
this.advance(); // watch
|
|
1313
1300
|
let eventName = this.expect(T.IDENT).value;
|
|
@@ -1326,4 +1313,98 @@ export class Parser {
|
|
|
1326
1313
|
const body = this.parseBlock();
|
|
1327
1314
|
return new ASTNode('WatchDecl', { eventName, params, body });
|
|
1328
1315
|
}
|
|
1316
|
+
|
|
1317
|
+
// static "/public"
|
|
1318
|
+
parseStatic() {
|
|
1319
|
+
this.advance(); // static
|
|
1320
|
+
const path = this.parseString();
|
|
1321
|
+
return new ASTNode('StaticDecl', { path });
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// ws "/chat":
|
|
1325
|
+
// on "message" (data):
|
|
1326
|
+
// broadcast(data)
|
|
1327
|
+
// on "connect":
|
|
1328
|
+
// send({type: "welcome"})
|
|
1329
|
+
parseWs() {
|
|
1330
|
+
this.advance(); // ws
|
|
1331
|
+
const path = this.parseString();
|
|
1332
|
+
this.expect(T.COLON);
|
|
1333
|
+
this.skipNewlines();
|
|
1334
|
+
this.expect(T.INDENT);
|
|
1335
|
+
|
|
1336
|
+
const events = [];
|
|
1337
|
+
this.skipNewlines();
|
|
1338
|
+
|
|
1339
|
+
while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
|
|
1340
|
+
if (this.at(T.ON)) {
|
|
1341
|
+
this.advance(); // on
|
|
1342
|
+
const eventName = this.parseString();
|
|
1343
|
+
let params = [];
|
|
1344
|
+
if (this.match(T.LPAREN)) {
|
|
1345
|
+
while (!this.at(T.RPAREN) && !this.at(T.EOF)) {
|
|
1346
|
+
params.push(this.expect(T.IDENT).value);
|
|
1347
|
+
this.match(T.COMMA);
|
|
1348
|
+
}
|
|
1349
|
+
this.expect(T.RPAREN);
|
|
1350
|
+
}
|
|
1351
|
+
this.expect(T.COLON);
|
|
1352
|
+
const body = this.parseBlock();
|
|
1353
|
+
events.push({ name: eventName, params, body });
|
|
1354
|
+
} else {
|
|
1355
|
+
this.advance();
|
|
1356
|
+
}
|
|
1357
|
+
this.skipNewlines();
|
|
1358
|
+
}
|
|
1359
|
+
this.match(T.DEDENT);
|
|
1360
|
+
|
|
1361
|
+
return new ASTNode('WsDecl', { path, events });
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
parseGroup() {
|
|
1365
|
+
this.advance(); // group
|
|
1366
|
+
const prefix = this.parseString();
|
|
1367
|
+
this.expect(T.COLON);
|
|
1368
|
+
this.skipNewlines();
|
|
1369
|
+
this.expect(T.INDENT);
|
|
1370
|
+
|
|
1371
|
+
const routes = [];
|
|
1372
|
+
this.skipNewlines();
|
|
1373
|
+
|
|
1374
|
+
while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
|
|
1375
|
+
if (this.atAny(T.GET, T.POST, T.PUT, T.DEL)) {
|
|
1376
|
+
routes.push(this.parseRoute());
|
|
1377
|
+
} else if (this.at(T.MID)) {
|
|
1378
|
+
routes.push(this.parseMiddleware());
|
|
1379
|
+
} else if (this.at(T.CRUD)) {
|
|
1380
|
+
routes.push(this.parseCrud());
|
|
1381
|
+
} else if (this.at(T.AUTH)) {
|
|
1382
|
+
routes.push(this.parseAuth());
|
|
1383
|
+
} else if (this.at(T.GROUP)) {
|
|
1384
|
+
routes.push(this.parseGroup());
|
|
1385
|
+
} else {
|
|
1386
|
+
routes.push(this.parseStatement());
|
|
1387
|
+
}
|
|
1388
|
+
this.skipNewlines();
|
|
1389
|
+
}
|
|
1390
|
+
this.match(T.DEDENT);
|
|
1391
|
+
|
|
1392
|
+
return new ASTNode('GroupDecl', { prefix, routes });
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
parseErrorHandler() {
|
|
1396
|
+
this.advance(); // error (ident)
|
|
1397
|
+
let params = ['err', 'req', 'res'];
|
|
1398
|
+
if (this.match(T.LPAREN)) {
|
|
1399
|
+
params = [];
|
|
1400
|
+
while (!this.at(T.RPAREN) && !this.at(T.EOF)) {
|
|
1401
|
+
params.push(this.expect(T.IDENT).value);
|
|
1402
|
+
this.match(T.COMMA);
|
|
1403
|
+
}
|
|
1404
|
+
this.expect(T.RPAREN);
|
|
1405
|
+
}
|
|
1406
|
+
this.expect(T.COLON);
|
|
1407
|
+
const body = this.parseBlock();
|
|
1408
|
+
return new ASTNode('ErrorHandler', { params, body });
|
|
1409
|
+
}
|
|
1329
1410
|
}
|
package/src/preprocess.js
CHANGED
|
@@ -107,6 +107,14 @@ export function preprocess(source) {
|
|
|
107
107
|
const rest = line.slice(1).trim();
|
|
108
108
|
if (rest.startsWith('.s ') || rest.startsWith('.s(')) {
|
|
109
109
|
out = 'ret.status ' + transformContent(rest.slice(3));
|
|
110
|
+
} else if (rest.startsWith('.r ')) {
|
|
111
|
+
out = 'ret.redirect ' + transformContent(rest.slice(3));
|
|
112
|
+
} else if (rest.startsWith('.h ')) {
|
|
113
|
+
out = 'ret.html ' + transformContent(rest.slice(3));
|
|
114
|
+
} else if (rest.startsWith('.t ')) {
|
|
115
|
+
out = 'ret.text ' + transformContent(rest.slice(3));
|
|
116
|
+
} else if (rest.startsWith('.f ')) {
|
|
117
|
+
out = 'ret.file ' + transformContent(rest.slice(3));
|
|
110
118
|
} else {
|
|
111
119
|
out = rest ? 'ret ' + transformContent(rest) : 'ret';
|
|
112
120
|
}
|