naider 1.19.0 → 1.20.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/bin/naide.js +218 -6
- package/mcp/server.js +28 -1
- package/package.json +1 -1
- package/src/gen.js +731 -0
- package/src/index.js +8 -1
- package/src/parser.js +1 -1
package/bin/naide.js
CHANGED
|
@@ -8,6 +8,42 @@ import { spawn } from 'child_process';
|
|
|
8
8
|
import { createRequire } from 'module';
|
|
9
9
|
const _require = createRequire(import.meta.url);
|
|
10
10
|
|
|
11
|
+
const NAIDE_VERSION = '1.20.0';
|
|
12
|
+
|
|
13
|
+
function crashReport(err, context = {}) {
|
|
14
|
+
const info = [
|
|
15
|
+
`NAIDE v${NAIDE_VERSION}`,
|
|
16
|
+
`Node ${process.version}`,
|
|
17
|
+
`${process.platform} ${process.arch}`,
|
|
18
|
+
];
|
|
19
|
+
if (context.file) info.push(`File: ${context.file}`);
|
|
20
|
+
if (context.target) info.push(`Target: ${context.target}`);
|
|
21
|
+
|
|
22
|
+
console.error(`\n ┌─ NAIDE Internal Error ───────────────────────────`);
|
|
23
|
+
console.error(` │ ${err.message}`);
|
|
24
|
+
console.error(` │`);
|
|
25
|
+
console.error(` │ ${info.join(' | ')}`);
|
|
26
|
+
if (err.stack) {
|
|
27
|
+
const frames = err.stack.split('\n').slice(1, 4).map(l => l.trim());
|
|
28
|
+
for (const f of frames) console.error(` │ ${f}`);
|
|
29
|
+
}
|
|
30
|
+
console.error(` │`);
|
|
31
|
+
console.error(` │ This is a compiler bug, not a syntax error.`);
|
|
32
|
+
console.error(` │ Report: https://github.com/irxk/naide/issues/new`);
|
|
33
|
+
console.error(` │ Include the .naide file and the info above.`);
|
|
34
|
+
console.error(` └──────────────────────────────────────────────────\n`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
process.on('uncaughtException', (err) => {
|
|
38
|
+
crashReport(err);
|
|
39
|
+
process.exit(99);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
process.on('unhandledRejection', (reason) => {
|
|
43
|
+
crashReport(reason instanceof Error ? reason : new Error(String(reason)));
|
|
44
|
+
process.exit(99);
|
|
45
|
+
});
|
|
46
|
+
|
|
11
47
|
function checkDependencies(jsCode) {
|
|
12
48
|
const depMap = {
|
|
13
49
|
"from 'express'": { pkg: 'express', reason: 'server' },
|
|
@@ -77,6 +113,7 @@ const flags = {
|
|
|
77
113
|
debug: false,
|
|
78
114
|
check: false,
|
|
79
115
|
target: 'node',
|
|
116
|
+
expand: false,
|
|
80
117
|
};
|
|
81
118
|
|
|
82
119
|
const files = [];
|
|
@@ -95,10 +132,108 @@ for (let i = 0; i < args.length; i++) {
|
|
|
95
132
|
case '--debug': case '-d': flags.debug = true; break;
|
|
96
133
|
case '--check': flags.check = true; flags.run = false; break;
|
|
97
134
|
case '--target': case '-t': flags.target = args[++i]; break;
|
|
135
|
+
case '--expand': flags.expand = true; flags.run = false; break;
|
|
98
136
|
default: files.push(arg);
|
|
99
137
|
}
|
|
100
138
|
}
|
|
101
139
|
|
|
140
|
+
// ── Code Generator (~~) ──
|
|
141
|
+
if (files[0] === 'gen' || files[0] === '~~') {
|
|
142
|
+
const instruction = files.slice(1).join(' ');
|
|
143
|
+
if (!instruction) {
|
|
144
|
+
console.log(`
|
|
145
|
+
NAIDE Code Generator (~~)
|
|
146
|
+
|
|
147
|
+
Usage:
|
|
148
|
+
naide ~~ "REST API for users with auth"
|
|
149
|
+
naide gen "todo app with database"
|
|
150
|
+
naide gen "Discord bot with hello command"
|
|
151
|
+
|
|
152
|
+
Generates valid NAIDE code from natural language instructions.
|
|
153
|
+
No AI, no network — pure pattern matching with self-healing validation.
|
|
154
|
+
|
|
155
|
+
Examples:
|
|
156
|
+
naide ~~ "REST API for products with name price stock"
|
|
157
|
+
naide ~~ "blog app with auth and database"
|
|
158
|
+
naide ~~ "Discord bot with hello and help commands"
|
|
159
|
+
naide ~~ "CLI tool"
|
|
160
|
+
naide ~~ "chat app with websocket"
|
|
161
|
+
naide ~~ "user management fullstack app"
|
|
162
|
+
naide ~~ "ユーザー管理APIを認証付きで"
|
|
163
|
+
naide ~~ "掲示板アプリ"
|
|
164
|
+
|
|
165
|
+
In .naide files:
|
|
166
|
+
~~ "REST API for users with auth"
|
|
167
|
+
# Expands at compile time to full NAIDE code
|
|
168
|
+
|
|
169
|
+
Debug:
|
|
170
|
+
naide --expand app.naide Show ~~ expansion results
|
|
171
|
+
|
|
172
|
+
API:
|
|
173
|
+
import { generate } from 'naider';
|
|
174
|
+
const result = generate("REST API for users");
|
|
175
|
+
console.log(result.code);
|
|
176
|
+
|
|
177
|
+
── Keyword Cheat Sheet ──────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
Intent server, api, rest, bot, cli, page, test, database,
|
|
180
|
+
ai, crud, auth, websocket, mail, graphql
|
|
181
|
+
Composite todo, blog, chat, shop, fullstack, board
|
|
182
|
+
Platform discord, slack, telegram, line
|
|
183
|
+
DB Type sqlite, postgres
|
|
184
|
+
JP Intent サーバー, 認証, ログイン, 会員, CRUD, 管理, データベース,
|
|
185
|
+
ボット, テスト, ページ, メール, リアルタイム
|
|
186
|
+
JP Entity ユーザー, 商品, 記事, タスク, 注文, コメント, イベント,
|
|
187
|
+
問い合わせ, 通知, カテゴリ, プロジェクト, 掲示板, 決済
|
|
188
|
+
Fields "with name email age" — auto-inferred types & validators
|
|
189
|
+
Field Alias e-mail→email, pwd→password, tel→phone, desc→description
|
|
190
|
+
|
|
191
|
+
────────────────────────────────────────────────────────────
|
|
192
|
+
`);
|
|
193
|
+
process.exit(0);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const { generate } = await import('../src/gen.js');
|
|
197
|
+
const result = generate(instruction);
|
|
198
|
+
|
|
199
|
+
if (flags.output) {
|
|
200
|
+
writeFileSync(flags.output, result.code, 'utf-8');
|
|
201
|
+
console.log(` Generated: ${flags.output} (${result.intents.join(' + ')})`);
|
|
202
|
+
} else {
|
|
203
|
+
console.log(result.code);
|
|
204
|
+
const meta = [`[${result.intents.join(' + ')}]`];
|
|
205
|
+
if (result.schema) meta.push(`→ ${result.schema}`);
|
|
206
|
+
meta.push(`valid: ${result.valid}`);
|
|
207
|
+
if (result.fixed) meta.push('(auto-fixed)');
|
|
208
|
+
process.stderr.write(`\n ── gen ~~ ${meta.join(' ')} ──\n`);
|
|
209
|
+
if (result.repairs.length > 0) {
|
|
210
|
+
process.stderr.write(' Repairs:\n');
|
|
211
|
+
for (const r of result.repairs) process.stderr.write(` - ${r}\n`);
|
|
212
|
+
}
|
|
213
|
+
if (result.suggestion) {
|
|
214
|
+
process.stderr.write(`\n${result.suggestion}\n`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
process.exit(0);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── Expand Debug ──
|
|
221
|
+
if (flags.expand && files.length > 0) {
|
|
222
|
+
const { expandDirectives } = await import('../src/gen.js');
|
|
223
|
+
for (const file of files) {
|
|
224
|
+
const filePath = resolve(file);
|
|
225
|
+
const source = readFileSync(filePath, 'utf-8');
|
|
226
|
+
const { source: expanded, expanded: didExpand } = expandDirectives(source);
|
|
227
|
+
if (didExpand) {
|
|
228
|
+
console.log(` ── ${file} (expanded) ──\n`);
|
|
229
|
+
console.log(expanded);
|
|
230
|
+
} else {
|
|
231
|
+
console.log(` ${file}: no ~~ directives found`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
process.exit(0);
|
|
235
|
+
}
|
|
236
|
+
|
|
102
237
|
// ── VS Code Extension Install ──
|
|
103
238
|
if (files[0] === 'vscode') {
|
|
104
239
|
const os = await import('os');
|
|
@@ -357,24 +492,67 @@ if (files[0] === 'build') {
|
|
|
357
492
|
|
|
358
493
|
console.log(`\n NAIDE build — ${sourceFiles.length} file(s)\n`);
|
|
359
494
|
let errors = 0;
|
|
495
|
+
const schemaMap = {};
|
|
496
|
+
const routeMap = {};
|
|
497
|
+
|
|
360
498
|
for (const srcFile of sourceFiles) {
|
|
361
499
|
const rel = relative(dir, srcFile);
|
|
362
500
|
const mode = srcFile.endsWith('.nx') ? 'x' : 'naide';
|
|
363
501
|
try {
|
|
364
502
|
const source = readFileSync(srcFile, 'utf-8');
|
|
365
|
-
const { js } = compile(source, { mode });
|
|
503
|
+
const { js, ast } = compile(source, { mode });
|
|
366
504
|
const outName = rel.replace(/\.(naide|nx)$/, '.mjs');
|
|
367
505
|
const outPath = outDir ? join(outDir, outName) : join(dir, outName);
|
|
368
506
|
const outDirPath = resolve(outPath, '..');
|
|
369
507
|
if (!existsSync(outDirPath)) mkdirSync(outDirPath, { recursive: true });
|
|
370
508
|
writeFileSync(outPath, js);
|
|
371
509
|
console.log(` ${rel} → ${outDir ? join(relative('.', outDir), outName) : outName}`);
|
|
510
|
+
|
|
511
|
+
if (ast && ast.body) {
|
|
512
|
+
for (const node of ast.body) {
|
|
513
|
+
if (node.type === 'SchemaDecl' && node.name) {
|
|
514
|
+
(schemaMap[node.name] = schemaMap[node.name] || []).push(rel);
|
|
515
|
+
}
|
|
516
|
+
if (node.type === 'Server' && node.routes) {
|
|
517
|
+
for (const s of node.routes) {
|
|
518
|
+
if (s.type === 'CrudDecl' && s.path) {
|
|
519
|
+
const key = `CRUD ${s.path.raw || s.path}`;
|
|
520
|
+
(routeMap[key] = routeMap[key] || []).push(rel);
|
|
521
|
+
}
|
|
522
|
+
if (s.type === 'Route' && s.path) {
|
|
523
|
+
const key = `${(s.method || 'get').toUpperCase()} ${s.path.raw || s.path}`;
|
|
524
|
+
(routeMap[key] = routeMap[key] || []).push(rel);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
372
530
|
} catch (e) {
|
|
373
|
-
|
|
531
|
+
const isSyntaxErr = e.message.includes('line ') || e.message.includes('Unexpected') || e.message.includes('Expected');
|
|
532
|
+
if (isSyntaxErr) {
|
|
533
|
+
console.error(` FAIL ${rel}: ${e.message.split('\n')[0]}`);
|
|
534
|
+
} else {
|
|
535
|
+
crashReport(e, { file: rel });
|
|
536
|
+
}
|
|
374
537
|
errors++;
|
|
375
538
|
}
|
|
376
539
|
}
|
|
377
|
-
|
|
540
|
+
|
|
541
|
+
let warnings = 0;
|
|
542
|
+
for (const [name, files] of Object.entries(schemaMap)) {
|
|
543
|
+
if (files.length > 1) {
|
|
544
|
+
console.error(` WARN: schema "${name}" defined in ${files.length} files: ${files.join(', ')}`);
|
|
545
|
+
warnings++;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
for (const [route, files] of Object.entries(routeMap)) {
|
|
549
|
+
if (files.length > 1) {
|
|
550
|
+
console.error(` WARN: route ${route} defined in ${files.length} files: ${files.join(', ')}`);
|
|
551
|
+
warnings++;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
console.log(`\n Done. ${sourceFiles.length - errors} compiled, ${errors} failed${warnings > 0 ? `, ${warnings} warning(s)` : ''}.`);
|
|
378
556
|
process.exit(errors > 0 ? 1 : 0);
|
|
379
557
|
}
|
|
380
558
|
|
|
@@ -664,6 +842,8 @@ if (flags.help) {
|
|
|
664
842
|
naide Start interactive REPL
|
|
665
843
|
naide <file.naide> Run a NAIDE file
|
|
666
844
|
naide <file.nx> Run a NAIDE-X file (auto-detected)
|
|
845
|
+
naide ~~ "instruction" Generate NAIDE code from instruction (no AI)
|
|
846
|
+
naide gen "instruction" Same as ~~ (alias)
|
|
667
847
|
naide init [dir] Create a new NAIDE project
|
|
668
848
|
naide build [dir] [outdir] Transpile all files to JavaScript
|
|
669
849
|
naide check <files...> Type-check without running
|
|
@@ -683,10 +863,28 @@ if (flags.help) {
|
|
|
683
863
|
naide -w <file.naide> Watch mode (auto-restart on changes)
|
|
684
864
|
naide -d <file> Debug mode (Node.js inspector)
|
|
685
865
|
|
|
686
|
-
|
|
866
|
+
Code Generation (~~):
|
|
867
|
+
naide ~~ "instruction" Generate code from instruction (no AI)
|
|
868
|
+
naide gen "instruction" Alias for ~~
|
|
869
|
+
naide --expand <file> Show ~~ directive expansion results
|
|
870
|
+
In .naide files: ~~ "instruction" expands at compile time
|
|
871
|
+
|
|
872
|
+
Targets (15):
|
|
687
873
|
node Node.js / JavaScript (default)
|
|
688
874
|
bun Bun-optimized JavaScript
|
|
689
|
-
|
|
875
|
+
ts TypeScript
|
|
876
|
+
python Python (Flask)
|
|
877
|
+
go Go (net/http)
|
|
878
|
+
java Java (HttpServer)
|
|
879
|
+
rust Rust (actix-web)
|
|
880
|
+
cpp C++ (cpp-httplib)
|
|
881
|
+
c C (libmicrohttpd)
|
|
882
|
+
csharp C# (ASP.NET)
|
|
883
|
+
kotlin Kotlin (Ktor)
|
|
884
|
+
swift Swift (Vapor)
|
|
885
|
+
dart Dart (shelf)
|
|
886
|
+
php PHP
|
|
887
|
+
ruby Ruby (Sinatra)
|
|
690
888
|
|
|
691
889
|
Modes:
|
|
692
890
|
.naide Standard NAIDE (~40% fewer tokens than JS)
|
|
@@ -701,9 +899,18 @@ if (flags.help) {
|
|
|
701
899
|
-t, --target Compile target: node (default), bun, python/py
|
|
702
900
|
--check Type-check files without running
|
|
703
901
|
--mid Show intermediate NAIDE v1 (X mode only)
|
|
902
|
+
--expand Show ~~ directive expansion (debug)
|
|
704
903
|
--ast Print AST
|
|
705
904
|
--tokens Print tokens
|
|
706
905
|
-h, --help Show this help
|
|
906
|
+
|
|
907
|
+
~~ Keyword Cheat Sheet:
|
|
908
|
+
Intent server api rest bot cli page test database ai crud auth ws mail graphql
|
|
909
|
+
Composite todo blog chat shop fullstack board
|
|
910
|
+
Platform discord slack telegram line
|
|
911
|
+
JP Intent サーバー 認証 ログイン 会員 CRUD 管理 データベース ボット テスト ページ
|
|
912
|
+
JP Entity ユーザー 商品 記事 タスク 注文 コメント イベント 問い合わせ 掲示板 決済
|
|
913
|
+
Fields "with name email age done" → auto-inferred types
|
|
707
914
|
`);
|
|
708
915
|
process.exit(0);
|
|
709
916
|
}
|
|
@@ -872,7 +1079,12 @@ for (const file of files) {
|
|
|
872
1079
|
} catch {}
|
|
873
1080
|
}
|
|
874
1081
|
} catch (err) {
|
|
875
|
-
|
|
1082
|
+
const isSyntaxErr = err.message.includes('line ') || err.message.includes('Unexpected') || err.message.includes('Expected');
|
|
1083
|
+
if (isSyntaxErr) {
|
|
1084
|
+
console.error(`\n${err.message}`);
|
|
1085
|
+
} else {
|
|
1086
|
+
crashReport(err, { file, target: flags.target });
|
|
1087
|
+
}
|
|
876
1088
|
if (process.env.NAIDE_DEBUG) console.error(err.stack);
|
|
877
1089
|
process.exit(1);
|
|
878
1090
|
}
|
package/mcp/server.js
CHANGED
|
@@ -63,6 +63,19 @@ const TOOLS = [
|
|
|
63
63
|
description: 'List all available NAIDE compilation targets with details.',
|
|
64
64
|
inputSchema: { type: 'object', properties: {} }
|
|
65
65
|
},
|
|
66
|
+
{
|
|
67
|
+
name: 'naide_generate',
|
|
68
|
+
description: 'Generate NAIDE code from a natural language instruction. No AI — uses pattern matching and template composition with self-healing parser validation. Extremely lightweight, runs in microseconds.',
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: {
|
|
72
|
+
instruction: { type: 'string', description: 'Natural language instruction (e.g. "REST API for users with auth", "Discord bot", "todo app")' },
|
|
73
|
+
port: { type: 'number', description: 'Server port (default: 3000)' },
|
|
74
|
+
schemaName: { type: 'string', description: 'Override schema/entity name' }
|
|
75
|
+
},
|
|
76
|
+
required: ['instruction']
|
|
77
|
+
}
|
|
78
|
+
},
|
|
66
79
|
{
|
|
67
80
|
name: 'naide_spec',
|
|
68
81
|
description: 'Get the NAIDE language specification. Use this to understand NAIDE syntax before writing code.',
|
|
@@ -133,6 +146,20 @@ async function handleToolCall(name, args) {
|
|
|
133
146
|
return { content: [{ type: 'text', text: `NAIDE Compilation Targets (15):\n\n${'Target'.padEnd(12)} ${'Language'.padEnd(25)} ${'Server'.padEnd(20)} Flag\n${'─'.repeat(75)}\n${text}` }] };
|
|
134
147
|
}
|
|
135
148
|
|
|
149
|
+
case 'naide_generate': {
|
|
150
|
+
try {
|
|
151
|
+
const { generate } = await import(resolve(__dirname, '..', 'src', 'gen.js'));
|
|
152
|
+
const opts = {};
|
|
153
|
+
if (args.port) opts.port = args.port;
|
|
154
|
+
if (args.schemaName) opts.schemaName = args.schemaName;
|
|
155
|
+
const result = generate(args.instruction, opts);
|
|
156
|
+
const meta = `[${result.intents.join(' + ')}]${result.schema ? ` → ${result.schema}` : ''} | valid: ${result.valid}${result.fixed ? ' (auto-fixed)' : ''}`;
|
|
157
|
+
return { content: [{ type: 'text', text: `${result.code}\n# ${meta}` }] };
|
|
158
|
+
} catch (e) {
|
|
159
|
+
return { content: [{ type: 'text', text: `Generation error: ${e.message}` }], isError: true };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
136
163
|
case 'naide_spec': {
|
|
137
164
|
try {
|
|
138
165
|
let spec = readFileSync(resolve(__dirname, '..', 'SPEC.naide'), 'utf-8');
|
|
@@ -165,7 +192,7 @@ async function handleMessage(msg) {
|
|
|
165
192
|
respond(msg.id, {
|
|
166
193
|
protocolVersion: '2024-11-05',
|
|
167
194
|
capabilities: { tools: {} },
|
|
168
|
-
serverInfo: { name: 'naide-mcp', version: '1.
|
|
195
|
+
serverInfo: { name: 'naide-mcp', version: '1.20.0' }
|
|
169
196
|
});
|
|
170
197
|
break;
|
|
171
198
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "naider",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"description": "NAIDE - Simpler than Python, compiles to 15 targets. AI-specialized language with 35+ built-in functions, syntax sugar (unless/until/repeat/swap/is/isnt), and 47 features. Targets: Node.js, Python, TypeScript, C, C++, Java, Go, Rust, PHP, Ruby, Kotlin, Swift, Dart, C#, Bun.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"exports": {
|
package/src/gen.js
ADDED
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
import { Lexer } from './lexer.js';
|
|
2
|
+
import { Parser } from './parser.js';
|
|
3
|
+
|
|
4
|
+
// ── Vocabulary ──────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
const INTENT_WORDS = {
|
|
7
|
+
server: ['server', 'api', 'rest', 'restful', 'endpoint', 'web', 'http', 'https', 'backend', 'app', 'application', 'service', 'microservice'],
|
|
8
|
+
schema: ['schema', 'model', 'entity', 'struct', 'table', 'type', 'data', 'record', 'resource', 'object', 'definition'],
|
|
9
|
+
bot: ['bot', 'chatbot', 'robot'],
|
|
10
|
+
cli: ['cli', 'command', 'terminal', 'console', 'argv', 'args'],
|
|
11
|
+
page: ['page', 'html', 'website', 'site', 'frontend', 'ui', 'view', 'template', 'render', 'layout'],
|
|
12
|
+
test: ['test', 'testing', 'spec', 'unit', 'e2e', 'integration', 'assert', 'expect'],
|
|
13
|
+
database: ['database', 'db', 'storage', 'persist', 'store', 'repository', 'sqlite', 'postgres', 'postgresql', 'json-db', 'mongo'],
|
|
14
|
+
ai: ['ai', 'llm', 'gpt', 'claude', 'openai', 'prompt', 'embedding', 'rag'],
|
|
15
|
+
crud: ['crud', 'create', 'read', 'update', 'delete', 'listing', 'list', 'index'],
|
|
16
|
+
auth: ['auth', 'jwt', 'login', 'authentication', 'authorization', 'signup', 'signin', 'sign-in', 'sign-up',
|
|
17
|
+
'password', 'token', 'session', 'protect', 'permission', 'role', 'rbac', 'oauth', 'register', 'member', 'membership', 'account'],
|
|
18
|
+
cors: ['cors', 'cross-origin'],
|
|
19
|
+
websocket: ['websocket', 'ws', 'realtime', 'real-time', 'socket', 'live', 'push-notification', 'pubsub', 'pub-sub'],
|
|
20
|
+
mail: ['mail', 'smtp', 'mailer', 'sendmail', 'newsletter', 'notification'],
|
|
21
|
+
graphql: ['graphql', 'gql', 'query', 'mutation', 'resolver', 'subscription'],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const JA_INTENTS = {
|
|
25
|
+
server: ['サーバー', 'エンドポイント', 'バックエンド', 'アプリ', 'ウェブアプリ', 'Webアプリ', 'webアプリ',
|
|
26
|
+
'API', 'サービス', 'マイクロサービス', 'ウェブサービス'],
|
|
27
|
+
schema: ['スキーマ', 'モデル', 'テーブル', 'データ型', '型定義', 'エンティティ', 'データモデル', 'リソース'],
|
|
28
|
+
bot: ['ボット', 'チャットボット', 'Bot', 'bot'],
|
|
29
|
+
cli: ['コマンド', 'ツール', 'ターミナル', 'コマンドライン', 'CLI', 'コンソール'],
|
|
30
|
+
page: ['ページ', 'サイト', 'ウェブ', 'フロントエンド', '画面', 'UI', '表示', 'テンプレート', 'ビュー', '見た目'],
|
|
31
|
+
test: ['テスト', '検証', 'テストコード', '単体テスト', '結合テスト', 'ユニットテスト'],
|
|
32
|
+
database: ['データベース', 'ストレージ', '保存', '永続化', 'DB', 'db', 'データ保存', '記録'],
|
|
33
|
+
ai: ['プロンプト', 'LLM', 'llm', '生成AI', '人工知能'],
|
|
34
|
+
auth: ['認証', 'ログイン', 'パスワード', '保護', '権限', '会員', 'サインイン', 'サインアップ',
|
|
35
|
+
'登録', 'ユーザー認証', 'アカウント', 'メンバー', '会員制', '会員機能', 'ログインできる',
|
|
36
|
+
'ログイン機能', '認証機能', '認証付き', 'パスワード保護', 'アクセス制御', 'セキュリティ'],
|
|
37
|
+
crud: ['CRUD', 'crud', '作成', '取得', '更新', '削除', '一覧', '追加', '編集', '管理',
|
|
38
|
+
'管理機能', '一覧表示', 'リスト', '操作'],
|
|
39
|
+
websocket: ['ウェブソケット', 'リアルタイム', 'ソケット', 'WebSocket', 'websocket',
|
|
40
|
+
'リアルタイム通信', 'プッシュ通知', 'ライブ'],
|
|
41
|
+
mail: ['メール', 'メール送信', 'メール機能', 'SMTP', '通知メール', 'メール通知'],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const JA_ENTITIES = {
|
|
45
|
+
'ユーザー': 'User', 'ユーザ': 'User', 'ユーザ管理': 'User', 'ユーザー管理': 'User',
|
|
46
|
+
'会員': 'User', 'メンバー': 'User', 'アカウント': 'User', '利用者': 'User',
|
|
47
|
+
'顧客': 'Customer', 'お客様': 'Customer', 'クライアント': 'Customer',
|
|
48
|
+
'商品': 'Product', 'プロダクト': 'Product', '製品': 'Product', '品物': 'Product',
|
|
49
|
+
'記事': 'Post', '投稿': 'Post', 'ブログ記事': 'Post',
|
|
50
|
+
'タスク': 'Task', 'やること': 'Task', '作業': 'Task',
|
|
51
|
+
'メッセージ': 'Message', 'チャットメッセージ': 'Message',
|
|
52
|
+
'注文': 'Order', 'オーダー': 'Order', '受注': 'Order', '発注': 'Order',
|
|
53
|
+
'コメント': 'Comment', '感想': 'Comment', 'レビュー': 'Review', '評価': 'Review',
|
|
54
|
+
'ファイル': 'File', 'アップロード': 'File',
|
|
55
|
+
'カテゴリ': 'Category', 'カテゴリー': 'Category', '分類': 'Category',
|
|
56
|
+
'イベント': 'Event', '予定': 'Event', 'スケジュール': 'Event', '予約': 'Reservation',
|
|
57
|
+
'プロジェクト': 'Project', '案件': 'Project',
|
|
58
|
+
'書籍': 'Book', '本': 'Book', '図書': 'Book',
|
|
59
|
+
'アイテム': 'Item', 'リスト': 'List', 'ノート': 'Note', 'メモ': 'Note',
|
|
60
|
+
'通知': 'Notification', 'お知らせ': 'Notification',
|
|
61
|
+
'決済': 'Payment', '支払い': 'Payment', '課金': 'Payment',
|
|
62
|
+
'設定': 'Setting', '環境設定': 'Setting', '構成': 'Setting',
|
|
63
|
+
'問い合わせ': 'Contact', 'お問い合わせ': 'Contact', '連絡': 'Contact',
|
|
64
|
+
'掲示板': 'Post', 'スレッド': 'Thread', 'トピック': 'Topic',
|
|
65
|
+
'レシピ': 'Recipe', '料理': 'Recipe',
|
|
66
|
+
'在庫': 'Inventory', '倉庫': 'Inventory',
|
|
67
|
+
'社員': 'Employee', '従業員': 'Employee', 'スタッフ': 'Staff',
|
|
68
|
+
'チケット': 'Ticket', 'チーム': 'Team', 'グループ': 'Group',
|
|
69
|
+
'ログ': 'Log', '履歴': 'Log', 'アクティビティ': 'Activity',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const PLATFORM_WORDS = {
|
|
73
|
+
discord: ['discord', 'ディスコード'],
|
|
74
|
+
slack: ['slack', 'スラック'],
|
|
75
|
+
telegram: ['telegram', 'テレグラム'],
|
|
76
|
+
line: ['line', 'ライン'],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const COMPOSITE_WORDS = {
|
|
80
|
+
fullstack: ['fullstack', 'full-stack', 'フルスタック', '管理システム', '管理アプリ', '管理画面'],
|
|
81
|
+
todo: ['todo', 'todos', 'to-do', 'タスク管理', 'todoアプリ', 'やることリスト', 'todo管理'],
|
|
82
|
+
blog: ['blog', 'ブログ', '記事管理', '投稿管理', 'ブログサイト', 'ブログアプリ'],
|
|
83
|
+
chat: ['chat', 'チャットアプリ', 'メッセンジャー', 'チャット機能', 'チャットルーム'],
|
|
84
|
+
shop: ['shop', 'ecommerce', 'e-commerce', 'ショップ', 'ストア', '通販', 'ECサイト', 'EC', 'オンラインショップ'],
|
|
85
|
+
board: ['board', '掲示板', 'フォーラム', 'BBS', 'bbs'],
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const NOISE = new Set([
|
|
89
|
+
'a', 'an', 'the', 'for', 'with', 'and', 'or', 'on', 'at', 'to', 'of', 'in', 'by',
|
|
90
|
+
'that', 'which', 'this', 'it', 'its', 'my', 'is', 'are', 'has', 'have', 'do', 'does',
|
|
91
|
+
'make', 'create', 'build', 'generate', 'add', 'implement', 'simple', 'basic', 'new',
|
|
92
|
+
'please', 'want', 'need', 'like', 'just', 'some', 'using', 'use', 'based',
|
|
93
|
+
'setup', 'set', 'up', 'get', 'should', 'can', 'could', 'would',
|
|
94
|
+
'の', 'を', 'に', 'で', 'は', 'が', 'と', 'も', 'から', 'まで', 'より', 'って',
|
|
95
|
+
'な', 'ような', 'みたいな', 'っぽい', 'ような感じ',
|
|
96
|
+
'作って', '作る', '作成', '生成', '追加', '実装', 'ください', 'して', 'つけて',
|
|
97
|
+
'付き', '付きの', 'ある', 'ない', 'する', 'できる', 'ほしい', 'したい',
|
|
98
|
+
'機能', '仕組み', 'もの', 'こと', 'やつ', '感じ', '的な',
|
|
99
|
+
'簡単な', 'シンプルな', '基本的な', '新しい',
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
// ── Field Normalization ─────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
const FIELD_SYNONYMS = {
|
|
105
|
+
'e-mail': 'email', 'email_address': 'email', 'mail_address': 'email', 'emailaddress': 'email', 'mail': 'email',
|
|
106
|
+
'user_name': 'username', 'login_name': 'username', 'loginname': 'username', 'login': 'username',
|
|
107
|
+
'first_name': 'firstname', 'firstname': 'name', 'last_name': 'lastname',
|
|
108
|
+
'display_name': 'name', 'full_name': 'name', 'fullname': 'name',
|
|
109
|
+
'phone_number': 'phone', 'tel': 'phone', 'telephone': 'phone', 'phonenumber': 'phone', 'mobile': 'phone',
|
|
110
|
+
'zip_code': 'zipcode', 'postal_code': 'zipcode', 'postalcode': 'zipcode',
|
|
111
|
+
'birth_date': 'birthdate', 'birthday': 'birthdate', 'date_of_birth': 'birthdate',
|
|
112
|
+
'created_at': 'created', 'creation_date': 'created', 'createdat': 'created', 'date_created': 'created',
|
|
113
|
+
'updated_at': 'updated', 'modification_date': 'updated', 'updatedat': 'updated', 'date_updated': 'updated', 'modified_at': 'updated',
|
|
114
|
+
'is_active': 'active', 'isactive': 'active',
|
|
115
|
+
'is_deleted': 'deleted', 'isdeleted': 'deleted',
|
|
116
|
+
'is_public': 'public', 'ispublic': 'public',
|
|
117
|
+
'is_admin': 'admin', 'isadmin': 'admin',
|
|
118
|
+
'is_verified': 'verified', 'isverified': 'verified',
|
|
119
|
+
'is_done': 'done', 'isdone': 'done', 'is_completed': 'completed',
|
|
120
|
+
'is_published': 'published', 'ispublished': 'published',
|
|
121
|
+
'is_enabled': 'enabled', 'isenabled': 'enabled',
|
|
122
|
+
'num_items': 'count', 'item_count': 'count',
|
|
123
|
+
'total_price': 'total', 'total_amount': 'total',
|
|
124
|
+
'img': 'image', 'pic': 'image', 'picture': 'image', 'photo': 'image', 'thumbnail': 'image', 'avatar': 'image',
|
|
125
|
+
'desc': 'description', 'memo': 'description', 'detail': 'description', 'details': 'description',
|
|
126
|
+
'pwd': 'password', 'passwd': 'password', 'pass': 'password', 'pw': 'password',
|
|
127
|
+
'qty': 'quantity', 'amt': 'amount', 'addr': 'address', 'msg': 'message',
|
|
128
|
+
'cat': 'category', 'tags': 'tag', 'lbl': 'label',
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
function normalizeField(name) {
|
|
132
|
+
const lower = name.toLowerCase();
|
|
133
|
+
if (FIELD_SYNONYMS[lower]) return FIELD_SYNONYMS[lower];
|
|
134
|
+
const n = lower.replace(/-/g, '_');
|
|
135
|
+
return FIELD_SYNONYMS[n] || n;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Field Type Inference ────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
const FIELD_TYPES = {
|
|
141
|
+
auto: ['id', 'created', 'updated', 'timestamp'],
|
|
142
|
+
bool: ['active', 'done', 'completed', 'enabled', 'published', 'verified', 'admin', 'public',
|
|
143
|
+
'visible', 'archived', 'deleted', 'premium', 'banned', 'muted', 'online', 'featured',
|
|
144
|
+
'approved', 'blocked', 'pinned', 'starred', 'read'],
|
|
145
|
+
int: ['age', 'count', 'quantity', 'amount', 'price', 'cost', 'total', 'score', 'rating',
|
|
146
|
+
'level', 'order', 'index', 'size', 'width', 'height', 'weight', 'year', 'month',
|
|
147
|
+
'day', 'port', 'priority', 'views', 'likes', 'followers', 'stock', 'points',
|
|
148
|
+
'duration', 'position', 'rank', 'votes'],
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
function inferType(rawName) {
|
|
152
|
+
const n = normalizeField(rawName);
|
|
153
|
+
for (const [type, names] of Object.entries(FIELD_TYPES)) {
|
|
154
|
+
if (names.includes(n)) return type;
|
|
155
|
+
}
|
|
156
|
+
if (n.endsWith('_id') || n.endsWith('id') && n !== 'id') return 'int';
|
|
157
|
+
if (n.startsWith('is_') || n.startsWith('has_') || n.startsWith('can_')) return 'bool';
|
|
158
|
+
if (n.endsWith('count') || n.endsWith('num') || n.endsWith('number')) return 'int';
|
|
159
|
+
if (n.includes('price') || n.includes('cost') || n.includes('amount')) return 'int';
|
|
160
|
+
if (n.includes('date') || n.includes('time') || n === 'birthdate') return 'str';
|
|
161
|
+
return 'str';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function inferModifiers(rawName, type) {
|
|
165
|
+
const n = normalizeField(rawName);
|
|
166
|
+
if (type === 'auto') return [];
|
|
167
|
+
const mods = [];
|
|
168
|
+
if (!['bool', 'auto'].includes(type)) {
|
|
169
|
+
if (['name', 'title', 'email', 'password', 'username', 'content', 'body', 'phone', 'address'].includes(n)) mods.push('required');
|
|
170
|
+
}
|
|
171
|
+
if (n === 'email') mods.push('email');
|
|
172
|
+
if (n === 'name' || n === 'title') { mods.push('min(1)'); mods.push('max(200)'); }
|
|
173
|
+
if (n === 'password') mods.push('min(8)');
|
|
174
|
+
return mods;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── Default Schema Fields ───────────────────────────────────
|
|
178
|
+
|
|
179
|
+
const SCHEMA_PRESETS = {
|
|
180
|
+
user: [['id','auto'], ['name','str','required'], ['email','str','required','email'], ['password','str','required','min(8)']],
|
|
181
|
+
customer: [['id','auto'], ['name','str','required'], ['email','str','required','email'], ['phone','str']],
|
|
182
|
+
post: [['id','auto'], ['title','str','required','min(1)','max(200)'], ['body','str','required'], ['published','bool'], ['created','auto']],
|
|
183
|
+
product: [['id','auto'], ['name','str','required'], ['price','int','required'], ['stock','int'], ['description','str']],
|
|
184
|
+
todo: [['id','auto'], ['title','str','required','min(1)'], ['done','bool']],
|
|
185
|
+
task: [['id','auto'], ['title','str','required'], ['description','str'], ['status','str'], ['done','bool']],
|
|
186
|
+
comment: [['id','auto'], ['body','str','required'], ['author','str','required'], ['created','auto']],
|
|
187
|
+
review: [['id','auto'], ['body','str','required'], ['rating','int'], ['author','str','required'], ['created','auto']],
|
|
188
|
+
message: [['id','auto'], ['content','str','required'], ['sender','str','required'], ['created','auto']],
|
|
189
|
+
order: [['id','auto'], ['total','int','required'], ['status','str'], ['created','auto']],
|
|
190
|
+
event: [['id','auto'], ['title','str','required'], ['date','str','required'], ['description','str']],
|
|
191
|
+
reservation:[['id','auto'], ['date','str','required'], ['name','str','required'], ['status','str'], ['created','auto']],
|
|
192
|
+
note: [['id','auto'], ['title','str','required'], ['content','str'], ['created','auto']],
|
|
193
|
+
book: [['id','auto'], ['title','str','required'], ['author','str','required'], ['price','int'], ['published','bool']],
|
|
194
|
+
category: [['id','auto'], ['name','str','required'], ['description','str']],
|
|
195
|
+
file: [['id','auto'], ['name','str','required'], ['path','str','required'], ['size','int'], ['created','auto']],
|
|
196
|
+
project: [['id','auto'], ['name','str','required'], ['description','str'], ['status','str'], ['created','auto']],
|
|
197
|
+
contact: [['id','auto'], ['name','str','required'], ['email','str','required','email'], ['message','str','required'], ['created','auto']],
|
|
198
|
+
notification:[['id','auto'], ['title','str','required'], ['body','str'], ['read','bool'], ['created','auto']],
|
|
199
|
+
payment: [['id','auto'], ['amount','int','required'], ['status','str'], ['method','str'], ['created','auto']],
|
|
200
|
+
setting: [['id','auto'], ['key','str','required'], ['value','str','required']],
|
|
201
|
+
ticket: [['id','auto'], ['title','str','required'], ['description','str'], ['status','str'], ['priority','int'], ['created','auto']],
|
|
202
|
+
employee: [['id','auto'], ['name','str','required'], ['email','str','required','email'], ['role','str']],
|
|
203
|
+
staff: [['id','auto'], ['name','str','required'], ['email','str','required','email'], ['role','str']],
|
|
204
|
+
thread: [['id','auto'], ['title','str','required'], ['body','str','required'], ['author','str','required'], ['created','auto']],
|
|
205
|
+
recipe: [['id','auto'], ['title','str','required'], ['description','str'], ['ingredients','str'], ['created','auto']],
|
|
206
|
+
inventory:[['id','auto'], ['name','str','required'], ['quantity','int','required'], ['location','str']],
|
|
207
|
+
log: [['id','auto'], ['action','str','required'], ['detail','str'], ['created','auto']],
|
|
208
|
+
activity: [['id','auto'], ['type','str','required'], ['description','str'], ['created','auto']],
|
|
209
|
+
item: [['id','auto'], ['name','str','required'], ['created','auto']],
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
function defaultFields(schemaName) {
|
|
213
|
+
const key = schemaName.toLowerCase();
|
|
214
|
+
const preset = SCHEMA_PRESETS[key];
|
|
215
|
+
if (preset) return preset.map(f => ({ name: f[0], type: f[1], modifiers: f.slice(2) }));
|
|
216
|
+
return [
|
|
217
|
+
{ name: 'id', type: 'auto', modifiers: [] },
|
|
218
|
+
{ name: 'name', type: 'str', modifiers: ['required'] },
|
|
219
|
+
{ name: 'created', type: 'auto', modifiers: [] },
|
|
220
|
+
];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── Utilities ───────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
function singularize(w) {
|
|
226
|
+
if (w.endsWith('ies')) return w.slice(0, -3) + 'y';
|
|
227
|
+
if (w.endsWith('ses') || w.endsWith('xes') || w.endsWith('zes') || w.endsWith('ches') || w.endsWith('shes')) return w.slice(0, -2);
|
|
228
|
+
if (w.endsWith('s') && !w.endsWith('ss') && !w.endsWith('us') && w.length > 3) return w.slice(0, -1);
|
|
229
|
+
return w;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function capitalize(w) {
|
|
233
|
+
return w.charAt(0).toUpperCase() + w.slice(1);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function pluralize(w) {
|
|
237
|
+
if (w.endsWith('y') && !/[aeiou]y$/i.test(w)) return w.slice(0, -1) + 'ies';
|
|
238
|
+
if (w.endsWith('s') || w.endsWith('x') || w.endsWith('z') || w.endsWith('ch') || w.endsWith('sh')) return w + 'es';
|
|
239
|
+
return w + 's';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const RESERVED = new Set([
|
|
243
|
+
'get', 'post', 'put', 'del', 'on', 'in', 'as', 'is', 'isnt', 'if', 'for', 'each', 'while',
|
|
244
|
+
'use', 'fn', 'ret', 'mut', 'pub', 'elif', 'else', 'match', 'try', 'fail', 'server', 'bot',
|
|
245
|
+
'page', 'cli', 'mail', 'db', 'log', 'new', 'not', 'and', 'or', 'then', 'from', 'break',
|
|
246
|
+
'continue', 'throw', 'schema', 'crud', 'auth', 'cors', 'model', 'self', 'test', 'assert',
|
|
247
|
+
'await', 'enum', 'swap', 'repeat', 'until', 'unless', 'extends', 'print', 'static',
|
|
248
|
+
'true', 'false', 'null', 'str', 'int', 'num', 'bool', 'list', 'map', 'any', 'json', 'void', 'auto',
|
|
249
|
+
'search', 'push', 'image', 'csv', 'graphql', 'desktop', 'screen', 'validate', 'prompt',
|
|
250
|
+
'every', 'watch', 'limit', 'env', 'session', 'cookie', 'upload', 'view', 'cache', 'patch',
|
|
251
|
+
'queue', 'job', 'ws', 'sse', 'group', 'storage', 'oauth', 'pay', 'pdf', 'i18n',
|
|
252
|
+
]);
|
|
253
|
+
|
|
254
|
+
function safeName(name) {
|
|
255
|
+
if (RESERVED.has(name.toLowerCase())) return name + 'App';
|
|
256
|
+
return name;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function align(rows) {
|
|
260
|
+
if (rows.length === 0) return [];
|
|
261
|
+
const cols = rows[0].length;
|
|
262
|
+
const widths = Array(cols).fill(0);
|
|
263
|
+
for (const row of rows) {
|
|
264
|
+
for (let i = 0; i < Math.min(cols, row.length); i++) {
|
|
265
|
+
widths[i] = Math.max(widths[i], (row[i] || '').length);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return rows.map(row =>
|
|
269
|
+
row.map((cell, i) => i < cols - 1 ? (cell || '').padEnd(widths[i]) : (cell || '')).join(' ').trimEnd()
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ── Instruction Parsing ─────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
function tokenize(instruction) {
|
|
276
|
+
return instruction
|
|
277
|
+
.replace(/[.,!?;:()'"\[\]{}]/g, ' ')
|
|
278
|
+
.replace(/\s+/g, ' ')
|
|
279
|
+
.trim()
|
|
280
|
+
.split(' ')
|
|
281
|
+
.filter(w => w.length > 0);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function classify(words, raw) {
|
|
285
|
+
const lower = raw.toLowerCase();
|
|
286
|
+
const intents = new Set();
|
|
287
|
+
const mods = {};
|
|
288
|
+
let matchStrength = 0;
|
|
289
|
+
|
|
290
|
+
// English keyword matching
|
|
291
|
+
for (const [intent, kws] of Object.entries(INTENT_WORDS)) {
|
|
292
|
+
for (const w of words) {
|
|
293
|
+
const wl = w.toLowerCase();
|
|
294
|
+
if (kws.includes(wl)) { intents.add(intent); matchStrength++; break; }
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Japanese substring matching
|
|
299
|
+
for (const [intent, kws] of Object.entries(JA_INTENTS)) {
|
|
300
|
+
for (const kw of kws) {
|
|
301
|
+
if (raw.includes(kw)) { intents.add(intent); matchStrength++; break; }
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Composite detection
|
|
306
|
+
for (const [comp, kws] of Object.entries(COMPOSITE_WORDS)) {
|
|
307
|
+
if (kws.some(k => lower.includes(k))) {
|
|
308
|
+
matchStrength += 2;
|
|
309
|
+
if (comp === 'fullstack') { intents.add('server'); intents.add('schema'); intents.add('crud'); intents.add('auth'); intents.add('database'); }
|
|
310
|
+
if (comp === 'todo') { intents.add('server'); intents.add('schema'); intents.add('crud'); intents.add('database'); mods.preset = 'todo'; }
|
|
311
|
+
if (comp === 'blog') { intents.add('server'); intents.add('schema'); intents.add('crud'); intents.add('database'); mods.preset = 'post'; }
|
|
312
|
+
if (comp === 'chat') { intents.add('server'); intents.add('schema'); intents.add('websocket'); intents.add('database'); mods.preset = 'message'; }
|
|
313
|
+
if (comp === 'shop') { intents.add('server'); intents.add('schema'); intents.add('crud'); intents.add('auth'); intents.add('database'); mods.preset = 'product'; }
|
|
314
|
+
if (comp === 'board') { intents.add('server'); intents.add('schema'); intents.add('crud'); intents.add('database'); mods.preset = 'post'; }
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Platform detection
|
|
319
|
+
for (const [plat, kws] of Object.entries(PLATFORM_WORDS)) {
|
|
320
|
+
if (kws.some(k => lower.includes(k))) mods.platform = plat;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// DB type detection
|
|
324
|
+
if (lower.includes('sqlite')) mods.dbType = 'sqlite';
|
|
325
|
+
else if (lower.includes('postgres')) mods.dbType = 'postgres';
|
|
326
|
+
|
|
327
|
+
// Detect entity-like words (nouns that could be schema names)
|
|
328
|
+
const allKnown = new Set([...Object.values(INTENT_WORDS).flat(), ...Object.values(PLATFORM_WORDS).flat(),
|
|
329
|
+
...Object.values(COMPOSITE_WORDS).flat()]);
|
|
330
|
+
const hasEntity = words.some(w => {
|
|
331
|
+
const wl = w.toLowerCase();
|
|
332
|
+
return !NOISE.has(wl) && !allKnown.has(wl) && !/^\d+$/.test(wl) && wl.length > 2 && /^[a-z]+s?$/i.test(wl);
|
|
333
|
+
}) || Object.keys(JA_ENTITIES).some(ja => raw.includes(ja));
|
|
334
|
+
|
|
335
|
+
// "for X" pattern implies schema + crud
|
|
336
|
+
if (hasEntity && intents.has('server')) {
|
|
337
|
+
intents.add('schema'); intents.add('crud');
|
|
338
|
+
}
|
|
339
|
+
if (lower.includes(' for ') && intents.has('server') && !intents.has('schema')) {
|
|
340
|
+
intents.add('schema'); intents.add('crud');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Implicit intent rules
|
|
344
|
+
if (intents.has('crud') && !intents.has('server')) intents.add('server');
|
|
345
|
+
if (intents.has('crud') && !intents.has('schema')) intents.add('schema');
|
|
346
|
+
if (intents.has('auth') && !intents.has('server')) intents.add('server');
|
|
347
|
+
if (intents.has('schema') && intents.has('server')) intents.add('database');
|
|
348
|
+
if (intents.has('bot') && !mods.platform) mods.platform = 'discord';
|
|
349
|
+
|
|
350
|
+
// Default fallback: if nothing detected, server + schema
|
|
351
|
+
if (intents.size === 0) {
|
|
352
|
+
intents.add('server'); intents.add('schema'); intents.add('crud'); intents.add('database');
|
|
353
|
+
mods.fallback = true;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
mods.matchStrength = matchStrength;
|
|
357
|
+
return { intents, mods };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function extractParams(words, raw, intents, mods) {
|
|
361
|
+
const params = { port: 3000, schemaName: null, fields: [], features: intents, platform: mods.platform || null, dbType: mods.dbType || null, commands: [] };
|
|
362
|
+
|
|
363
|
+
// Extract port number
|
|
364
|
+
for (let i = 0; i < words.length; i++) {
|
|
365
|
+
const n = parseInt(words[i]);
|
|
366
|
+
if (!isNaN(n) && n >= 80 && n <= 65535) params.port = n;
|
|
367
|
+
if (words[i].toLowerCase() === 'port' && i + 1 < words.length) {
|
|
368
|
+
const pn = parseInt(words[i + 1]);
|
|
369
|
+
if (!isNaN(pn)) params.port = pn;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Extract entity name — Japanese first (longest match wins)
|
|
374
|
+
const jaEntries = Object.entries(JA_ENTITIES).sort((a, b) => b[0].length - a[0].length);
|
|
375
|
+
for (const [ja, en] of jaEntries) {
|
|
376
|
+
if (raw.includes(ja)) { params.schemaName = en; break; }
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Extract entity name — English (find nouns that aren't keywords)
|
|
380
|
+
if (!params.schemaName) {
|
|
381
|
+
const allKnown = new Set([...Object.values(INTENT_WORDS).flat(), ...Object.values(PLATFORM_WORDS).flat(),
|
|
382
|
+
...Object.values(COMPOSITE_WORDS).flat()]);
|
|
383
|
+
for (const w of words) {
|
|
384
|
+
const wl = w.toLowerCase();
|
|
385
|
+
if (NOISE.has(wl) || allKnown.has(wl) || /^\d+$/.test(wl) || wl.length <= 2) continue;
|
|
386
|
+
params.schemaName = capitalize(singularize(wl));
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Preset override
|
|
392
|
+
if (mods.preset && !params.schemaName) {
|
|
393
|
+
params.schemaName = capitalize(mods.preset);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Default schema name
|
|
397
|
+
if (!params.schemaName && intents.has('schema')) params.schemaName = 'Item';
|
|
398
|
+
|
|
399
|
+
// Extract fields from "with" clause
|
|
400
|
+
const lower = raw.toLowerCase();
|
|
401
|
+
const withIdx = lower.indexOf(' with ');
|
|
402
|
+
if (withIdx !== -1) {
|
|
403
|
+
const afterWith = lower.slice(withIdx + 6);
|
|
404
|
+
const stopWords = new Set(Object.values(INTENT_WORDS).flat());
|
|
405
|
+
const fieldNames = afterWith
|
|
406
|
+
.split(/[\s,、]+/)
|
|
407
|
+
.map(w => w.replace(/[^a-zA-Z0-9_\-]/g, ''))
|
|
408
|
+
.filter(w => w.length > 1 && !NOISE.has(w) && !stopWords.has(w) && !/^\d+$/.test(w));
|
|
409
|
+
if (fieldNames.length > 0) {
|
|
410
|
+
params.fields = [{ name: 'id', type: 'auto', modifiers: [] }];
|
|
411
|
+
for (const rawName of fieldNames) {
|
|
412
|
+
const name = normalizeField(rawName);
|
|
413
|
+
const type = inferType(name);
|
|
414
|
+
params.fields.push({ name, type, modifiers: inferModifiers(name, type) });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Use preset fields if none extracted
|
|
420
|
+
if (params.fields.length === 0 && params.schemaName) {
|
|
421
|
+
params.fields = defaultFields(params.schemaName);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Extract bot commands
|
|
425
|
+
const cmdMatch = raw.match(/command[s]?\s+(.+)/i);
|
|
426
|
+
if (cmdMatch) {
|
|
427
|
+
params.commands = cmdMatch[1].split(/[\s,、and]+/).filter(c => c.length > 1).map(c => ({
|
|
428
|
+
trigger: c.startsWith('!') ? c : '!' + c,
|
|
429
|
+
response: `${capitalize(c.replace('!', ''))}!`,
|
|
430
|
+
}));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return params;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ── Template Engine ─────────────────────────────────────────
|
|
437
|
+
|
|
438
|
+
function schemaBlock(name, fields) {
|
|
439
|
+
const rows = fields.map(f => [f.name, f.type, ...f.modifiers]);
|
|
440
|
+
const aligned = align(rows);
|
|
441
|
+
return `schema ${name}:\n` + aligned.map(l => ' ' + l).join('\n');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function dbBlock(type) {
|
|
445
|
+
if (type === 'sqlite') return 'db.sql "sqlite"';
|
|
446
|
+
if (type === 'postgres') return 'db.sql "postgres"';
|
|
447
|
+
return 'db "data/"';
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function serverBlock(name, port, sections) {
|
|
451
|
+
return `server ${name} port ${port}:\n` + sections.map(l => ' ' + l).join('\n');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function botBlock(platform, commands) {
|
|
455
|
+
const token = platform.toUpperCase() + '_TOKEN';
|
|
456
|
+
const lines = [`bot mybot type "${platform}" token ${token} prefix "!":`];
|
|
457
|
+
if (commands.length === 0) {
|
|
458
|
+
commands = [
|
|
459
|
+
{ trigger: '!hello', response: 'Hello!' },
|
|
460
|
+
{ trigger: '!help', response: 'Commands: !hello, !help' },
|
|
461
|
+
];
|
|
462
|
+
}
|
|
463
|
+
for (const cmd of commands) {
|
|
464
|
+
lines.push(` on "${cmd.trigger}" (msg):`);
|
|
465
|
+
lines.push(` msg.reply("${cmd.response}")`);
|
|
466
|
+
}
|
|
467
|
+
return lines.join('\n');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function cliBlock(name) {
|
|
471
|
+
return `cli ${safeName(name || 'app')}:
|
|
472
|
+
flag "-n" "--name" str "Name"
|
|
473
|
+
flag "-o" "--output" str "Output file"
|
|
474
|
+
run (args):
|
|
475
|
+
log("Name: " + args.name)
|
|
476
|
+
log("Output: " + args.output)`;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function testBlock(name) {
|
|
480
|
+
return `test "${name || 'app'} tests":
|
|
481
|
+
assert 1 + 1 == 2
|
|
482
|
+
assert "hello".length == 5
|
|
483
|
+
assert [1, 2, 3].length == 3`;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function graphqlBlock() {
|
|
487
|
+
return `graphql "/graphql"`;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function pageBlock(title) {
|
|
491
|
+
return `page "index.html":
|
|
492
|
+
h1 "${title || 'Welcome'}"
|
|
493
|
+
p "Built with NAIDE"`;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function mailBlock() {
|
|
497
|
+
return `mail "smtp.example.com" 587:
|
|
498
|
+
user SMTP_USER
|
|
499
|
+
pass SMTP_PASS`;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ── Code Composer ───────────────────────────────────────────
|
|
503
|
+
|
|
504
|
+
function compose(intents, params) {
|
|
505
|
+
const sections = [];
|
|
506
|
+
const schemaName = params.schemaName || 'Item';
|
|
507
|
+
const nameLower = schemaName.toLowerCase();
|
|
508
|
+
const namePlural = pluralize(nameLower);
|
|
509
|
+
|
|
510
|
+
if (intents.has('schema') && params.fields.length > 0) {
|
|
511
|
+
sections.push(schemaBlock(schemaName, params.fields));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (intents.has('database')) {
|
|
515
|
+
sections.push(dbBlock(params.dbType));
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (intents.has('server')) {
|
|
519
|
+
const body = [];
|
|
520
|
+
body.push('cors "*"');
|
|
521
|
+
|
|
522
|
+
if (intents.has('crud') && intents.has('schema')) {
|
|
523
|
+
body.push(`crud "/api/${namePlural}" ${schemaName}`);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (intents.has('auth')) {
|
|
527
|
+
body.push('auth JWT_SECRET:');
|
|
528
|
+
body.push(' protect "/api/*"');
|
|
529
|
+
body.push(' public "/api/auth/*"');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (intents.has('websocket')) {
|
|
533
|
+
body.push('');
|
|
534
|
+
body.push('ws "/ws":');
|
|
535
|
+
body.push(' on "message" (data):');
|
|
536
|
+
body.push(' socket.send(data)');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (intents.has('ai')) {
|
|
540
|
+
body.push('');
|
|
541
|
+
body.push('post "/api/ask" (req, res):');
|
|
542
|
+
body.push(' str answer = await ai.ask(req.body.prompt)');
|
|
543
|
+
body.push(' ret {answer}');
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
body.push('');
|
|
547
|
+
body.push('get "/api/health" (req, res):');
|
|
548
|
+
if (intents.has('schema') && intents.has('crud')) {
|
|
549
|
+
body.push(` ret {status: "ok", ${namePlural}: ${schemaName}Store.count()}`);
|
|
550
|
+
} else {
|
|
551
|
+
body.push(' ret {status: "ok"}');
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
sections.push(serverBlock(safeName(nameLower === 'item' ? 'app' : nameLower), params.port, body));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (intents.has('bot')) {
|
|
558
|
+
sections.push(botBlock(params.platform || 'discord', params.commands));
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (intents.has('cli')) {
|
|
562
|
+
sections.push(cliBlock(nameLower));
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (intents.has('graphql')) {
|
|
566
|
+
sections.push(graphqlBlock());
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (intents.has('page')) {
|
|
570
|
+
sections.push(pageBlock(schemaName));
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (intents.has('mail')) {
|
|
574
|
+
sections.push(mailBlock());
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (intents.has('test')) {
|
|
578
|
+
sections.push(testBlock(nameLower));
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return sections.join('\n\n') + '\n';
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ── Self-Healing Validator ──────────────────────────────────
|
|
585
|
+
|
|
586
|
+
function validate(code) {
|
|
587
|
+
let current = code;
|
|
588
|
+
let fixed = false;
|
|
589
|
+
const repairs = [];
|
|
590
|
+
|
|
591
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
592
|
+
try {
|
|
593
|
+
const lexer = new Lexer(current);
|
|
594
|
+
const tokens = lexer.tokenize();
|
|
595
|
+
const parser = new Parser(tokens);
|
|
596
|
+
parser.parse();
|
|
597
|
+
return { code: current, valid: true, fixed, repairs };
|
|
598
|
+
} catch (err) {
|
|
599
|
+
const before = current;
|
|
600
|
+
const repair = heal(current, err);
|
|
601
|
+
current = repair.code;
|
|
602
|
+
if (current === before) return { code: current, valid: false, fixed, repairs };
|
|
603
|
+
if (repair.description) repairs.push(repair.description);
|
|
604
|
+
fixed = true;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return { code: current, valid: false, fixed, repairs };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function heal(code, err) {
|
|
612
|
+
const msg = err.message;
|
|
613
|
+
const lines = code.split('\n');
|
|
614
|
+
const lineMatch = msg.match(/line (\d+)/);
|
|
615
|
+
if (!lineMatch) return { code, description: null };
|
|
616
|
+
|
|
617
|
+
const lineIdx = parseInt(lineMatch[1]) - 1;
|
|
618
|
+
if (lineIdx < 0 || lineIdx >= lines.length) return { code, description: null };
|
|
619
|
+
const line = lines[lineIdx];
|
|
620
|
+
|
|
621
|
+
// Missing colon at end of block header
|
|
622
|
+
if (msg.includes('COLON') || msg.includes('Expected ":"')) {
|
|
623
|
+
if (!line.trimEnd().endsWith(':')) {
|
|
624
|
+
lines[lineIdx] = line.trimEnd() + ':';
|
|
625
|
+
return { code: lines.join('\n'), description: `L${lineIdx + 1}: added missing ':' at end of block header` };
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Unexpected indent — previous line probably needs a colon
|
|
630
|
+
if (msg.includes('INDENT') && lineIdx > 0) {
|
|
631
|
+
const prev = lines[lineIdx - 1];
|
|
632
|
+
if (!prev.trimEnd().endsWith(':') && !prev.trimEnd().endsWith(',')) {
|
|
633
|
+
lines[lineIdx - 1] = prev.trimEnd() + ':';
|
|
634
|
+
return { code: lines.join('\n'), description: `L${lineIdx}: added ':' to previous line to open block` };
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Reserved keyword used as identifier — rename it
|
|
639
|
+
const reserved = ['get', 'post', 'put', 'del', 'on', 'in', 'as', 'is', 'log', 'new', 'not', 'and', 'or', 'if', 'for', 'use', 'fn', 'ret', 'mut'];
|
|
640
|
+
if (msg.includes('Unexpected') || msg.includes('Expected IDENT')) {
|
|
641
|
+
for (const kw of reserved) {
|
|
642
|
+
const fieldPattern = new RegExp(`^(\\s+)${kw}(\\s+(?:str|int|num|bool|auto))`, 'm');
|
|
643
|
+
if (fieldPattern.test(line)) {
|
|
644
|
+
lines[lineIdx] = line.replace(fieldPattern, `$1${kw}_field$2`);
|
|
645
|
+
return { code: lines.join('\n'), description: `L${lineIdx + 1}: renamed reserved word '${kw}' → '${kw}_field'` };
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Unexpected token — try removing the problematic line (non-structural only)
|
|
651
|
+
if (msg.includes('Unexpected') && !line.trim().match(/^(schema|server|bot|cli|page|test|fn|db)\b/)) {
|
|
652
|
+
const removed = line.trim();
|
|
653
|
+
lines.splice(lineIdx, 1);
|
|
654
|
+
return { code: lines.join('\n'), description: `L${lineIdx + 1}: removed problematic line '${removed.slice(0, 40)}'` };
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return { code, description: null };
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// ── Suggestions ─────────────────────────────────────────────
|
|
661
|
+
|
|
662
|
+
const KEYWORD_CHEATSHEET = {
|
|
663
|
+
'Intent': 'server, api, rest, bot, cli, page, test, database, ai, crud, auth, websocket, mail, graphql',
|
|
664
|
+
'Composite': 'todo, blog, chat, shop, fullstack, board',
|
|
665
|
+
'Platform': 'discord, slack, telegram, line',
|
|
666
|
+
'DB': 'sqlite, postgres',
|
|
667
|
+
'Japanese': 'サーバー, 認証, ログイン, 会員, CRUD, 管理, データベース, ボット, テスト, ページ',
|
|
668
|
+
'JP Entity': 'ユーザー, 商品, 記事, タスク, 注文, コメント, イベント, 問い合わせ, 通知, 掲示板',
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
function buildSuggestion(mods) {
|
|
672
|
+
if (!mods.fallback && mods.matchStrength >= 1) return null;
|
|
673
|
+
const lines = [' Hint: use these keywords for better results:'];
|
|
674
|
+
for (const [cat, kws] of Object.entries(KEYWORD_CHEATSHEET)) {
|
|
675
|
+
lines.push(` ${cat.padEnd(10)} ${kws}`);
|
|
676
|
+
}
|
|
677
|
+
return lines.join('\n');
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// ── Pre-Processor (for ~~ in source files) ──────────────────
|
|
681
|
+
|
|
682
|
+
export function expandDirectives(source) {
|
|
683
|
+
const lines = source.split('\n');
|
|
684
|
+
const result = [];
|
|
685
|
+
let expanded = false;
|
|
686
|
+
|
|
687
|
+
for (const line of lines) {
|
|
688
|
+
const m = line.match(/^(\s*)~~\s+(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)')\s*$/);
|
|
689
|
+
if (m) {
|
|
690
|
+
const indent = m[1];
|
|
691
|
+
const instruction = m[2] ?? m[3];
|
|
692
|
+
const gen = generate(instruction);
|
|
693
|
+
const genLines = gen.code.trimEnd().split('\n');
|
|
694
|
+
for (const gl of genLines) {
|
|
695
|
+
result.push(indent + gl);
|
|
696
|
+
}
|
|
697
|
+
expanded = true;
|
|
698
|
+
} else {
|
|
699
|
+
result.push(line);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return { source: result.join('\n'), expanded };
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// ── Public API ──────────────────────────────────────────────
|
|
707
|
+
|
|
708
|
+
export function generate(instruction, options = {}) {
|
|
709
|
+
const words = tokenize(instruction);
|
|
710
|
+
const { intents, mods } = classify(words, instruction);
|
|
711
|
+
const params = extractParams(words, instruction, intents, mods);
|
|
712
|
+
|
|
713
|
+
if (options.port) params.port = options.port;
|
|
714
|
+
if (options.schemaName) params.schemaName = options.schemaName;
|
|
715
|
+
if (options.fields) params.fields = options.fields;
|
|
716
|
+
|
|
717
|
+
let code = compose(intents, params);
|
|
718
|
+
const result = validate(code);
|
|
719
|
+
|
|
720
|
+
return {
|
|
721
|
+
code: result.code,
|
|
722
|
+
valid: result.valid,
|
|
723
|
+
fixed: result.fixed,
|
|
724
|
+
repairs: result.repairs,
|
|
725
|
+
intents: [...intents],
|
|
726
|
+
schema: params.schemaName,
|
|
727
|
+
port: params.port,
|
|
728
|
+
suggestion: buildSuggestion(mods),
|
|
729
|
+
fallback: !!mods.fallback,
|
|
730
|
+
};
|
|
731
|
+
}
|
package/src/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Parser } from './parser.js';
|
|
|
3
3
|
import { Generator } from './generator.js';
|
|
4
4
|
import { preprocess } from './preprocess.js';
|
|
5
5
|
import { TypeChecker } from './typechecker.js';
|
|
6
|
+
import { generate, expandDirectives } from './gen.js';
|
|
6
7
|
|
|
7
8
|
async function getGenerator(target, options) {
|
|
8
9
|
switch (target) {
|
|
@@ -80,6 +81,9 @@ export function compile(source, { mode = 'naide', runtimePath, sourceFile, typeC
|
|
|
80
81
|
if (mode === 'x') {
|
|
81
82
|
processedSource = preprocess(source);
|
|
82
83
|
}
|
|
84
|
+
if (processedSource.includes('~~')) {
|
|
85
|
+
processedSource = expandDirectives(processedSource).source;
|
|
86
|
+
}
|
|
83
87
|
try {
|
|
84
88
|
const lexer = new Lexer(processedSource);
|
|
85
89
|
const tokens = lexer.tokenize();
|
|
@@ -118,6 +122,9 @@ export async function compileAsync(source, { mode = 'naide', runtimePath, source
|
|
|
118
122
|
if (mode === 'x') {
|
|
119
123
|
processedSource = preprocess(source);
|
|
120
124
|
}
|
|
125
|
+
if (processedSource.includes('~~')) {
|
|
126
|
+
processedSource = expandDirectives(processedSource).source;
|
|
127
|
+
}
|
|
121
128
|
try {
|
|
122
129
|
const lexer = new Lexer(processedSource);
|
|
123
130
|
const tokens = lexer.tokenize();
|
|
@@ -155,4 +162,4 @@ export function transpile(source, opts) {
|
|
|
155
162
|
return compile(source, opts).js;
|
|
156
163
|
}
|
|
157
164
|
|
|
158
|
-
export { preprocess };
|
|
165
|
+
export { preprocess, generate };
|
package/src/parser.js
CHANGED
|
@@ -1497,7 +1497,7 @@ export class Parser {
|
|
|
1497
1497
|
|
|
1498
1498
|
const modifiers = [];
|
|
1499
1499
|
while (!this.at(T.NEWLINE) && !this.at(T.DEDENT) && !this.at(T.EOF)) {
|
|
1500
|
-
if (this.
|
|
1500
|
+
if (this.isIdentLike(this.peek().type)) {
|
|
1501
1501
|
const modName = this.advance().value;
|
|
1502
1502
|
if (this.at(T.LPAREN)) {
|
|
1503
1503
|
this.advance();
|