naidejs 1.2.0 → 1.4.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 +382 -229
- package/SPEC-X.nx +23 -1
- package/SPEC.naide +56 -0
- package/bin/naide.js +99 -2
- package/package.json +1 -1
- package/src/generator.js +199 -0
- package/src/index.js +26 -8
- package/src/lexer.js +17 -4
- package/src/parser.js +216 -24
- package/src/preprocess.js +3 -0
- package/src/runtime.js +258 -0
- package/src/tokens.js +24 -0
package/SPEC-X.nx
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
-- Rule: first char of line = intent
|
|
7
7
|
-- > return ? if | elif : else
|
|
8
8
|
-- @ loop * while ! try !! catch
|
|
9
|
-
-- $ server ^ model < import % match
|
|
9
|
+
-- $ server ^ model < import % match X patch
|
|
10
10
|
-- f func ~f async + export
|
|
11
11
|
-- s:i:n:b:l:m:a: typed vars
|
|
12
12
|
-- ~ await ~~ await.all
|
|
@@ -138,6 +138,12 @@ $app:PORT
|
|
|
138
138
|
static "/public"
|
|
139
139
|
crud "/api/users" User
|
|
140
140
|
cookie
|
|
141
|
+
session JWT_SECRET
|
|
142
|
+
cache "/api/*" "5m"
|
|
143
|
+
view "./views"
|
|
144
|
+
sse "/events"
|
|
145
|
+
upload "/api/upload" "avatar" (req,res):
|
|
146
|
+
>{file:req.file.filename}
|
|
141
147
|
group "/api/v1":
|
|
142
148
|
G"/status"
|
|
143
149
|
>{version:"1.0"}
|
|
@@ -185,6 +191,22 @@ $app:PORT
|
|
|
185
191
|
-- sign({id:1}) → JWT (uses auth secret)
|
|
186
192
|
-- sign({id:1},"secret") → JWT (explicit)
|
|
187
193
|
|
|
194
|
+
-- Validate: auto-validate POST/PUT/PATCH bodies
|
|
195
|
+
-- validate "/api/users" User
|
|
196
|
+
|
|
197
|
+
-- Test (node:test)
|
|
198
|
+
test "math":
|
|
199
|
+
assert 1+1==2
|
|
200
|
+
assert 2*3==6
|
|
201
|
+
|
|
202
|
+
-- Queue (async job queue)
|
|
203
|
+
queue jobs:
|
|
204
|
+
job "notify" (data):
|
|
205
|
+
log data.msg
|
|
206
|
+
|
|
207
|
+
-- OpenAPI: auto-generate spec from schemas
|
|
208
|
+
-- openapi "/docs"
|
|
209
|
+
|
|
188
210
|
-- Scheduled tasks
|
|
189
211
|
every "5m":
|
|
190
212
|
log"cleanup"
|
package/SPEC.naide
CHANGED
|
@@ -183,6 +183,26 @@ server app port PORT:
|
|
|
183
183
|
# クッキーパーサー
|
|
184
184
|
cookie
|
|
185
185
|
|
|
186
|
+
# セッション管理 (cookie-based, ゼロ依存)
|
|
187
|
+
session JWT_SECRET
|
|
188
|
+
|
|
189
|
+
# レスポンスキャッシュ
|
|
190
|
+
cache "/api/*" "5m"
|
|
191
|
+
|
|
192
|
+
# テンプレートエンジン
|
|
193
|
+
view "./views"
|
|
194
|
+
|
|
195
|
+
# Server-Sent Events
|
|
196
|
+
sse "/events"
|
|
197
|
+
|
|
198
|
+
# ファイルアップロード (ゼロ依存)
|
|
199
|
+
upload "/api/upload" "avatar" (req, res):
|
|
200
|
+
ret {file: req.file.filename, size: req.file.size}
|
|
201
|
+
|
|
202
|
+
# 名前付きミドルウェア適用
|
|
203
|
+
# mid myMiddleware
|
|
204
|
+
# mid myMiddleware "/api"
|
|
205
|
+
|
|
186
206
|
# ルートグループ
|
|
187
207
|
group "/api/v1":
|
|
188
208
|
get "/status":
|
|
@@ -230,6 +250,41 @@ server app port PORT:
|
|
|
230
250
|
# any raw = await api.raw(url, opts)
|
|
231
251
|
|
|
232
252
|
|
|
253
|
+
# ---- HTTPメソッド ----
|
|
254
|
+
# get, post, put, del, patch がサーバー内で使える
|
|
255
|
+
# patch "/api/users/:id" (req, res):
|
|
256
|
+
# ret {updated: true}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# ---- リクエストバリデーション ----
|
|
260
|
+
# validate パス スキーマ名 → POST/PUT/PATCHのボディをスキーマで自動検証
|
|
261
|
+
# server app port 3000:
|
|
262
|
+
# validate "/api/users" User
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ---- テスト (node:test ベース) ----
|
|
266
|
+
test "math operations":
|
|
267
|
+
assert 1 + 1 == 2
|
|
268
|
+
assert 10 - 3 == 7
|
|
269
|
+
|
|
270
|
+
test "string operations":
|
|
271
|
+
str name = "NAIDE"
|
|
272
|
+
assert name == "NAIDE"
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# ---- ジョブキュー (インメモリ非同期) ----
|
|
276
|
+
queue jobs:
|
|
277
|
+
job "sendEmail" (data):
|
|
278
|
+
log "sending to {data.to}"
|
|
279
|
+
job "resize" (data):
|
|
280
|
+
log "resizing {data.path}"
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# ---- OpenAPI 自動生成 ----
|
|
284
|
+
# server app port 3000:
|
|
285
|
+
# openapi "/docs" # スキーマからOpenAPI仕様を自動生成して配信
|
|
286
|
+
|
|
287
|
+
|
|
233
288
|
# ---- レスポンスヘルパー ----
|
|
234
289
|
# ret {data} → JSON応答
|
|
235
290
|
# ret.status 404 {error} → ステータスコード付き
|
|
@@ -237,6 +292,7 @@ server app port PORT:
|
|
|
237
292
|
# ret.html "<h1>Hi</h1>" → HTML応答
|
|
238
293
|
# ret.text "pong" → テキスト応答
|
|
239
294
|
# ret.file "/path/to/file" → ファイル送信
|
|
295
|
+
# ret.render "template" {data} → テンプレート描画
|
|
240
296
|
|
|
241
297
|
|
|
242
298
|
# ---- 組み込み関数 (自動インポート) ----
|
package/bin/naide.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { readFileSync, writeFileSync, unlinkSync, watch as fsWatch } from 'fs';
|
|
4
|
-
import { resolve, basename, extname } from 'path';
|
|
3
|
+
import { readFileSync, writeFileSync, unlinkSync, watch as fsWatch, existsSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
4
|
+
import { resolve, basename, extname, join, relative } from 'path';
|
|
5
5
|
import { compile } from '../src/index.js';
|
|
6
6
|
import { spawn } from 'child_process';
|
|
7
7
|
|
|
@@ -36,6 +36,101 @@ for (let i = 0; i < args.length; i++) {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
if (files[0] === 'init') {
|
|
40
|
+
const dir = files[1] ? resolve(files[1]) : process.cwd();
|
|
41
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
42
|
+
const name = basename(dir === process.cwd() ? dir : dir);
|
|
43
|
+
|
|
44
|
+
if (!existsSync(resolve(dir, 'package.json'))) {
|
|
45
|
+
writeFileSync(resolve(dir, 'package.json'), JSON.stringify({
|
|
46
|
+
name, version: '1.0.0', type: 'module',
|
|
47
|
+
scripts: { start: 'naide app.naide', dev: 'naide -w app.naide', build: 'naide --emit app.naide -o dist/app.mjs' },
|
|
48
|
+
dependencies: { naidejs: '^1.3.0' }
|
|
49
|
+
}, null, 2) + '\n');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!existsSync(resolve(dir, 'app.naide'))) {
|
|
53
|
+
writeFileSync(resolve(dir, 'app.naide'), `db "data/"
|
|
54
|
+
|
|
55
|
+
env:
|
|
56
|
+
PORT int default(3000)
|
|
57
|
+
|
|
58
|
+
schema Item:
|
|
59
|
+
id auto
|
|
60
|
+
name str required min(1) max(100)
|
|
61
|
+
done bool default(false)
|
|
62
|
+
|
|
63
|
+
server app port PORT:
|
|
64
|
+
cors "*"
|
|
65
|
+
cookie
|
|
66
|
+
static "/public"
|
|
67
|
+
crud "/api/items" Item
|
|
68
|
+
|
|
69
|
+
get "/":
|
|
70
|
+
ret.text "NAIDE server running"
|
|
71
|
+
|
|
72
|
+
get "/api/health":
|
|
73
|
+
ret {status: "ok", items: ItemStore.count()}
|
|
74
|
+
`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!existsSync(resolve(dir, 'public'))) mkdirSync(resolve(dir, 'public'), { recursive: true });
|
|
78
|
+
|
|
79
|
+
console.log(`\n NAIDE project initialized!
|
|
80
|
+
|
|
81
|
+
${dir === process.cwd() ? '' : ` cd ${basename(dir)}\n`} npm install
|
|
82
|
+
npm run dev
|
|
83
|
+
`);
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (files[0] === 'build') {
|
|
88
|
+
const dir = resolve(files[1] || '.');
|
|
89
|
+
const outDir = files[2] ? resolve(files[2]) : null;
|
|
90
|
+
|
|
91
|
+
function walkDir(d) {
|
|
92
|
+
const found = [];
|
|
93
|
+
for (const entry of readdirSync(d)) {
|
|
94
|
+
const full = join(d, entry);
|
|
95
|
+
if (statSync(full).isDirectory()) {
|
|
96
|
+
if (entry === 'node_modules' || entry === '.git' || entry === 'dist') continue;
|
|
97
|
+
found.push(...walkDir(full));
|
|
98
|
+
} else if (entry.endsWith('.naide') || entry.endsWith('.nx')) {
|
|
99
|
+
found.push(full);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return found;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const sourceFiles = walkDir(dir);
|
|
106
|
+
if (sourceFiles.length === 0) {
|
|
107
|
+
console.log(' No .naide or .nx files found.');
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log(`\n NAIDE build — ${sourceFiles.length} file(s)\n`);
|
|
112
|
+
let errors = 0;
|
|
113
|
+
for (const srcFile of sourceFiles) {
|
|
114
|
+
const rel = relative(dir, srcFile);
|
|
115
|
+
const mode = srcFile.endsWith('.nx') ? 'x' : 'naide';
|
|
116
|
+
try {
|
|
117
|
+
const source = readFileSync(srcFile, 'utf-8');
|
|
118
|
+
const { js } = compile(source, { mode });
|
|
119
|
+
const outName = rel.replace(/\.(naide|nx)$/, '.mjs');
|
|
120
|
+
const outPath = outDir ? join(outDir, outName) : join(dir, outName);
|
|
121
|
+
const outDirPath = resolve(outPath, '..');
|
|
122
|
+
if (!existsSync(outDirPath)) mkdirSync(outDirPath, { recursive: true });
|
|
123
|
+
writeFileSync(outPath, js);
|
|
124
|
+
console.log(` ${rel} → ${outDir ? join(relative('.', outDir), outName) : outName}`);
|
|
125
|
+
} catch (e) {
|
|
126
|
+
console.error(` FAIL ${rel}: ${e.message.split('\n')[0]}`);
|
|
127
|
+
errors++;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
console.log(`\n Done. ${sourceFiles.length - errors} compiled, ${errors} failed.`);
|
|
131
|
+
process.exit(errors > 0 ? 1 : 0);
|
|
132
|
+
}
|
|
133
|
+
|
|
39
134
|
if (flags.help || files.length === 0) {
|
|
40
135
|
console.log(`
|
|
41
136
|
NAIDE - Node AI Development Environment
|
|
@@ -44,6 +139,8 @@ if (flags.help || files.length === 0) {
|
|
|
44
139
|
Usage:
|
|
45
140
|
naide <file.naide> Run a NAIDE file
|
|
46
141
|
naide <file.nx> Run a NAIDE-X file (auto-detected)
|
|
142
|
+
naide init [dir] Create a new NAIDE project
|
|
143
|
+
naide build [dir] [outdir] Transpile all files to JavaScript
|
|
47
144
|
naide --emit <file.nx> Output generated JavaScript
|
|
48
145
|
naide --mid <file.nx> Output intermediate NAIDE v1 (debug)
|
|
49
146
|
naide -x <file.naide> Force NAIDE-X mode
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "naidejs",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "NAIDE - Node AI Development Environment. AI-specialized language with built-in server, auth, DB, WebSocket, and more — transpiles to Node.js. Standard mode (~40% fewer tokens) and X mode (~80% fewer tokens).",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"exports": {
|
package/src/generator.js
CHANGED
|
@@ -12,6 +12,8 @@ export class Generator {
|
|
|
12
12
|
this.authSecret = null;
|
|
13
13
|
this.hasWs = false;
|
|
14
14
|
this.wsNodes = [];
|
|
15
|
+
this.hasTests = false;
|
|
16
|
+
this.hasAsserts = false;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
generate(ast) {
|
|
@@ -29,6 +31,14 @@ export class Generator {
|
|
|
29
31
|
preamble.push('');
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
if (this.hasTests) {
|
|
35
|
+
preamble.push("import { test } from 'node:test';");
|
|
36
|
+
}
|
|
37
|
+
if (this.hasTests || this.hasAsserts) {
|
|
38
|
+
preamble.push("import assert from 'node:assert/strict';");
|
|
39
|
+
if (this.hasTests) preamble.push('');
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
if (this.needsEventBus) {
|
|
33
43
|
preamble.push('const __eventBus = createEventBus();');
|
|
34
44
|
preamble.push('');
|
|
@@ -96,6 +106,18 @@ export class Generator {
|
|
|
96
106
|
case 'GroupDecl': return this.visitGroupTopLevel(node);
|
|
97
107
|
case 'ErrorHandler': return this.visitErrorHandlerTopLevel(node);
|
|
98
108
|
case 'CookieDecl': return this.visitCookieTopLevel(node);
|
|
109
|
+
case 'UploadDecl': return this.visitUploadTopLevel(node);
|
|
110
|
+
case 'SessionDecl': return this.visitSessionTopLevel(node);
|
|
111
|
+
case 'ViewDecl': return this.visitViewTopLevel(node);
|
|
112
|
+
case 'SseDecl': return this.visitSseTopLevel(node);
|
|
113
|
+
case 'CacheDecl': return this.visitCacheTopLevel(node);
|
|
114
|
+
case 'MiddlewareRef': return this.visitMiddlewareRefTopLevel(node);
|
|
115
|
+
case 'ReturnRender': return this.visitReturnRender(node);
|
|
116
|
+
case 'ValidateDecl': return this.visitValidateTopLevel(node);
|
|
117
|
+
case 'TestDecl': return this.visitTest(node);
|
|
118
|
+
case 'AssertStmt': return this.visitAssert(node);
|
|
119
|
+
case 'QueueDecl': return this.visitQueue(node);
|
|
120
|
+
case 'OpenapiDecl': return this.visitOpenapiTopLevel(node);
|
|
99
121
|
default:
|
|
100
122
|
this.emit(`/* unknown: ${node.type} */`);
|
|
101
123
|
}
|
|
@@ -324,6 +346,24 @@ export class Generator {
|
|
|
324
346
|
errorHandlers.push(child);
|
|
325
347
|
} else if (child.type === 'CookieDecl') {
|
|
326
348
|
this.visitCookie(node.name, child);
|
|
349
|
+
} else if (child.type === 'UploadDecl') {
|
|
350
|
+
this.visitUpload(node.name, child);
|
|
351
|
+
} else if (child.type === 'SessionDecl') {
|
|
352
|
+
this.visitSession(node.name, child);
|
|
353
|
+
} else if (child.type === 'ViewDecl') {
|
|
354
|
+
this.visitView(node.name, child);
|
|
355
|
+
} else if (child.type === 'SseDecl') {
|
|
356
|
+
this.visitSse(node.name, child);
|
|
357
|
+
} else if (child.type === 'CacheDecl') {
|
|
358
|
+
this.visitCache(node.name, child);
|
|
359
|
+
} else if (child.type === 'MiddlewareRef') {
|
|
360
|
+
this.visitMiddlewareRef(node.name, child);
|
|
361
|
+
} else if (child.type === 'ValidateDecl') {
|
|
362
|
+
this.visitValidate(node.name, child);
|
|
363
|
+
} else if (child.type === 'OpenapiDecl') {
|
|
364
|
+
this.visitOpenapi(node.name, child);
|
|
365
|
+
} else if (child.type === 'QueueDecl') {
|
|
366
|
+
this.visitQueue(child);
|
|
327
367
|
} else {
|
|
328
368
|
this.visitStatement(child);
|
|
329
369
|
}
|
|
@@ -759,6 +799,165 @@ export class Generator {
|
|
|
759
799
|
this.visitCookie('app', node);
|
|
760
800
|
}
|
|
761
801
|
|
|
802
|
+
visitUpload(appName, node) {
|
|
803
|
+
this.runtimeImports.add('uploadMiddleware');
|
|
804
|
+
const path = this.stringValue(node.path);
|
|
805
|
+
const field = this.stringValue(node.fieldName);
|
|
806
|
+
const params = node.params.length > 0 ? node.params.join(', ') : 'req, res';
|
|
807
|
+
const needsAsync = this.bodyUsesAwait(node.body);
|
|
808
|
+
const asyncPrefix = needsAsync ? 'async ' : '';
|
|
809
|
+
this.emit(`${appName}.post(${path}, uploadMiddleware(${field}), ${asyncPrefix}(${params}) => {`);
|
|
810
|
+
this.indent++;
|
|
811
|
+
for (const stmt of node.body) {
|
|
812
|
+
if (stmt.type === 'Return' && stmt.value !== null) {
|
|
813
|
+
this.emit(`res.json(${this.expr(stmt.value)});`);
|
|
814
|
+
} else {
|
|
815
|
+
this.visitStatement(stmt);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
this.indent--;
|
|
819
|
+
this.emit('});');
|
|
820
|
+
this.emitRaw('');
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
visitUploadTopLevel(node) {
|
|
824
|
+
this.visitUpload('app', node);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
visitSession(appName, node) {
|
|
828
|
+
this.runtimeImports.add('sessionMiddleware');
|
|
829
|
+
const secret = this.expr(node.secret);
|
|
830
|
+
this.emit(`${appName}.use(sessionMiddleware(${secret}));`);
|
|
831
|
+
this.emitRaw('');
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
visitSessionTopLevel(node) {
|
|
835
|
+
this.visitSession('app', node);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
visitView(appName, node) {
|
|
839
|
+
this.runtimeImports.add('createRenderer');
|
|
840
|
+
const dir = this.stringValue(node.dir);
|
|
841
|
+
this.emit(`const __render = createRenderer(${dir});`);
|
|
842
|
+
this.emitRaw('');
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
visitViewTopLevel(node) {
|
|
846
|
+
this.visitView('app', node);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
visitSse(appName, node) {
|
|
850
|
+
this.runtimeImports.add('createSseManager');
|
|
851
|
+
const path = this.stringValue(node.path);
|
|
852
|
+
this.emit(`const sse = createSseManager();`);
|
|
853
|
+
this.emit(`${appName}.get(${path}, sse.handler());`);
|
|
854
|
+
this.emitRaw('');
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
visitSseTopLevel(node) {
|
|
858
|
+
this.visitSse('app', node);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
visitCache(appName, node) {
|
|
862
|
+
this.runtimeImports.add('cacheMiddleware');
|
|
863
|
+
const path = this.stringValue(node.path);
|
|
864
|
+
const duration = this.expr(node.duration);
|
|
865
|
+
this.emit(`${appName}.use(${path}, cacheMiddleware(${duration}));`);
|
|
866
|
+
this.emitRaw('');
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
visitCacheTopLevel(node) {
|
|
870
|
+
this.visitCache('app', node);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
visitMiddlewareRef(appName, node) {
|
|
874
|
+
if (node.path) {
|
|
875
|
+
this.emit(`${appName}.use(${this.stringValue(node.path)}, ${node.name});`);
|
|
876
|
+
} else {
|
|
877
|
+
this.emit(`${appName}.use(${node.name});`);
|
|
878
|
+
}
|
|
879
|
+
this.emitRaw('');
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
visitMiddlewareRefTopLevel(node) {
|
|
883
|
+
this.visitMiddlewareRef('app', node);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
visitReturnRender(node) {
|
|
887
|
+
const template = this.expr(node.template);
|
|
888
|
+
const data = node.data ? this.expr(node.data) : '{}';
|
|
889
|
+
this.emit(`return res.type('html').send(__render(${template}, ${data}));`);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
visitValidate(appName, node) {
|
|
893
|
+
this.runtimeImports.add('validateMiddleware');
|
|
894
|
+
const path = this.stringValue(node.path);
|
|
895
|
+
this.emit(`${appName}.use(${path}, validateMiddleware(${node.schemaName}Schema));`);
|
|
896
|
+
this.emitRaw('');
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
visitValidateTopLevel(node) {
|
|
900
|
+
this.visitValidate('app', node);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
visitTest(node) {
|
|
904
|
+
this.hasTests = true;
|
|
905
|
+
const name = this.expr(node.name);
|
|
906
|
+
const needsAsync = this.bodyUsesAwait(node.body);
|
|
907
|
+
const asyncPrefix = needsAsync ? 'async ' : '';
|
|
908
|
+
this.emit(`test(${name}, ${asyncPrefix}() => {`);
|
|
909
|
+
this.indent++;
|
|
910
|
+
for (const stmt of node.body) this.visitStatement(stmt);
|
|
911
|
+
this.indent--;
|
|
912
|
+
this.emit('});');
|
|
913
|
+
this.emitRaw('');
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
visitAssert(node) {
|
|
917
|
+
this.hasAsserts = true;
|
|
918
|
+
const exprNode = node.expr;
|
|
919
|
+
if (exprNode.type === 'Binary' && exprNode.op === '===') {
|
|
920
|
+
this.emit(`assert.strictEqual(${this.expr(exprNode.left)}, ${this.expr(exprNode.right)});`);
|
|
921
|
+
} else if (exprNode.type === 'Binary' && exprNode.op === '!==') {
|
|
922
|
+
this.emit(`assert.notStrictEqual(${this.expr(exprNode.left)}, ${this.expr(exprNode.right)});`);
|
|
923
|
+
} else {
|
|
924
|
+
this.emit(`assert.ok(${this.expr(exprNode)});`);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
visitQueue(node) {
|
|
929
|
+
this.runtimeImports.add('createQueue');
|
|
930
|
+
this.emit(`const ${node.name} = createQueue();`);
|
|
931
|
+
for (const job of node.jobs) {
|
|
932
|
+
const params = job.params.length > 0 ? job.params.join(', ') : 'data';
|
|
933
|
+
const needsAsync = this.bodyUsesAwait(job.body);
|
|
934
|
+
const asyncPrefix = needsAsync ? 'async ' : '';
|
|
935
|
+
this.emit(`${node.name}.register(${this.generateString(job.name)}, ${asyncPrefix}(${params}) => {`);
|
|
936
|
+
this.indent++;
|
|
937
|
+
for (const stmt of job.body) this.visitStatement(stmt);
|
|
938
|
+
this.indent--;
|
|
939
|
+
this.emit('});');
|
|
940
|
+
}
|
|
941
|
+
this.emitRaw('');
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
visitOpenapi(appName, node) {
|
|
945
|
+
this.runtimeImports.add('buildOpenApiSpec');
|
|
946
|
+
const path = this.stringValue(node.path);
|
|
947
|
+
const schemaNames = [...this.schemas.keys()];
|
|
948
|
+
const schemaArgs = schemaNames.map(n => `${n}Schema`).join(', ');
|
|
949
|
+
this.emit(`${appName}.get(${path}, (req, res) => {`);
|
|
950
|
+
this.indent++;
|
|
951
|
+
this.emit(`res.json(buildOpenApiSpec([${schemaArgs}]));`);
|
|
952
|
+
this.indent--;
|
|
953
|
+
this.emit('});');
|
|
954
|
+
this.emitRaw('');
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
visitOpenapiTopLevel(node) {
|
|
958
|
+
this.visitOpenapi('app', node);
|
|
959
|
+
}
|
|
960
|
+
|
|
762
961
|
visitEnv(node) {
|
|
763
962
|
this.runtimeImports.add('loadEnv');
|
|
764
963
|
|
package/src/index.js
CHANGED
|
@@ -4,16 +4,34 @@ import { Generator } from './generator.js';
|
|
|
4
4
|
import { preprocess } from './preprocess.js';
|
|
5
5
|
|
|
6
6
|
export function compile(source, { mode = 'naide', runtimePath } = {}) {
|
|
7
|
+
let processedSource = source;
|
|
7
8
|
if (mode === 'x') {
|
|
8
|
-
|
|
9
|
+
processedSource = preprocess(source);
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const lexer = new Lexer(processedSource);
|
|
13
|
+
const tokens = lexer.tokenize();
|
|
14
|
+
const parser = new Parser(tokens);
|
|
15
|
+
const ast = parser.parse();
|
|
16
|
+
const generator = new Generator({ runtimePath });
|
|
17
|
+
const js = generator.generate(ast);
|
|
18
|
+
return { js, ast, tokens, naide: mode === 'x' ? processedSource : null };
|
|
19
|
+
} catch (e) {
|
|
20
|
+
const lineMatch = e.message.match(/line (\d+)/);
|
|
21
|
+
if (lineMatch) {
|
|
22
|
+
const lineNum = parseInt(lineMatch[1]);
|
|
23
|
+
const lines = processedSource.split('\n');
|
|
24
|
+
const start = Math.max(0, lineNum - 3);
|
|
25
|
+
const end = Math.min(lines.length, lineNum + 2);
|
|
26
|
+
const context = lines.slice(start, end).map((l, i) => {
|
|
27
|
+
const num = start + i + 1;
|
|
28
|
+
const marker = num === lineNum ? ' >> ' : ' ';
|
|
29
|
+
return `${marker}${num} | ${l}`;
|
|
30
|
+
}).join('\n');
|
|
31
|
+
e.message += `\n\n${context}\n`;
|
|
32
|
+
}
|
|
33
|
+
throw e;
|
|
9
34
|
}
|
|
10
|
-
const lexer = new Lexer(source);
|
|
11
|
-
const tokens = lexer.tokenize();
|
|
12
|
-
const parser = new Parser(tokens);
|
|
13
|
-
const ast = parser.parse();
|
|
14
|
-
const generator = new Generator({ runtimePath });
|
|
15
|
-
const js = generator.generate(ast);
|
|
16
|
-
return { js, ast, tokens, naide: mode === 'x' ? source : null };
|
|
17
35
|
}
|
|
18
36
|
|
|
19
37
|
export function transpile(source, opts) {
|
package/src/lexer.js
CHANGED
|
@@ -11,7 +11,7 @@ class Token {
|
|
|
11
11
|
|
|
12
12
|
export class Lexer {
|
|
13
13
|
constructor(source) {
|
|
14
|
-
this.source = source;
|
|
14
|
+
this.source = source.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
15
15
|
this.pos = 0;
|
|
16
16
|
this.line = 1;
|
|
17
17
|
this.col = 1;
|
|
@@ -78,6 +78,11 @@ export class Lexer {
|
|
|
78
78
|
continue;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
if (ch === ';') {
|
|
82
|
+
this.advance();
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
81
86
|
if (ch === '"' || ch === "'") {
|
|
82
87
|
this.readString(ch);
|
|
83
88
|
continue;
|
|
@@ -195,7 +200,11 @@ export class Lexer {
|
|
|
195
200
|
}
|
|
196
201
|
}
|
|
197
202
|
|
|
198
|
-
if (this.peek() === quote)
|
|
203
|
+
if (this.peek() === quote) {
|
|
204
|
+
this.advance();
|
|
205
|
+
} else {
|
|
206
|
+
throw new Error(`[NAIDE Lexer Error] Unterminated string starting at line ${startLine}:${startCol}`);
|
|
207
|
+
}
|
|
199
208
|
|
|
200
209
|
if (current) parts.push({ type: 'text', value: current });
|
|
201
210
|
|
|
@@ -221,7 +230,11 @@ export class Lexer {
|
|
|
221
230
|
value += this.advance();
|
|
222
231
|
}
|
|
223
232
|
}
|
|
224
|
-
if (this.peek() === '`')
|
|
233
|
+
if (this.peek() === '`') {
|
|
234
|
+
this.advance();
|
|
235
|
+
} else {
|
|
236
|
+
throw new Error(`[NAIDE Lexer Error] Unterminated template string starting at line ${startLine}:${startCol}`);
|
|
237
|
+
}
|
|
225
238
|
this.tokens.push(new Token(T.STRING, { parts: [{ type: 'text', value }], raw: value }, startLine, startCol));
|
|
226
239
|
}
|
|
227
240
|
|
|
@@ -371,7 +384,7 @@ export class Lexer {
|
|
|
371
384
|
case ':': this.tokens.push(new Token(T.COLON, ':', startLine, startCol)); break;
|
|
372
385
|
case ',': this.tokens.push(new Token(T.COMMA, ',', startLine, startCol)); break;
|
|
373
386
|
default:
|
|
374
|
-
throw new Error(`Unexpected character '${ch}' at line ${startLine}:${startCol}`);
|
|
387
|
+
throw new Error(`[NAIDE Lexer Error] Unexpected character '${ch}' at line ${startLine}:${startCol}`);
|
|
375
388
|
}
|
|
376
389
|
}
|
|
377
390
|
|