naider 1.11.0 → 1.12.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/generator.js CHANGED
@@ -143,6 +143,11 @@ export class Generator {
143
143
  case 'ReturnRedirect': return this.visitReturnRedirect(node);
144
144
  case 'ReturnDownload': return this.visitReturnDownload(node);
145
145
  case 'PromptDecl': return this.visitPrompt(node);
146
+ case 'Page': return this.visitPage(node);
147
+ case 'CliApp': return this.visitCli(node);
148
+ case 'MailConfig': return this.visitMail(node);
149
+ case 'DesktopApp': return this.visitDesktop(node);
150
+ case 'Screen': return this.visitScreen(node);
146
151
  default:
147
152
  this.emit(`/* unknown: ${node.type} */`);
148
153
  }
@@ -400,6 +405,8 @@ export class Generator {
400
405
  this.visitOpenapi(node.name, child);
401
406
  } else if (child.type === 'QueueDecl') {
402
407
  this.visitQueue(child);
408
+ } else if (child.type === 'GraphqlDecl') {
409
+ this.visitGraphql(node.name, child);
403
410
  } else {
404
411
  this.visitStatement(child);
405
412
  }
@@ -428,8 +435,21 @@ export class Generator {
428
435
  }
429
436
 
430
437
  visitBot(node) {
431
- const tokenExpr = node.token ? this.expr(node.token) : 'process.env.DISCORD_TOKEN';
438
+ const tokenExpr = node.token ? this.expr(node.token) : 'process.env.BOT_TOKEN';
439
+ const botType = this.rawBotType(node.botType);
432
440
 
441
+ if (botType === 'slack') return this.visitSlackBot(node, tokenExpr);
442
+ if (botType === 'telegram') return this.visitTelegramBot(node, tokenExpr);
443
+ if (botType === 'line') return this.visitLineBot(node, tokenExpr);
444
+ return this.visitDiscordBot(node, tokenExpr);
445
+ }
446
+
447
+ rawBotType(bt) {
448
+ if (!bt) return 'discord';
449
+ return bt.raw || bt.parts?.map(p => p.value).join('') || 'discord';
450
+ }
451
+
452
+ visitDiscordBot(node, tokenExpr) {
433
453
  this.emit(`import { Client, GatewayIntentBits, REST, Routes, SlashCommandBuilder } from 'discord.js';`);
434
454
  this.emitRaw('');
435
455
  this.emit(`const ${node.name} = new Client({`);
@@ -503,6 +523,120 @@ export class Generator {
503
523
  this.emitRaw('');
504
524
  }
505
525
 
526
+ visitSlackBot(node, tokenExpr) {
527
+ this.emit(`import pkg from '@slack/bolt';`);
528
+ this.emit(`const { App: SlackApp } = pkg;`);
529
+ this.emitRaw('');
530
+ this.emit(`const ${node.name} = new SlackApp({`);
531
+ this.indent++;
532
+ this.emit(`token: ${tokenExpr},`);
533
+ this.emit(`signingSecret: process.env.SLACK_SIGNING_SECRET,`);
534
+ this.indent--;
535
+ this.emit(`});`);
536
+ this.emitRaw('');
537
+
538
+ for (const handler of node.handlers) {
539
+ if (handler.type === 'BotEvent') {
540
+ const evtName = handler.event.raw || handler.event.parts?.map(p => p.value).join('');
541
+ const params = handler.params.length > 0 ? handler.params.join(', ') : '';
542
+ const needsAsync = this.bodyUsesAwait(handler.body);
543
+ const asyncPrefix = needsAsync ? 'async ' : '';
544
+ if (evtName === 'message') {
545
+ this.emit(`${node.name}.message(${asyncPrefix}({ message, say${params ? ', ' + params : ''} }) => {`);
546
+ } else {
547
+ this.emit(`${node.name}.event('${evtName}', ${asyncPrefix}({ event${params ? ', ' + params : ''} }) => {`);
548
+ }
549
+ this.indent++;
550
+ for (const stmt of handler.body) this.visitStatement(stmt);
551
+ this.indent--;
552
+ this.emit(`});`);
553
+ this.emitRaw('');
554
+ } else if (handler.type === 'BotSlashCmd') {
555
+ const cmdName = handler.name.raw || handler.name.parts?.map(p => p.value).join('');
556
+ this.emit(`${node.name}.command('/${cmdName}', async ({ command, ack, respond }) => {`);
557
+ this.indent++;
558
+ this.emit(`await ack();`);
559
+ for (const stmt of handler.body) this.visitStatement(stmt);
560
+ this.indent--;
561
+ this.emit(`});`);
562
+ this.emitRaw('');
563
+ }
564
+ }
565
+
566
+ this.emit(`(async () => { await ${node.name}.start(3000); console.log('Slack bot running'); })();`);
567
+ this.emitRaw('');
568
+ }
569
+
570
+ visitTelegramBot(node, tokenExpr) {
571
+ this.emit(`import TelegramBot from 'node-telegram-bot-api';`);
572
+ this.emitRaw('');
573
+ this.emit(`const ${node.name} = new TelegramBot(${tokenExpr}, { polling: true });`);
574
+ this.emitRaw('');
575
+
576
+ for (const handler of node.handlers) {
577
+ if (handler.type === 'BotEvent') {
578
+ const evtName = handler.event.raw || handler.event.parts?.map(p => p.value).join('');
579
+ const params = handler.params.length > 0 ? handler.params.join(', ') : 'msg';
580
+ const needsAsync = this.bodyUsesAwait(handler.body);
581
+ const asyncPrefix = needsAsync ? 'async ' : '';
582
+ this.emit(`${node.name}.on('${evtName}', ${asyncPrefix}(${params}) => {`);
583
+ this.indent++;
584
+ for (const stmt of handler.body) this.visitStatement(stmt);
585
+ this.indent--;
586
+ this.emit(`});`);
587
+ this.emitRaw('');
588
+ } else if (handler.type === 'BotSlashCmd') {
589
+ const cmdName = handler.name.raw || handler.name.parts?.map(p => p.value).join('');
590
+ const param = handler.params.length > 0 ? handler.params[0] : 'msg';
591
+ this.emit(`${node.name}.onText(/\\/${cmdName}/, async (${param}) => {`);
592
+ this.indent++;
593
+ for (const stmt of handler.body) this.visitStatement(stmt);
594
+ this.indent--;
595
+ this.emit(`});`);
596
+ this.emitRaw('');
597
+ }
598
+ }
599
+ }
600
+
601
+ visitLineBot(node, tokenExpr) {
602
+ this.emit(`import line from '@line/bot-sdk';`);
603
+ this.emit(`import express from 'express';`);
604
+ this.emitRaw('');
605
+ this.emit(`const __lineConfig = { channelAccessToken: ${tokenExpr}, channelSecret: process.env.LINE_CHANNEL_SECRET };`);
606
+ this.emit(`const ${node.name} = new line.messagingApi.MessagingApiClient({ channelAccessToken: ${tokenExpr} });`);
607
+ this.emit(`const __lineApp = express();`);
608
+ this.emitRaw('');
609
+
610
+ this.emit(`__lineApp.post('/webhook', line.middleware(__lineConfig), (req, res) => {`);
611
+ this.indent++;
612
+ this.emit(`Promise.all(req.body.events.map(__handleLineEvent)).then(r => res.json(r));`);
613
+ this.indent--;
614
+ this.emit(`});`);
615
+ this.emitRaw('');
616
+
617
+ this.emit(`async function __handleLineEvent(event) {`);
618
+ this.indent++;
619
+ for (const handler of node.handlers) {
620
+ if (handler.type === 'BotEvent') {
621
+ const evtName = handler.event.raw || handler.event.parts?.map(p => p.value).join('');
622
+ if (evtName === 'message') {
623
+ this.emit(`if (event.type === 'message') {`);
624
+ } else {
625
+ this.emit(`if (event.type === '${evtName}') {`);
626
+ }
627
+ this.indent++;
628
+ for (const stmt of handler.body) this.visitStatement(stmt);
629
+ this.indent--;
630
+ this.emit(`}`);
631
+ }
632
+ }
633
+ this.indent--;
634
+ this.emit(`}`);
635
+ this.emitRaw('');
636
+ this.emit(`__lineApp.listen(3000, () => console.log('LINE bot running on port 3000'));`);
637
+ this.emitRaw('');
638
+ }
639
+
506
640
  visitRoute(appName, route) {
507
641
  const method = route.method === 'del' ? 'delete' : route.method;
508
642
  const path = this.stringValue(route.path);
@@ -1161,13 +1295,24 @@ export class Generator {
1161
1295
  }
1162
1296
 
1163
1297
  visitEvery(node) {
1164
- this.runtimeImports.add('scheduleEvery');
1165
-
1166
1298
  const interval = this.expr(node.interval);
1167
1299
  const needsAsync = this.bodyUsesAwait(node.body);
1168
1300
  const asyncPrefix = needsAsync ? 'async ' : '';
1169
1301
 
1170
- this.emit(`scheduleEvery(${interval}, ${asyncPrefix}() => {`);
1302
+ let raw = '';
1303
+ if (node.interval.type === 'String' && node.interval.value) {
1304
+ raw = node.interval.value.raw || node.interval.value.parts?.map(p => p.value).join('') || '';
1305
+ }
1306
+ const isCron = /^[\d*\/,-]+\s+[\d*\/,-]+\s+[\d*\/,-]+\s+[\d*\/,-]+\s+[\d*\/,-]+/.test(raw.trim());
1307
+
1308
+ if (isCron) {
1309
+ this.emit(`import cron from 'node-cron';`);
1310
+ this.emitRaw('');
1311
+ this.emit(`cron.schedule(${interval}, ${asyncPrefix}() => {`);
1312
+ } else {
1313
+ this.runtimeImports.add('scheduleEvery');
1314
+ this.emit(`scheduleEvery(${interval}, ${asyncPrefix}() => {`);
1315
+ }
1171
1316
  this.indent++;
1172
1317
  for (const stmt of node.body) this.visitStatement(stmt);
1173
1318
  this.indent--;
@@ -1190,6 +1335,282 @@ export class Generator {
1190
1335
  this.emitRaw('');
1191
1336
  }
1192
1337
 
1338
+ // ===== Page (Frontend HTML) =====
1339
+
1340
+ visitPage(node) {
1341
+ const filename = this.rawPageString(node.filename);
1342
+ this.emit(`import { writeFileSync } from 'fs';`);
1343
+ this.emitRaw('');
1344
+
1345
+ const headParts = [];
1346
+ const bodyParts = [];
1347
+ let pageTitle = 'NAIDE Page';
1348
+
1349
+ for (const el of node.elements) {
1350
+ this.classifyPageElement(el, headParts, bodyParts, (t) => { pageTitle = t; });
1351
+ }
1352
+
1353
+ this.emit(`writeFileSync(${JSON.stringify(filename)}, \`<!DOCTYPE html>`);
1354
+ this.emit(`<html lang="en">`);
1355
+ this.emit(`<head>`);
1356
+ this.emit(`<meta charset="UTF-8">`);
1357
+ this.emit(`<meta name="viewport" content="width=device-width, initial-scale=1.0">`);
1358
+ this.emit(`<title>\${${JSON.stringify(pageTitle)}}</title>`);
1359
+ for (const h of headParts) this.emit(h);
1360
+ this.emit(`</head>`);
1361
+ this.emit(`<body>`);
1362
+ for (const b of bodyParts) this.emit(b);
1363
+ this.emit(`</body>`);
1364
+ this.emit(`</html>\`);`);
1365
+ this.emitRaw('');
1366
+ }
1367
+
1368
+ classifyPageElement(el, head, body, setTitle) {
1369
+ const tag = el.tag;
1370
+ const arg0 = el.args[0] ? this.rawPageString(el.args[0]) : '';
1371
+ const arg1 = el.args[1] ? this.rawPageString(el.args[1]) : '';
1372
+
1373
+ if (tag === 'title') { setTitle(arg0); return; }
1374
+ if (tag === 'style') { head.push(`<link rel="stylesheet" href="${arg0}">`); return; }
1375
+ if (tag === 'meta') { head.push(`<meta name="${arg0}" content="${arg1}">`); return; }
1376
+ if (tag === 'script') { body.push(`<script src="${arg0}"></script>`); return; }
1377
+ if (tag === 'link') { head.push(`<link rel="stylesheet" href="${arg0}">`); return; }
1378
+ if (tag === 'img') { body.push(`<img src="${arg0}" alt="${arg1}">`); return; }
1379
+ if (tag === 'a') { body.push(`<a href="${arg0}">${arg1}</a>`); return; }
1380
+ if (tag === 'input') { body.push(`<input type="${arg0}" name="${arg1}" placeholder="${arg1}">`); return; }
1381
+
1382
+ if (el.children && el.children.length > 0) {
1383
+ const cls = arg0 ? ` class="${arg0}"` : '';
1384
+ body.push(`<${tag}${cls}>`);
1385
+ for (const child of el.children) this.classifyPageElement(child, head, body, setTitle);
1386
+ body.push(`</${tag}>`);
1387
+ } else {
1388
+ body.push(`<${tag}>${arg0}</${tag}>`);
1389
+ }
1390
+ }
1391
+
1392
+ rawPageString(strData) {
1393
+ if (!strData) return '';
1394
+ if (strData.raw !== null && strData.raw !== undefined) return strData.raw;
1395
+ if (strData.parts) return strData.parts.map(p => p.value).join('');
1396
+ return '';
1397
+ }
1398
+
1399
+ // ===== CLI App =====
1400
+
1401
+ visitCli(node) {
1402
+ const name = node.name;
1403
+ const desc = node.description ? this.rawPageString(node.description) : name;
1404
+
1405
+ this.emit(`const __argv = process.argv.slice(2);`);
1406
+ this.emit(`const args = {};`);
1407
+ this.emitRaw('');
1408
+
1409
+ this.emit(`if (__argv.includes('--help') || __argv.includes('-h')) {`);
1410
+ this.indent++;
1411
+ this.emit(`console.log(${JSON.stringify(desc)});`);
1412
+ this.emit(`console.log('Options:');`);
1413
+ for (const arg of node.args) {
1414
+ const argName = this.rawPageString(arg.name);
1415
+ const argDesc = arg.description ? this.rawPageString(arg.description) : '';
1416
+ this.emit(`console.log(' --${argName} <${arg.type}> ${argDesc}');`);
1417
+ }
1418
+ for (const flag of node.flags) {
1419
+ const s = this.rawPageString(flag.short);
1420
+ const l = this.rawPageString(flag.long);
1421
+ const d = flag.description ? this.rawPageString(flag.description) : '';
1422
+ this.emit(`console.log(' -${s}, --${l} ${d}');`);
1423
+ }
1424
+ this.emit(`process.exit(0);`);
1425
+ this.indent--;
1426
+ this.emit(`}`);
1427
+ this.emitRaw('');
1428
+
1429
+ this.emit(`for (let __i = 0; __i < __argv.length; __i++) {`);
1430
+ this.indent++;
1431
+ for (const arg of node.args) {
1432
+ const argName = this.rawPageString(arg.name);
1433
+ const coerce = arg.type === 'int' ? 'parseInt(__argv[++__i])' :
1434
+ arg.type === 'num' ? 'parseFloat(__argv[++__i])' :
1435
+ arg.type === 'bool' ? 'true' : '__argv[++__i]';
1436
+ this.emit(`if (__argv[__i] === '--${argName}') { args.${argName} = ${coerce}; continue; }`);
1437
+ }
1438
+ for (const flag of node.flags) {
1439
+ const s = this.rawPageString(flag.short);
1440
+ const l = this.rawPageString(flag.long);
1441
+ this.emit(`if (__argv[__i] === '-${s}' || __argv[__i] === '--${l}') { args.${l} = true; continue; }`);
1442
+ }
1443
+ this.indent--;
1444
+ this.emit(`}`);
1445
+ this.emitRaw('');
1446
+
1447
+ if (node.run) {
1448
+ const runParam = node.run.params.length > 0 ? node.run.params[0] : 'args';
1449
+ if (runParam !== 'args') {
1450
+ this.emit(`const ${runParam} = args;`);
1451
+ }
1452
+ for (const stmt of node.run.body) this.visitStatement(stmt);
1453
+ }
1454
+ this.emitRaw('');
1455
+ }
1456
+
1457
+ // ===== Mail =====
1458
+
1459
+ visitMail(node) {
1460
+ const host = this.generateString(node.host);
1461
+ const port = this.expr(node.port);
1462
+ const user = node.user ? this.expr(node.user) : 'process.env.MAIL_USER';
1463
+ const pass = node.pass ? this.expr(node.pass) : 'process.env.MAIL_PASS';
1464
+
1465
+ this.emit(`import nodemailer from 'nodemailer';`);
1466
+ this.emitRaw('');
1467
+ this.emit(`const mail = nodemailer.createTransport({`);
1468
+ this.indent++;
1469
+ this.emit(`host: ${host},`);
1470
+ this.emit(`port: ${port},`);
1471
+ this.emit(`secure: ${port} === 465,`);
1472
+ this.emit(`auth: { user: ${user}, pass: ${pass} },`);
1473
+ this.indent--;
1474
+ this.emit(`});`);
1475
+ this.emitRaw('');
1476
+ }
1477
+
1478
+ // ===== GraphQL =====
1479
+
1480
+ visitGraphql(appName, node) {
1481
+ const path = this.stringValue(node.path);
1482
+
1483
+ this.emit(`import { buildSchema } from 'graphql';`);
1484
+ this.emit(`import { createHandler } from 'graphql-http/lib/use/express';`);
1485
+ this.emitRaw('');
1486
+
1487
+ const schemaNames = [...this.schemas.keys()];
1488
+ if (schemaNames.length > 0) {
1489
+ let schemaDef = '';
1490
+ for (const name of schemaNames) {
1491
+ const schema = this.schemas.get(name);
1492
+ const fields = schema.fields.map(f => {
1493
+ const gqlType = this.toGraphqlType(f.type);
1494
+ return ` ${f.name}: ${gqlType}`;
1495
+ }).join('\\n');
1496
+ schemaDef += `type ${name} {\\n${fields}\\n}\\n`;
1497
+ }
1498
+ schemaDef += `type Query {\\n`;
1499
+ for (const name of schemaNames) {
1500
+ schemaDef += ` ${name.toLowerCase()}s: [${name}]\\n`;
1501
+ schemaDef += ` ${name.toLowerCase()}(id: ID): ${name}\\n`;
1502
+ }
1503
+ schemaDef += `}`;
1504
+
1505
+ this.emit(`const __graphqlSchema = buildSchema(\`${schemaDef}\`);`);
1506
+ this.emit(`const __graphqlRoot = {`);
1507
+ this.indent++;
1508
+ for (const name of schemaNames) {
1509
+ this.emit(`${name.toLowerCase()}s: () => ${name}Store.getAll(),`);
1510
+ this.emit(`${name.toLowerCase()}: ({ id }) => ${name}Store.getById(id),`);
1511
+ }
1512
+ this.indent--;
1513
+ this.emit(`};`);
1514
+ } else {
1515
+ this.emit(`const __graphqlSchema = buildSchema(\`type Query { hello: String }\`);`);
1516
+ this.emit(`const __graphqlRoot = { hello: () => 'Hello from NAIDE GraphQL' };`);
1517
+ }
1518
+
1519
+ this.emit(`${appName}.all(${path}, createHandler({ schema: __graphqlSchema, rootValue: __graphqlRoot }));`);
1520
+ this.emitRaw('');
1521
+ }
1522
+
1523
+ toGraphqlType(naideType) {
1524
+ const map = { 'str': 'String', 'int': 'Int', 'num': 'Float', 'bool': 'Boolean', 'auto': 'ID', 'timestamp': 'String', 'enum': 'String' };
1525
+ return map[naideType] || 'String';
1526
+ }
1527
+
1528
+ // ===== Desktop (Electron) =====
1529
+
1530
+ visitDesktop(node) {
1531
+ const title = node.title ? this.rawPageString(node.title) : node.name;
1532
+ const width = node.width ? this.expr(node.width) : '800';
1533
+ const height = node.height ? this.expr(node.height) : '600';
1534
+ const load = node.load ? this.expr(node.load) : '"index.html"';
1535
+
1536
+ this.emit(`import { app, BrowserWindow } from 'electron';`);
1537
+ this.emitRaw('');
1538
+ this.emit(`function createWindow() {`);
1539
+ this.indent++;
1540
+ this.emit(`const ${node.name} = new BrowserWindow({`);
1541
+ this.indent++;
1542
+ this.emit(`width: ${width},`);
1543
+ this.emit(`height: ${height},`);
1544
+ this.emit(`title: ${JSON.stringify(title)},`);
1545
+ this.emit(`webPreferences: { nodeIntegration: true, contextIsolation: false },`);
1546
+ this.indent--;
1547
+ this.emit(`});`);
1548
+ this.emit(`${node.name}.loadFile(${load});`);
1549
+ this.indent--;
1550
+ this.emit(`}`);
1551
+ this.emitRaw('');
1552
+ this.emit(`app.whenReady().then(createWindow);`);
1553
+ this.emit(`app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });`);
1554
+ this.emit(`app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });`);
1555
+ this.emitRaw('');
1556
+ }
1557
+
1558
+ // ===== Screen (React Native) =====
1559
+
1560
+ visitScreen(node) {
1561
+ const imports = new Set(['View', 'StyleSheet']);
1562
+ const elements = [];
1563
+
1564
+ for (const el of node.elements) {
1565
+ const tag = el.tag;
1566
+ const arg0 = el.args[0] ? this.rawPageString(el.args[0]) : '';
1567
+ if (tag === 'text') { imports.add('Text'); elements.push({ jsx: `<Text>${arg0}</Text>` }); }
1568
+ else if (tag === 'button') { imports.add('Button'); elements.push({ jsx: `<Button title="${arg0}" onPress={() => { ${this.screenBodyToJs(el.body)} }} />` }); }
1569
+ else if (tag === 'input') { imports.add('TextInput'); elements.push({ jsx: `<TextInput placeholder="${arg0}" style={styles.input} />` }); }
1570
+ else if (tag === 'image') { imports.add('Image'); elements.push({ jsx: `<Image source={require('${arg0}')} style={styles.image} />` }); }
1571
+ else { imports.add('Text'); elements.push({ jsx: `<Text>${arg0}</Text>` }); }
1572
+ }
1573
+
1574
+ this.emit(`import React from 'react';`);
1575
+ this.emit(`import { ${[...imports].join(', ')} } from 'react-native';`);
1576
+ this.emitRaw('');
1577
+ this.emit(`export function ${node.name}() {`);
1578
+ this.indent++;
1579
+ this.emit(`return (`);
1580
+ this.indent++;
1581
+ this.emit(`<View style={styles.container}>`);
1582
+ this.indent++;
1583
+ for (const el of elements) this.emit(el.jsx);
1584
+ this.indent--;
1585
+ this.emit(`</View>`);
1586
+ this.indent--;
1587
+ this.emit(`);`);
1588
+ this.indent--;
1589
+ this.emit(`}`);
1590
+ this.emitRaw('');
1591
+ this.emit(`const styles = StyleSheet.create({`);
1592
+ this.indent++;
1593
+ this.emit(`container: { flex: 1, padding: 16, justifyContent: 'center' },`);
1594
+ this.emit(`input: { borderWidth: 1, borderColor: '#ccc', padding: 8, marginVertical: 4, borderRadius: 4 },`);
1595
+ this.emit(`image: { width: 200, height: 200, alignSelf: 'center' },`);
1596
+ this.indent--;
1597
+ this.emit(`});`);
1598
+ this.emitRaw('');
1599
+ }
1600
+
1601
+ screenBodyToJs(body) {
1602
+ if (!body || body.length === 0) return '';
1603
+ const saved = this.output;
1604
+ const savedIndent = this.indent;
1605
+ this.output = [];
1606
+ this.indent = 0;
1607
+ for (const stmt of body) this.visitStatement(stmt);
1608
+ const result = this.output.join(' ');
1609
+ this.output = saved;
1610
+ this.indent = savedIndent;
1611
+ return result;
1612
+ }
1613
+
1193
1614
  // ===== Expression generation =====
1194
1615
 
1195
1616
  expr(node) {