naidejs 1.1.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/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;
@@ -94,7 +90,9 @@ export class Parser {
94
90
  tok.type === T.FROM || tok.type === T.AS || tok.type === T.SELF ||
95
91
  tok.type === T.SCHEMA || tok.type === T.CRUD || tok.type === T.AUTH ||
96
92
  tok.type === T.CORS || tok.type === T.LIMIT || tok.type === T.ENV ||
97
- tok.type === T.EVERY || tok.type === T.WATCH) {
93
+ tok.type === T.EVERY || tok.type === T.WATCH ||
94
+ tok.type === T.STATIC || tok.type === T.WS ||
95
+ tok.type === T.GROUP || tok.type === T.COOKIE) {
98
96
  return tok.value;
99
97
  }
100
98
  throw this.error(`Expected property name but got ${tok.type} ('${tok.value}')`, tok);
@@ -148,7 +146,9 @@ export class Parser {
148
146
  case T.BREAK: this.advance(); return new ASTNode('Break');
149
147
  case T.CONTINUE: this.advance(); return new ASTNode('Continue');
150
148
  case T.MUT: return this.parseMutVariable();
151
- case T.DB: return this.parseDbStatement();
149
+ case T.DB:
150
+ if (this.peek(1).type === T.DOT) return this.parseDbStatement();
151
+ return this.parseDbDir();
152
152
  case T.AWAIT: return this.parseAwaitStatement();
153
153
  case T.AWAIT_ALL: return this.parseAwaitAll();
154
154
  case T.SCHEMA: return this.parseSchema();
@@ -173,6 +173,19 @@ export class Parser {
173
173
  case T.WATCH:
174
174
  if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
175
175
  return this.parseWatch();
176
+ case T.STATIC:
177
+ if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
178
+ return this.parseStatic();
179
+ case T.WS:
180
+ if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
181
+ return this.parseWs();
182
+ case T.GROUP:
183
+ if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
184
+ return this.parseGroup();
185
+ case T.COOKIE:
186
+ if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
187
+ this.advance();
188
+ return new ASTNode('CookieDecl');
176
189
  default:
177
190
  if (TYPE_TOKENS.has(tok.type)) {
178
191
  return this.parseTypedVariable();
@@ -181,14 +194,10 @@ export class Parser {
181
194
  }
182
195
  }
183
196
 
184
- // use express
185
- // use express from "express"
186
- // use {readFile, writeFile} from "fs/promises"
187
197
  parseUse() {
188
198
  this.advance(); // use
189
199
 
190
200
  if (this.at(T.LBRACE)) {
191
- // use {a, b} from "module"
192
201
  this.advance();
193
202
  const names = [];
194
203
  while (!this.at(T.RBRACE) && !this.at(T.EOF)) {
@@ -226,8 +235,6 @@ export class Parser {
226
235
  return tok.value;
227
236
  }
228
237
 
229
- // fn name(params) -> returnType:
230
- // body
231
238
  parseFunction(isAsync, isPublic) {
232
239
  this.advance(); // fn or fn.async
233
240
  const name = this.expect(T.IDENT).value;
@@ -300,7 +307,6 @@ export class Parser {
300
307
  if (this.at(T.FN_ASYNC)) return this.parseFunction(true, true);
301
308
  if (this.at(T.MODEL)) return this.parseModel(true);
302
309
 
303
- // pub variable
304
310
  if (TYPE_TOKENS.has(this.peek().type)) {
305
311
  const node = this.parseTypedVariable();
306
312
  node.isPublic = true;
@@ -310,24 +316,25 @@ export class Parser {
310
316
  throw this.error('Expected fn, model, or type after pub');
311
317
  }
312
318
 
313
- // ret expression
314
319
  parseReturn() {
315
320
  this.advance(); // ret
316
321
  if (this.at(T.NEWLINE) || this.at(T.EOF) || this.at(T.DEDENT)) {
317
322
  return new ASTNode('Return', { value: null });
318
323
  }
319
- // ret.status 200 {...}
320
324
  if (this.match(T.DOT)) {
321
325
  const method = this.expect(T.IDENT).value;
322
- const statusCode = this.parseExpression();
323
- const body = this.parseExpression();
324
- return new ASTNode('ReturnStatus', { method, statusCode, body });
326
+ if (method === 'status') {
327
+ const statusCode = this.parseExpression();
328
+ const body = this.parseExpression();
329
+ return new ASTNode('ReturnStatus', { method, statusCode, body });
330
+ }
331
+ const value = this.parseExpression();
332
+ return new ASTNode('ReturnMethod', { method, value });
325
333
  }
326
334
  const value = this.parseExpression();
327
335
  return new ASTNode('Return', { value });
328
336
  }
329
337
 
330
- // str name = "hello"
331
338
  parseTypedVariable() {
332
339
  const varType = this.advance().value;
333
340
  const name = this.expect(T.IDENT).value;
@@ -336,7 +343,6 @@ export class Parser {
336
343
  return new ASTNode('TypedVar', { varType, name, value, isMut: false, isPublic: false });
337
344
  }
338
345
 
339
- // mut int counter = 0
340
346
  parseMutVariable() {
341
347
  this.advance(); // mut
342
348
  if (TYPE_TOKENS.has(this.peek().type)) {
@@ -344,19 +350,12 @@ export class Parser {
344
350
  node.isMut = true;
345
351
  return node;
346
352
  }
347
- // mut name = value (no type)
348
353
  const name = this.expect(T.IDENT).value;
349
354
  this.expect(T.ASSIGN);
350
355
  const value = this.parseExpression();
351
356
  return new ASTNode('TypedVar', { varType: null, name, value, isMut: true });
352
357
  }
353
358
 
354
- // if condition:
355
- // body
356
- // elif condition:
357
- // body
358
- // else:
359
- // body
360
359
  parseIf() {
361
360
  this.advance(); // if
362
361
  const condition = this.parseExpression();
@@ -384,15 +383,12 @@ export class Parser {
384
383
  return new ASTNode('If', { condition, body, elifs, elseBody });
385
384
  }
386
385
 
387
- // each item in collection:
388
- // body
389
386
  parseEach() {
390
387
  this.advance(); // each
391
388
  let key = null;
392
389
  const valueName = this.expect(T.IDENT).value;
393
390
  if (this.match(T.COMMA)) {
394
391
  key = valueName;
395
- // The next ident is actually the value
396
392
  const val = this.expect(T.IDENT).value;
397
393
  this.expect(T.IN);
398
394
  const collection = this.parseExpression();
@@ -407,8 +403,6 @@ export class Parser {
407
403
  return new ASTNode('Each', { key: null, value: valueName, collection, body });
408
404
  }
409
405
 
410
- // for i in 0..10:
411
- // body
412
406
  parseFor() {
413
407
  this.advance(); // for
414
408
  const varName = this.expect(T.IDENT).value;
@@ -421,8 +415,6 @@ export class Parser {
421
415
  return new ASTNode('For', { varName, start, end, body });
422
416
  }
423
417
 
424
- // while condition:
425
- // body
426
418
  parseWhile() {
427
419
  this.advance(); // while
428
420
  const condition = this.parseExpression();
@@ -431,9 +423,6 @@ export class Parser {
431
423
  return new ASTNode('While', { condition, body });
432
424
  }
433
425
 
434
- // match value:
435
- // pattern: body
436
- // _: default
437
426
  parseMatch() {
438
427
  this.advance(); // match
439
428
  const value = this.parseExpression();
@@ -471,10 +460,6 @@ export class Parser {
471
460
  return new ASTNode('Match', { value, cases });
472
461
  }
473
462
 
474
- // try:
475
- // body
476
- // fail e:
477
- // handler
478
463
  parseTry() {
479
464
  this.advance(); // try
480
465
  this.expect(T.COLON);
@@ -495,14 +480,10 @@ export class Parser {
495
480
  return new ASTNode('Try', { body, catchVar, catchBody });
496
481
  }
497
482
 
498
- // server app port 3000:
499
- // get "/path" -> type:
500
- // body
501
483
  parseServer() {
502
484
  this.advance(); // server
503
485
  const name = this.expect(T.IDENT).value;
504
486
 
505
- let portKw = null;
506
487
  let port = null;
507
488
  if (this.at(T.IDENT) && this.peek().value === 'port') {
508
489
  this.advance();
@@ -530,6 +511,17 @@ export class Parser {
530
511
  routes.push(this.parseCors());
531
512
  } else if (this.at(T.LIMIT)) {
532
513
  routes.push(this.parseLimit());
514
+ } else if (this.at(T.STATIC)) {
515
+ routes.push(this.parseStatic());
516
+ } else if (this.at(T.WS)) {
517
+ routes.push(this.parseWs());
518
+ } else if (this.at(T.GROUP)) {
519
+ routes.push(this.parseGroup());
520
+ } else if (this.at(T.COOKIE)) {
521
+ this.advance();
522
+ routes.push(new ASTNode('CookieDecl'));
523
+ } else if (this.at(T.IDENT) && this.peek().value === 'error') {
524
+ routes.push(this.parseErrorHandler());
533
525
  } else {
534
526
  routes.push(this.parseStatement());
535
527
  }
@@ -579,11 +571,6 @@ export class Parser {
579
571
  return new ASTNode('Middleware', { name, params, body });
580
572
  }
581
573
 
582
- // model User:
583
- // str name
584
- // int age
585
- // fn greet() -> str:
586
- // ret "hi"
587
574
  parseModel(isPublic = false) {
588
575
  this.advance(); // model
589
576
  const name = this.expect(T.IDENT).value;
@@ -615,7 +602,6 @@ export class Parser {
615
602
  }
616
603
  fields.push({ name: fieldName, type: fieldType, defaultValue });
617
604
  } else {
618
- // skip unknown
619
605
  this.advance();
620
606
  }
621
607
  this.skipNewlines();
@@ -625,8 +611,6 @@ export class Parser {
625
611
  return new ASTNode('Model', { name, parent, fields, methods, isPublic });
626
612
  }
627
613
 
628
- // on event:
629
- // body
630
614
  parseOn() {
631
615
  this.advance(); // on
632
616
  const event = this.parseExpression();
@@ -635,8 +619,6 @@ export class Parser {
635
619
  return new ASTNode('On', { event, body });
636
620
  }
637
621
 
638
- // log "message"
639
- // log.error "message"
640
622
  parseLog() {
641
623
  this.advance(); // log
642
624
  let level = 'log';
@@ -664,17 +646,20 @@ export class Parser {
664
646
  const connectionString = this.parseExpression();
665
647
  return new ASTNode('DbConnect', { connectionString });
666
648
  }
667
- // db.query, db.find, etc -> treat as expression
668
- this.pos -= 3; // rewind to parse as expression
649
+ this.pos -= 3;
669
650
  return this.parseExpressionStatement();
670
651
  }
671
652
 
653
+ parseDbDir() {
654
+ this.advance(); // db
655
+ const path = this.parseString();
656
+ return new ASTNode('DbDir', { path });
657
+ }
658
+
672
659
  parseAwaitStatement() {
673
- // could be await expression or standalone await call
674
660
  return this.parseExpressionStatement();
675
661
  }
676
662
 
677
- // [a, b] = await.all [expr1, expr2]
678
663
  parseAwaitAll() {
679
664
  this.advance(); // await.all
680
665
  const exprs = [];
@@ -696,7 +681,6 @@ export class Parser {
696
681
  parseExpressionStatement() {
697
682
  const expr = this.parseExpression();
698
683
 
699
- // Check for assignment: expr = value
700
684
  if (this.match(T.ASSIGN)) {
701
685
  const value = this.parseExpression();
702
686
  return new ASTNode('Assignment', { target: expr, value });
@@ -713,7 +697,6 @@ export class Parser {
713
697
  return new ASTNode('ExprStatement', { expression: expr });
714
698
  }
715
699
 
716
- // Expression parsing with precedence climbing
717
700
  parseExpression() {
718
701
  return this.parsePipe();
719
702
  }
@@ -730,14 +713,13 @@ export class Parser {
730
713
 
731
714
  matchPipeAcrossLines() {
732
715
  if (this.match(T.PIPE)) return true;
733
- // Lookahead across newlines for pipe (INDENT/DEDENT already preprocessed)
734
716
  let scanPos = this.pos;
735
717
  while (scanPos < this.tokens.length && this.tokens[scanPos].type === T.NEWLINE) {
736
718
  scanPos++;
737
719
  }
738
720
  if (scanPos < this.tokens.length && this.tokens[scanPos].type === T.PIPE) {
739
721
  while (this.at(T.NEWLINE)) this.advance();
740
- this.advance(); // consume PIPE
722
+ this.advance();
741
723
  return true;
742
724
  }
743
725
  return false;
@@ -745,12 +727,9 @@ export class Parser {
745
727
 
746
728
  parseTernary() {
747
729
  let expr = this.parseNullish();
748
- // inline if: value = if cond then a else b
749
730
  if (this.at(T.IF)) {
750
- // Only treat as ternary if we're in an expression context
751
- // Lookahead: if ... then ... else
752
731
  const savedPos = this.pos;
753
- this.advance(); // if
732
+ this.advance();
754
733
  const condition = this.parseNullish();
755
734
  if (this.match(T.THEN)) {
756
735
  const consequent = this.parseNullish();
@@ -758,7 +737,6 @@ export class Parser {
758
737
  const alternate = this.parseNullish();
759
738
  return new ASTNode('Ternary', { condition, consequent, alternate });
760
739
  }
761
- // Not a ternary, restore
762
740
  this.pos = savedPos;
763
741
  }
764
742
  return expr;
@@ -922,12 +900,12 @@ export class Parser {
922
900
  case T.TYPE_STR: case T.TYPE_INT: case T.TYPE_NUM:
923
901
  case T.TYPE_BOOL: case T.TYPE_LIST: case T.TYPE_MAP:
924
902
  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
903
  this.advance();
927
904
  return new ASTNode('Identifier', { name: tok.value });
928
905
 
929
906
  case T.SCHEMA: case T.CRUD: case T.AUTH: case T.CORS:
930
907
  case T.LIMIT: case T.ENV: case T.EVERY: case T.WATCH:
908
+ case T.STATIC: case T.WS: case T.GROUP: case T.COOKIE:
931
909
  this.advance();
932
910
  return new ASTNode('Identifier', { name: tok.value });
933
911
 
@@ -1002,11 +980,9 @@ export class Parser {
1002
980
  }
1003
981
 
1004
982
  parseGroupOrArrow() {
1005
- // Check if this is an arrow function: (params) => body
1006
983
  const savedPos = this.pos;
1007
984
  this.advance(); // (
1008
985
 
1009
- // Try to parse as arrow function params
1010
986
  let isArrow = false;
1011
987
  let depth = 1;
1012
988
  let scanPos = this.pos;
@@ -1036,7 +1012,6 @@ export class Parser {
1036
1012
  return new ASTNode('ArrowFn', { params, body });
1037
1013
  }
1038
1014
 
1039
- // Regular grouping
1040
1015
  this.pos = savedPos;
1041
1016
  this.advance(); // (
1042
1017
  const expr = this.parseExpression();
@@ -1114,10 +1089,8 @@ export class Parser {
1114
1089
  return new ASTNode('Lambda', { params, returnType, body, isAsync });
1115
1090
  }
1116
1091
 
1117
- // schema User:
1118
- // id auto
1119
- // name str required min(2) max(50)
1120
- // email str required email unique
1092
+ // ===== High-level feature parsers =====
1093
+
1121
1094
  parseSchema() {
1122
1095
  this.advance(); // schema
1123
1096
  const name = this.expect(T.IDENT).value;
@@ -1184,7 +1157,6 @@ export class Parser {
1184
1157
  return { name, type: fieldType, enumValues, modifiers };
1185
1158
  }
1186
1159
 
1187
- // crud "/api/users" User
1188
1160
  parseCrud() {
1189
1161
  this.advance(); // crud
1190
1162
  const path = this.parseString();
@@ -1192,9 +1164,6 @@ export class Parser {
1192
1164
  return new ASTNode('CrudDecl', { path, schemaName });
1193
1165
  }
1194
1166
 
1195
- // auth SECRET:
1196
- // protect "/api/*"
1197
- // public "/api/auth/*"
1198
1167
  parseAuth() {
1199
1168
  this.advance(); // auth
1200
1169
  const secret = this.parseExpression();
@@ -1224,15 +1193,12 @@ export class Parser {
1224
1193
  return new ASTNode('AuthDecl', { secret, protectedPaths, publicPaths });
1225
1194
  }
1226
1195
 
1227
- // cors "*"
1228
- // cors ["origin1", "origin2"]
1229
1196
  parseCors() {
1230
1197
  this.advance(); // cors
1231
1198
  const origins = this.parseExpression();
1232
1199
  return new ASTNode('CorsDecl', { origins });
1233
1200
  }
1234
1201
 
1235
- // limit "/api/*" 100 "1m"
1236
1202
  parseLimit() {
1237
1203
  this.advance(); // limit
1238
1204
  const path = this.parseString();
@@ -1241,9 +1207,6 @@ export class Parser {
1241
1207
  return new ASTNode('LimitDecl', { path, max, window });
1242
1208
  }
1243
1209
 
1244
- // env:
1245
- // PORT int default(3000)
1246
- // JWT_SECRET str required
1247
1210
  parseEnv() {
1248
1211
  this.advance(); // env
1249
1212
  this.expect(T.COLON);
@@ -1296,8 +1259,6 @@ export class Parser {
1296
1259
  return { name, type: fieldType, modifiers };
1297
1260
  }
1298
1261
 
1299
- // every "5m":
1300
- // log "tick"
1301
1262
  parseEvery() {
1302
1263
  this.advance(); // every
1303
1264
  const interval = this.parseExpression();
@@ -1306,8 +1267,6 @@ export class Parser {
1306
1267
  return new ASTNode('EveryDecl', { interval, body });
1307
1268
  }
1308
1269
 
1309
- // watch User.create (event):
1310
- // log event
1311
1270
  parseWatch() {
1312
1271
  this.advance(); // watch
1313
1272
  let eventName = this.expect(T.IDENT).value;
@@ -1326,4 +1285,98 @@ export class Parser {
1326
1285
  const body = this.parseBlock();
1327
1286
  return new ASTNode('WatchDecl', { eventName, params, body });
1328
1287
  }
1288
+
1289
+ // static "/public"
1290
+ parseStatic() {
1291
+ this.advance(); // static
1292
+ const path = this.parseString();
1293
+ return new ASTNode('StaticDecl', { path });
1294
+ }
1295
+
1296
+ // ws "/chat":
1297
+ // on "message" (data):
1298
+ // broadcast(data)
1299
+ // on "connect":
1300
+ // send({type: "welcome"})
1301
+ parseWs() {
1302
+ this.advance(); // ws
1303
+ const path = this.parseString();
1304
+ this.expect(T.COLON);
1305
+ this.skipNewlines();
1306
+ this.expect(T.INDENT);
1307
+
1308
+ const events = [];
1309
+ this.skipNewlines();
1310
+
1311
+ while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
1312
+ if (this.at(T.ON)) {
1313
+ this.advance(); // on
1314
+ const eventName = this.parseString();
1315
+ let params = [];
1316
+ if (this.match(T.LPAREN)) {
1317
+ while (!this.at(T.RPAREN) && !this.at(T.EOF)) {
1318
+ params.push(this.expect(T.IDENT).value);
1319
+ this.match(T.COMMA);
1320
+ }
1321
+ this.expect(T.RPAREN);
1322
+ }
1323
+ this.expect(T.COLON);
1324
+ const body = this.parseBlock();
1325
+ events.push({ name: eventName, params, body });
1326
+ } else {
1327
+ this.advance();
1328
+ }
1329
+ this.skipNewlines();
1330
+ }
1331
+ this.match(T.DEDENT);
1332
+
1333
+ return new ASTNode('WsDecl', { path, events });
1334
+ }
1335
+
1336
+ parseGroup() {
1337
+ this.advance(); // group
1338
+ const prefix = this.parseString();
1339
+ this.expect(T.COLON);
1340
+ this.skipNewlines();
1341
+ this.expect(T.INDENT);
1342
+
1343
+ const routes = [];
1344
+ this.skipNewlines();
1345
+
1346
+ while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
1347
+ if (this.atAny(T.GET, T.POST, T.PUT, T.DEL)) {
1348
+ routes.push(this.parseRoute());
1349
+ } else if (this.at(T.MID)) {
1350
+ routes.push(this.parseMiddleware());
1351
+ } else if (this.at(T.CRUD)) {
1352
+ routes.push(this.parseCrud());
1353
+ } else if (this.at(T.AUTH)) {
1354
+ routes.push(this.parseAuth());
1355
+ } else if (this.at(T.GROUP)) {
1356
+ routes.push(this.parseGroup());
1357
+ } else {
1358
+ routes.push(this.parseStatement());
1359
+ }
1360
+ this.skipNewlines();
1361
+ }
1362
+ this.match(T.DEDENT);
1363
+
1364
+ return new ASTNode('GroupDecl', { prefix, routes });
1365
+ }
1366
+
1367
+ parseErrorHandler() {
1368
+ this.advance(); // error (ident)
1369
+ let params = ['err', 'req', 'res'];
1370
+ if (this.match(T.LPAREN)) {
1371
+ params = [];
1372
+ while (!this.at(T.RPAREN) && !this.at(T.EOF)) {
1373
+ params.push(this.expect(T.IDENT).value);
1374
+ this.match(T.COMMA);
1375
+ }
1376
+ this.expect(T.RPAREN);
1377
+ }
1378
+ this.expect(T.COLON);
1379
+ const body = this.parseBlock();
1380
+ return new ASTNode('ErrorHandler', { params, body });
1381
+ }
1329
1382
  }
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
  }