naider 1.12.0 → 1.13.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 CHANGED
@@ -676,6 +676,57 @@ watch User.create (event):
676
676
  Intervals: `"30s"`, `"5m"`, `"1h"`, `"1d"`. Cron expressions auto-detected.
677
677
  `watch` connects to `crud` events automatically.
678
678
 
679
+ ## OAuth (Social Login)
680
+
681
+ ```python
682
+ oauth "google" env.CLIENT_ID env.CLIENT_SECRET:
683
+ callback "/auth/callback"
684
+ scope "email profile"
685
+ ```
686
+
687
+ Supports Google, GitHub. Compiles to passport.js (Node.js) or authlib (Python).
688
+
689
+ ## Payment (Stripe)
690
+
691
+ ```python
692
+ pay "stripe" env.STRIPE_KEY:
693
+ webhook "/webhook"
694
+ ```
695
+
696
+ Usage: `pay.checkout([{name: "Item", price: 1000, qty: 1}])`. Compiles to Stripe SDK.
697
+
698
+ ## Cloud Storage (S3/GCS)
699
+
700
+ ```python
701
+ storage "s3" env.BUCKET env.AWS_KEY env.AWS_SECRET:
702
+ region "ap-northeast-1"
703
+ ```
704
+
705
+ Usage: `storage.upload("key", data)`, `storage.download("key")`, `storage.remove("key")`. Compiles to AWS SDK (Node.js) or boto3 (Python).
706
+
707
+ ## PDF Generation
708
+
709
+ ```python
710
+ pdf "report.pdf":
711
+ title "Monthly Report"
712
+ h1 "Summary"
713
+ text "This is the content."
714
+ image "chart.png"
715
+ ```
716
+
717
+ Compiles to pdfkit (Node.js) or FPDF (Python).
718
+
719
+ ## i18n (Internationalization)
720
+
721
+ ```python
722
+ i18n "locales/":
723
+ default "en"
724
+ lang "en" "en.json"
725
+ lang "ja" "ja.json"
726
+ ```
727
+
728
+ Usage: `i18n.t("greeting.hello")`, `i18n.setLang("ja")`.
729
+
679
730
  ## Environment Variables
680
731
 
681
732
  ```python
package/SPEC.naide CHANGED
@@ -520,3 +520,47 @@ screen Home:
520
520
  button "Click Me"
521
521
  input "Enter your name"
522
522
  image "logo.png"
523
+
524
+
525
+ # ---- OAuth (ソーシャルログイン) ----
526
+ oauth "google" env.CLIENT_ID env.CLIENT_SECRET:
527
+ callback "/auth/callback"
528
+ scope "email profile"
529
+
530
+ oauth "github" env.GH_ID env.GH_SECRET:
531
+ callback "/auth/github/callback"
532
+ scope "user:email"
533
+
534
+
535
+ # ---- 決済 (Stripe) ----
536
+ pay "stripe" env.STRIPE_KEY:
537
+ webhook "/webhook"
538
+ success "/thanks"
539
+ cancel "/cancel"
540
+ # usage: pay.checkout([{name: "Item", price: 1000, qty: 1}])
541
+
542
+
543
+ # ---- ファイルストレージ (S3/GCS) ----
544
+ storage "s3" env.BUCKET env.AWS_KEY env.AWS_SECRET:
545
+ region "ap-northeast-1"
546
+ # usage: storage.upload("key", data)
547
+ # usage: storage.download("key")
548
+ # usage: storage.remove("key")
549
+
550
+
551
+ # ---- PDF生成 ----
552
+ pdf "report.pdf":
553
+ title "Monthly Report"
554
+ h1 "Summary"
555
+ text "This is the report content."
556
+ line
557
+ image "chart.png"
558
+
559
+
560
+ # ---- i18n (多言語) ----
561
+ i18n "locales/":
562
+ default "en"
563
+ lang "en" "en.json"
564
+ lang "ja" "ja.json"
565
+ # usage: i18n.t("greeting.hello")
566
+ # usage: i18n.setLang("ja")
package/lsp/server.js CHANGED
@@ -289,6 +289,7 @@ function getCompletions() {
289
289
  'cors', 'auth', 'crud', 'limit', 'cookie', 'session', 'static', 'ws', 'sse',
290
290
  'cache', 'view', 'upload', 'group', 'validate', 'openapi', 'error', 'mid', 'prompt',
291
291
  'page', 'cli', 'mail', 'graphql', 'desktop', 'screen',
292
+ 'oauth', 'pay', 'storage', 'pdf', 'i18n',
292
293
  ];
293
294
  const builtins = [
294
295
  { label: 'uuid()', detail: 'Generate UUID v4', insertText: 'uuid()' },
@@ -345,6 +346,11 @@ const HOVER_DOCS = {
345
346
  'desktop': '**desktop** — Desktop app (Electron/pywebview)\n```naide\ndesktop myApp:\n title "My App"\n size 1024 768\n load "index.html"\n```',
346
347
  'screen': '**screen** — Mobile screen (React Native/Kivy)\n```naide\nscreen Home:\n text "Hello World"\n button "Click Me"\n input "Enter name"\n```',
347
348
  'every': '**every** — Scheduled task / cron\n```naide\nevery "5s":\n log "tick"\nevery "*/5 * * * *":\n log "cron"\n```',
349
+ 'oauth': '**oauth** — Social login (Google/GitHub)\n```naide\noauth "google" env.CLIENT_ID env.CLIENT_SECRET:\n callback "/auth/callback"\n scope "email profile"\n```',
350
+ 'pay': '**pay** — Payment (Stripe)\n```naide\npay "stripe" env.STRIPE_KEY:\n webhook "/webhook"\n```\nUsage: `pay.checkout(items, successUrl, cancelUrl)`',
351
+ 'storage': '**storage** — Cloud storage (S3/GCS)\n```naide\nstorage "s3" env.BUCKET env.KEY env.SECRET:\n region "ap-northeast-1"\n```\nUsage: `storage.upload(key, body)`, `storage.download(key)`',
352
+ 'pdf': '**pdf** — PDF generation\n```naide\npdf "report.pdf":\n title "Report"\n text "Hello"\n```',
353
+ 'i18n': '**i18n** — Internationalization\n```naide\ni18n "locales/":\n default "en"\n lang "en" "en.json"\n lang "ja" "ja.json"\n```\nUsage: `i18n.t("key")`, `i18n.setLang("ja")`',
348
354
  };
349
355
 
350
356
  function getHover(params) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naider",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "NAIDE - Node AI Development Environment. AI-specialized language with built-in server, auth, DB, SQL, AI/LLM, WebSocket, bots (Discord/Slack/Telegram/LINE), CLI, HTML, email, GraphQL, desktop, mobile, and more — transpiles to Node.js/Python/Bun.",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -148,6 +148,11 @@ export class PythonGenerator {
148
148
  case 'MailConfig': return this.visitMail(node);
149
149
  case 'DesktopApp': return this.visitDesktop(node);
150
150
  case 'Screen': return this.visitScreen(node);
151
+ case 'OauthDecl': return this.visitOauth(node);
152
+ case 'PayDecl': return this.visitPay(node);
153
+ case 'StorageDecl': return this.visitStorage(node);
154
+ case 'PdfDecl': return this.visitPdf(node);
155
+ case 'I18nDecl': return this.visitI18n(node);
151
156
  default:
152
157
  this.emit(`# unknown: ${node.type}`);
153
158
  }
@@ -1955,4 +1960,190 @@ export class PythonGenerator {
1955
1960
  this.indent--;
1956
1961
  this.emitRaw('');
1957
1962
  }
1963
+
1964
+ // ===== OAuth =====
1965
+ visitOauth(node) {
1966
+ const provider = this.rawString(node.provider);
1967
+ const clientId = this.expr(node.clientId);
1968
+ const clientSecret = this.expr(node.clientSecret);
1969
+ const callback = node.callback ? this.rawString(node.callback) : '/auth/callback';
1970
+ const scope = node.scope ? this.rawString(node.scope) : 'email profile';
1971
+
1972
+ if (provider === 'google') {
1973
+ this.addFromImport('authlib.integrations.flask_client', 'OAuth');
1974
+ } else if (provider === 'github') {
1975
+ this.addFromImport('authlib.integrations.flask_client', 'OAuth');
1976
+ }
1977
+ this.emitRaw('');
1978
+ this.emit(`oauth = OAuth()`);
1979
+ this.emit(`oauth.register(`);
1980
+ this.indent++;
1981
+ this.emit(`name=${JSON.stringify(provider)},`);
1982
+ this.emit(`client_id=${clientId},`);
1983
+ this.emit(`client_secret=${clientSecret},`);
1984
+ if (provider === 'google') {
1985
+ this.emit(`server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',`);
1986
+ this.emit(`client_kwargs={'scope': ${JSON.stringify(scope)}},`);
1987
+ } else if (provider === 'github') {
1988
+ this.emit(`access_token_url='https://github.com/login/oauth/access_token',`);
1989
+ this.emit(`authorize_url='https://github.com/login/oauth/authorize',`);
1990
+ this.emit(`client_kwargs={'scope': ${JSON.stringify(scope)}},`);
1991
+ }
1992
+ this.indent--;
1993
+ this.emit(`)`);
1994
+ this.emitRaw('');
1995
+ }
1996
+
1997
+ // ===== Pay =====
1998
+ visitPay(node) {
1999
+ const provider = this.rawString(node.provider);
2000
+ const secretKey = this.expr(node.secretKey);
2001
+
2002
+ if (provider === 'stripe') {
2003
+ this.addImport('stripe');
2004
+ this.emitRaw('');
2005
+ this.emit(`stripe.api_key = ${secretKey}`);
2006
+ this.emitRaw('');
2007
+ this.emit(`class Pay:`);
2008
+ this.indent++;
2009
+ this.emit(`@staticmethod`);
2010
+ this.emit(`def checkout(items, success_url=None, cancel_url=None):`);
2011
+ this.indent++;
2012
+ this.emit(`line_items = [{'price_data': {'currency': 'usd', 'product_data': {'name': i['name']}, 'unit_amount': i['price']}, 'quantity': i.get('qty', 1)} for i in items]`);
2013
+ this.emit(`return stripe.checkout.Session.create(line_items=line_items, mode='payment', success_url=success_url or 'http://localhost:3000/success', cancel_url=cancel_url or 'http://localhost:3000/cancel')`);
2014
+ this.indent--;
2015
+ this.indent--;
2016
+ this.emit(`pay = Pay()`);
2017
+ } else {
2018
+ this.emit(`# ${provider} payment integration`);
2019
+ this.emit(`pay = None`);
2020
+ }
2021
+ this.emitRaw('');
2022
+ }
2023
+
2024
+ // ===== Storage =====
2025
+ visitStorage(node) {
2026
+ const provider = this.rawString(node.provider);
2027
+ const bucket = this.expr(node.bucket);
2028
+ const accessKey = this.expr(node.accessKey);
2029
+ const secretKey = this.expr(node.secretKey);
2030
+ const region = node.region ? this.rawString(node.region) : 'us-east-1';
2031
+
2032
+ if (provider === 's3') {
2033
+ this.addImport('boto3');
2034
+ this.emitRaw('');
2035
+ this.emit(`__s3 = boto3.client('s3', region_name=${JSON.stringify(region)}, aws_access_key_id=${accessKey}, aws_secret_access_key=${secretKey})`);
2036
+ this.emitRaw('');
2037
+ this.emit(`class Storage:`);
2038
+ this.indent++;
2039
+ this.emit(`@staticmethod`);
2040
+ this.emit(`def upload(key, body):`);
2041
+ this.indent++;
2042
+ this.emit(`__s3.put_object(Bucket=${bucket}, Key=key, Body=body)`);
2043
+ this.indent--;
2044
+ this.emit(`@staticmethod`);
2045
+ this.emit(`def download(key):`);
2046
+ this.indent++;
2047
+ this.emit(`return __s3.get_object(Bucket=${bucket}, Key=key)['Body'].read()`);
2048
+ this.indent--;
2049
+ this.emit(`@staticmethod`);
2050
+ this.emit(`def remove(key):`);
2051
+ this.indent++;
2052
+ this.emit(`__s3.delete_object(Bucket=${bucket}, Key=key)`);
2053
+ this.indent--;
2054
+ this.indent--;
2055
+ this.emit(`storage = Storage()`);
2056
+ } else if (provider === 'gcs') {
2057
+ this.addFromImport('google.cloud', 'storage as gcs_storage');
2058
+ this.emit(`__gcs = gcs_storage.Client()`);
2059
+ this.emit(`__bucket = __gcs.bucket(${bucket})`);
2060
+ this.emit(`class Storage:`);
2061
+ this.indent++;
2062
+ this.emit(`@staticmethod`);
2063
+ this.emit(`def upload(key, body): __bucket.blob(key).upload_from_string(body)`);
2064
+ this.emit(`@staticmethod`);
2065
+ this.emit(`def download(key): return __bucket.blob(key).download_as_bytes()`);
2066
+ this.emit(`@staticmethod`);
2067
+ this.emit(`def remove(key): __bucket.blob(key).delete()`);
2068
+ this.indent--;
2069
+ this.emit(`storage = Storage()`);
2070
+ }
2071
+ this.emitRaw('');
2072
+ }
2073
+
2074
+ // ===== PDF =====
2075
+ visitPdf(node) {
2076
+ const filename = this.rawString(node.filename);
2077
+ this.addFromImport('fpdf', 'FPDF');
2078
+ this.emitRaw('');
2079
+ this.emit(`__pdf = FPDF()`);
2080
+ this.emit(`__pdf.add_page()`);
2081
+ this.emit(`__pdf.set_auto_page_break(auto=True, margin=15)`);
2082
+
2083
+ for (const el of node.elements) {
2084
+ const tag = el.tag;
2085
+ const arg0 = el.args[0] ? this.rawString(el.args[0]) : '';
2086
+ if (tag === 'title') {
2087
+ this.emit(`__pdf.set_font('Helvetica', 'B', 24)`);
2088
+ this.emit(`__pdf.cell(0, 15, ${JSON.stringify(arg0)}, ln=True)`);
2089
+ } else if (tag === 'h1' || tag === 'h2' || tag === 'heading') {
2090
+ const size = tag === 'h1' ? 20 : 16;
2091
+ this.emit(`__pdf.set_font('Helvetica', 'B', ${size})`);
2092
+ this.emit(`__pdf.cell(0, 12, ${JSON.stringify(arg0)}, ln=True)`);
2093
+ } else if (tag === 'text' || tag === 'p') {
2094
+ this.emit(`__pdf.set_font('Helvetica', '', 12)`);
2095
+ this.emit(`__pdf.multi_cell(0, 8, ${JSON.stringify(arg0)})`);
2096
+ } else if (tag === 'image') {
2097
+ this.emit(`__pdf.image(${JSON.stringify(arg0)}, w=100)`);
2098
+ } else if (tag === 'line') {
2099
+ this.emit(`__pdf.line(10, __pdf.get_y(), 200, __pdf.get_y())`);
2100
+ } else {
2101
+ this.emit(`__pdf.set_font('Helvetica', '', 12)`);
2102
+ this.emit(`__pdf.cell(0, 8, ${JSON.stringify(arg0)}, ln=True)`);
2103
+ }
2104
+ }
2105
+
2106
+ this.emit(`__pdf.output(${JSON.stringify(filename)})`);
2107
+ this.emit(`print(f"Generated: ${filename}")`);
2108
+ this.emitRaw('');
2109
+ }
2110
+
2111
+ // ===== i18n =====
2112
+ visitI18n(node) {
2113
+ const dir = this.rawString(node.dir);
2114
+ const defaultLang = node.defaultLang ? this.rawString(node.defaultLang) : 'en';
2115
+
2116
+ this.addImport('json');
2117
+ this.addImport('os');
2118
+ this.emitRaw('');
2119
+ this.emit(`__i18n_data = {}`);
2120
+ for (const lang of node.langs) {
2121
+ const code = this.rawString(lang.code);
2122
+ const file = this.rawString(lang.file);
2123
+ this.emit(`with open(os.path.join(${JSON.stringify(dir)}, ${JSON.stringify(file)})) as f:`);
2124
+ this.indent++;
2125
+ this.emit(`__i18n_data[${JSON.stringify(code)}] = json.load(f)`);
2126
+ this.indent--;
2127
+ }
2128
+ this.emit(`__i18n_lang = ${JSON.stringify(defaultLang)}`);
2129
+ this.emitRaw('');
2130
+ this.emit(`class I18n:`);
2131
+ this.indent++;
2132
+ this.emit(`@staticmethod`);
2133
+ this.emit(`def t(key, **params):`);
2134
+ this.indent++;
2135
+ this.emit(`data = __i18n_data.get(__i18n_lang, {})`);
2136
+ this.emit(`for k in key.split('.'): data = data.get(k, key) if isinstance(data, dict) else key`);
2137
+ this.emit(`text = str(data)`);
2138
+ this.emit(`for k, v in params.items(): text = text.replace('{' + k + '}', str(v))`);
2139
+ this.emit(`return text`);
2140
+ this.indent--;
2141
+ this.emit(`@staticmethod`);
2142
+ this.emit(`def set_lang(code): global __i18n_lang; __i18n_lang = code`);
2143
+ this.emit(`@staticmethod`);
2144
+ this.emit(`def get_lang(): return __i18n_lang`);
2145
+ this.indent--;
2146
+ this.emit(`i18n = I18n()`);
2147
+ this.emitRaw('');
2148
+ }
1958
2149
  }
package/src/generator.js CHANGED
@@ -148,6 +148,11 @@ export class Generator {
148
148
  case 'MailConfig': return this.visitMail(node);
149
149
  case 'DesktopApp': return this.visitDesktop(node);
150
150
  case 'Screen': return this.visitScreen(node);
151
+ case 'OauthDecl': return this.visitOauth(node);
152
+ case 'PayDecl': return this.visitPay(node);
153
+ case 'StorageDecl': return this.visitStorage(node);
154
+ case 'PdfDecl': return this.visitPdf(node);
155
+ case 'I18nDecl': return this.visitI18n(node);
151
156
  default:
152
157
  this.emit(`/* unknown: ${node.type} */`);
153
158
  }
@@ -1848,4 +1853,213 @@ export class Generator {
1848
1853
  this.indent = savedIndent;
1849
1854
  return result;
1850
1855
  }
1856
+
1857
+ // ===== OAuth =====
1858
+
1859
+ visitOauth(node) {
1860
+ const provider = this.rawPageString(node.provider);
1861
+ const clientId = this.expr(node.clientId);
1862
+ const clientSecret = this.expr(node.clientSecret);
1863
+ const callback = node.callback ? this.rawPageString(node.callback) : '/auth/callback';
1864
+ const scope = node.scope ? this.rawPageString(node.scope) : 'email profile';
1865
+
1866
+ if (provider === 'google') {
1867
+ this.emit(`import passport from 'passport';`);
1868
+ this.emit(`import { Strategy as GoogleStrategy } from 'passport-google-oauth20';`);
1869
+ this.emitRaw('');
1870
+ this.emit(`passport.use(new GoogleStrategy({`);
1871
+ this.indent++;
1872
+ this.emit(`clientID: ${clientId},`);
1873
+ this.emit(`clientSecret: ${clientSecret},`);
1874
+ this.emit(`callbackURL: ${JSON.stringify(callback)},`);
1875
+ this.indent--;
1876
+ this.emit(`}, (accessToken, refreshToken, profile, done) => done(null, profile)));`);
1877
+ } else if (provider === 'github') {
1878
+ this.emit(`import passport from 'passport';`);
1879
+ this.emit(`import { Strategy as GitHubStrategy } from 'passport-github2';`);
1880
+ this.emitRaw('');
1881
+ this.emit(`passport.use(new GitHubStrategy({`);
1882
+ this.indent++;
1883
+ this.emit(`clientID: ${clientId},`);
1884
+ this.emit(`clientSecret: ${clientSecret},`);
1885
+ this.emit(`callbackURL: ${JSON.stringify(callback)},`);
1886
+ this.indent--;
1887
+ this.emit(`}, (accessToken, refreshToken, profile, done) => done(null, profile)));`);
1888
+ } else {
1889
+ this.emit(`import passport from 'passport';`);
1890
+ this.emit(`// Configure ${provider} OAuth strategy`);
1891
+ }
1892
+
1893
+ this.emit(`passport.serializeUser((user, done) => done(null, user));`);
1894
+ this.emit(`passport.deserializeUser((user, done) => done(null, user));`);
1895
+ this.emitRaw('');
1896
+ }
1897
+
1898
+ // ===== Pay (Stripe) =====
1899
+
1900
+ visitPay(node) {
1901
+ const provider = this.rawPageString(node.provider);
1902
+ const secretKey = this.expr(node.secretKey);
1903
+ const webhook = node.webhook ? this.rawPageString(node.webhook) : '/webhook';
1904
+
1905
+ if (provider === 'stripe') {
1906
+ this.emit(`import Stripe from 'stripe';`);
1907
+ this.emit(`const stripe = new Stripe(${secretKey});`);
1908
+ this.emitRaw('');
1909
+ this.emit(`const pay = {`);
1910
+ this.indent++;
1911
+ this.emit(`async checkout(items, successUrl, cancelUrl) {`);
1912
+ this.indent++;
1913
+ this.emit(`return stripe.checkout.sessions.create({`);
1914
+ this.indent++;
1915
+ this.emit(`line_items: items.map(i => ({ price_data: { currency: 'usd', product_data: { name: i.name }, unit_amount: i.price }, quantity: i.qty || 1 })),`);
1916
+ this.emit(`mode: 'payment',`);
1917
+ this.emit(`success_url: successUrl || ${node.successUrl ? this.expr(node.successUrl) : "'http://localhost:3000/success'"},`);
1918
+ this.emit(`cancel_url: cancelUrl || ${node.cancelUrl ? this.expr(node.cancelUrl) : "'http://localhost:3000/cancel'"},`);
1919
+ this.indent--;
1920
+ this.emit(`});`);
1921
+ this.indent--;
1922
+ this.emit(`},`);
1923
+ this.emit(`async verify(body, sig) {`);
1924
+ this.indent++;
1925
+ this.emit(`return stripe.webhooks.constructEvent(body, sig, ${secretKey});`);
1926
+ this.indent--;
1927
+ this.emit(`},`);
1928
+ this.indent--;
1929
+ this.emit(`};`);
1930
+ } else {
1931
+ this.emit(`// ${provider} payment integration`);
1932
+ this.emit(`const pay = {};`);
1933
+ }
1934
+ this.emitRaw('');
1935
+ }
1936
+
1937
+ // ===== Storage (S3) =====
1938
+
1939
+ visitStorage(node) {
1940
+ const provider = this.rawPageString(node.provider);
1941
+ const bucket = this.expr(node.bucket);
1942
+ const accessKey = this.expr(node.accessKey);
1943
+ const secretKey = this.expr(node.secretKey);
1944
+ const region = node.region ? this.rawPageString(node.region) : 'us-east-1';
1945
+
1946
+ if (provider === 's3') {
1947
+ this.emit(`import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';`);
1948
+ this.emitRaw('');
1949
+ this.emit(`const __s3 = new S3Client({`);
1950
+ this.indent++;
1951
+ this.emit(`region: ${JSON.stringify(region)},`);
1952
+ this.emit(`credentials: { accessKeyId: ${accessKey}, secretAccessKey: ${secretKey} },`);
1953
+ this.indent--;
1954
+ this.emit(`});`);
1955
+ this.emitRaw('');
1956
+ this.emit(`const storage = {`);
1957
+ this.indent++;
1958
+ this.emit(`async upload(key, body, contentType = 'application/octet-stream') {`);
1959
+ this.indent++;
1960
+ this.emit(`return __s3.send(new PutObjectCommand({ Bucket: ${bucket}, Key: key, Body: body, ContentType: contentType }));`);
1961
+ this.indent--;
1962
+ this.emit(`},`);
1963
+ this.emit(`async download(key) {`);
1964
+ this.indent++;
1965
+ this.emit(`const res = await __s3.send(new GetObjectCommand({ Bucket: ${bucket}, Key: key }));`);
1966
+ this.emit(`return res.Body;`);
1967
+ this.indent--;
1968
+ this.emit(`},`);
1969
+ this.emit(`async remove(key) {`);
1970
+ this.indent++;
1971
+ this.emit(`return __s3.send(new DeleteObjectCommand({ Bucket: ${bucket}, Key: key }));`);
1972
+ this.indent--;
1973
+ this.emit(`},`);
1974
+ this.indent--;
1975
+ this.emit(`};`);
1976
+ } else if (provider === 'gcs') {
1977
+ this.emit(`import { Storage } from '@google-cloud/storage';`);
1978
+ this.emit(`const __gcs = new Storage();`);
1979
+ this.emit(`const __bucket = __gcs.bucket(${bucket});`);
1980
+ this.emit(`const storage = {`);
1981
+ this.indent++;
1982
+ this.emit(`async upload(key, body) { await __bucket.file(key).save(body); },`);
1983
+ this.emit(`async download(key) { const [buf] = await __bucket.file(key).download(); return buf; },`);
1984
+ this.emit(`async remove(key) { await __bucket.file(key).delete(); },`);
1985
+ this.indent--;
1986
+ this.emit(`};`);
1987
+ } else {
1988
+ this.emit(`// ${provider} storage integration`);
1989
+ this.emit(`const storage = {};`);
1990
+ }
1991
+ this.emitRaw('');
1992
+ }
1993
+
1994
+ // ===== PDF =====
1995
+
1996
+ visitPdf(node) {
1997
+ const filename = this.rawPageString(node.filename);
1998
+ this.emit(`import PDFDocument from 'pdfkit';`);
1999
+ this.emit(`import { createWriteStream } from 'fs';`);
2000
+ this.emitRaw('');
2001
+ this.emit(`const __pdf = new PDFDocument();`);
2002
+ this.emit(`__pdf.pipe(createWriteStream(${JSON.stringify(filename)}));`);
2003
+
2004
+ for (const el of node.elements) {
2005
+ const tag = el.tag;
2006
+ const arg0 = el.args[0] ? (el.args[0].raw !== undefined ? JSON.stringify(el.args[0].raw || el.args[0].parts?.map(p => p.value).join('')) : this.expr(el.args[0])) : '""';
2007
+ if (tag === 'title') {
2008
+ this.emit(`__pdf.fontSize(24).text(${arg0});`);
2009
+ this.emit(`__pdf.moveDown();`);
2010
+ } else if (tag === 'heading' || tag === 'h1' || tag === 'h2') {
2011
+ const size = tag === 'h1' ? 20 : 16;
2012
+ this.emit(`__pdf.fontSize(${size}).text(${arg0});`);
2013
+ this.emit(`__pdf.moveDown();`);
2014
+ } else if (tag === 'text' || tag === 'p') {
2015
+ this.emit(`__pdf.fontSize(12).text(${arg0});`);
2016
+ } else if (tag === 'image') {
2017
+ this.emit(`__pdf.image(${arg0}, { width: 300 });`);
2018
+ } else if (tag === 'line') {
2019
+ this.emit(`__pdf.moveTo(50, __pdf.y).lineTo(550, __pdf.y).stroke();`);
2020
+ this.emit(`__pdf.moveDown();`);
2021
+ } else if (tag === 'table') {
2022
+ this.emit(`// table: ${arg0}`);
2023
+ } else {
2024
+ this.emit(`__pdf.text(${arg0});`);
2025
+ }
2026
+ }
2027
+
2028
+ this.emit(`__pdf.end();`);
2029
+ this.emit(`console.log('Generated: ${filename}');`);
2030
+ this.emitRaw('');
2031
+ }
2032
+
2033
+ // ===== i18n =====
2034
+
2035
+ visitI18n(node) {
2036
+ const dir = this.rawPageString(node.dir);
2037
+ const defaultLang = node.defaultLang ? this.rawPageString(node.defaultLang) : 'en';
2038
+
2039
+ this.emit(`import { readFileSync } from 'fs';`);
2040
+ this.emit(`import { join } from 'path';`);
2041
+ this.emitRaw('');
2042
+ this.emit(`const __i18nData = {};`);
2043
+ for (const lang of node.langs) {
2044
+ const code = this.rawPageString(lang.code);
2045
+ const file = this.rawPageString(lang.file);
2046
+ this.emit(`__i18nData[${JSON.stringify(code)}] = JSON.parse(readFileSync(join(${JSON.stringify(dir)}, ${JSON.stringify(file)}), 'utf-8'));`);
2047
+ }
2048
+ this.emit(`let __i18nLang = ${JSON.stringify(defaultLang)};`);
2049
+ this.emitRaw('');
2050
+ this.emit(`const i18n = {`);
2051
+ this.indent++;
2052
+ this.emit(`t(key, params = {}) {`);
2053
+ this.indent++;
2054
+ this.emit(`let text = key.split('.').reduce((o, k) => o?.[k], __i18nData[__i18nLang]) || key;`);
2055
+ this.emit(`for (const [k, v] of Object.entries(params)) text = text.replace(new RegExp(\`{$\{k}}\`, 'g'), v);`);
2056
+ this.emit(`return text;`);
2057
+ this.indent--;
2058
+ this.emit(`},`);
2059
+ this.emit(`setLang(code) { __i18nLang = code; },`);
2060
+ this.emit(`getLang() { return __i18nLang; },`);
2061
+ this.indent--;
2062
+ this.emit(`};`);
2063
+ this.emitRaw('');
2064
+ }
1851
2065
  }
package/src/parser.js CHANGED
@@ -111,7 +111,9 @@ export class Parser {
111
111
  type === T.TYPEOF || type === T.INSTANCEOF || type === T.ENSURE ||
112
112
  type === T.MODEL || type === T.PROMPT ||
113
113
  type === T.PAGE || type === T.CLI_APP || type === T.MAIL ||
114
- type === T.GRAPHQL || type === T.DESKTOP || type === T.SCREEN;
114
+ type === T.GRAPHQL || type === T.DESKTOP || type === T.SCREEN ||
115
+ type === T.OAUTH || type === T.PAY || type === T.STORAGE ||
116
+ type === T.PDF || type === T.I18N;
115
117
  }
116
118
 
117
119
  expectPropertyName() {
@@ -173,6 +175,11 @@ export class Parser {
173
175
  case T.MAIL: return this.parseMail();
174
176
  case T.DESKTOP: return this.parseDesktop();
175
177
  case T.SCREEN: return this.parseScreen();
178
+ case T.OAUTH: return this.parseOauth();
179
+ case T.PAY: return this.parsePay();
180
+ case T.STORAGE: return this.parseStorage();
181
+ case T.PDF: return this.parsePdf();
182
+ case T.I18N: return this.parseI18n();
176
183
  case T.MODEL: return this.parseModel();
177
184
  case T.ON: return this.parseOn();
178
185
  case T.LOG: return this.parseLog();
@@ -1148,6 +1155,7 @@ export class Parser {
1148
1155
  case T.PROMPT:
1149
1156
  case T.PAGE: case T.CLI_APP: case T.MAIL:
1150
1157
  case T.GRAPHQL: case T.DESKTOP: case T.SCREEN:
1158
+ case T.OAUTH: case T.PAY: case T.STORAGE: case T.PDF: case T.I18N:
1151
1159
  case T.FROM: case T.AS: case T.IN:
1152
1160
  this.advance();
1153
1161
  return new ASTNode('Identifier', { name: tok.value });
@@ -1935,4 +1943,129 @@ export class Parser {
1935
1943
  }
1936
1944
  return new ASTNode('ScreenElement', { tag, args, body });
1937
1945
  }
1946
+
1947
+ // oauth "google" clientId clientSecret:
1948
+ // callback "/auth/callback"
1949
+ // scope "email profile"
1950
+ parseOauth() {
1951
+ this.expect(T.OAUTH);
1952
+ const provider = this.parseString();
1953
+ const clientId = this.parseExpression();
1954
+ const clientSecret = this.parseExpression();
1955
+ this.expect(T.COLON);
1956
+ this.skipNewlines();
1957
+ this.expect(T.INDENT);
1958
+ let callback = null, scope = null;
1959
+ while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
1960
+ this.skipNewlines();
1961
+ if (this.at(T.DEDENT) || this.at(T.EOF)) break;
1962
+ const kw = this.peek().value;
1963
+ if (kw === 'callback') { this.advance(); callback = this.parseString(); }
1964
+ else if (kw === 'scope') { this.advance(); scope = this.parseString(); }
1965
+ else { this.advance(); }
1966
+ this.skipNewlines();
1967
+ }
1968
+ if (this.at(T.DEDENT)) this.advance();
1969
+ return new ASTNode('OauthDecl', { provider, clientId, clientSecret, callback, scope });
1970
+ }
1971
+
1972
+ // pay "stripe" secretKey:
1973
+ // webhook "/webhook"
1974
+ parsePay() {
1975
+ this.expect(T.PAY);
1976
+ const provider = this.parseString();
1977
+ const secretKey = this.parseExpression();
1978
+ this.expect(T.COLON);
1979
+ this.skipNewlines();
1980
+ this.expect(T.INDENT);
1981
+ let webhook = null, successUrl = null, cancelUrl = null;
1982
+ while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
1983
+ this.skipNewlines();
1984
+ if (this.at(T.DEDENT) || this.at(T.EOF)) break;
1985
+ const kw = this.peek().value;
1986
+ if (kw === 'webhook') { this.advance(); webhook = this.parseString(); }
1987
+ else if (kw === 'success') { this.advance(); successUrl = this.parseString(); }
1988
+ else if (kw === 'cancel') { this.advance(); cancelUrl = this.parseString(); }
1989
+ else { this.advance(); }
1990
+ this.skipNewlines();
1991
+ }
1992
+ if (this.at(T.DEDENT)) this.advance();
1993
+ return new ASTNode('PayDecl', { provider, secretKey, webhook, successUrl, cancelUrl });
1994
+ }
1995
+
1996
+ // storage "s3" bucket accessKey secretKey:
1997
+ // region "ap-northeast-1"
1998
+ parseStorage() {
1999
+ this.expect(T.STORAGE);
2000
+ const provider = this.parseString();
2001
+ const bucket = this.parseExpression();
2002
+ const accessKey = this.parseExpression();
2003
+ const secretKey = this.parseExpression();
2004
+ this.expect(T.COLON);
2005
+ this.skipNewlines();
2006
+ this.expect(T.INDENT);
2007
+ let region = null;
2008
+ while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
2009
+ this.skipNewlines();
2010
+ if (this.at(T.DEDENT) || this.at(T.EOF)) break;
2011
+ const kw = this.peek().value;
2012
+ if (kw === 'region') { this.advance(); region = this.parseString(); }
2013
+ else { this.advance(); }
2014
+ this.skipNewlines();
2015
+ }
2016
+ if (this.at(T.DEDENT)) this.advance();
2017
+ return new ASTNode('StorageDecl', { provider, bucket, accessKey, secretKey, region });
2018
+ }
2019
+
2020
+ // pdf "output.pdf":
2021
+ // title "My Document"
2022
+ // text "Hello World"
2023
+ // table data
2024
+ parsePdf() {
2025
+ this.expect(T.PDF);
2026
+ const filename = this.parseString();
2027
+ this.expect(T.COLON);
2028
+ this.skipNewlines();
2029
+ this.expect(T.INDENT);
2030
+ const elements = [];
2031
+ while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
2032
+ this.skipNewlines();
2033
+ if (this.at(T.DEDENT) || this.at(T.EOF)) break;
2034
+ const tag = this.advance().value;
2035
+ const args = [];
2036
+ while (this.at(T.STRING)) args.push(this.parseString());
2037
+ if (args.length === 0 && !this.at(T.NEWLINE) && !this.at(T.DEDENT) && !this.at(T.EOF)) {
2038
+ args.push(this.parseExpression());
2039
+ }
2040
+ elements.push({ tag, args });
2041
+ this.skipNewlines();
2042
+ }
2043
+ if (this.at(T.DEDENT)) this.advance();
2044
+ return new ASTNode('PdfDecl', { filename, elements });
2045
+ }
2046
+
2047
+ // i18n "locales/":
2048
+ // default "en"
2049
+ // lang "ja" "jp.json"
2050
+ // lang "en" "en.json"
2051
+ parseI18n() {
2052
+ this.expect(T.I18N);
2053
+ const dir = this.parseString();
2054
+ this.expect(T.COLON);
2055
+ this.skipNewlines();
2056
+ this.expect(T.INDENT);
2057
+ let defaultLang = null;
2058
+ const langs = [];
2059
+ while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
2060
+ this.skipNewlines();
2061
+ if (this.at(T.DEDENT) || this.at(T.EOF)) break;
2062
+ const kw = this.peek().value;
2063
+ if (kw === 'default') { this.advance(); defaultLang = this.parseString(); }
2064
+ else if (kw === 'lang') { this.advance(); const code = this.parseString(); const file = this.parseString(); langs.push({ code, file }); }
2065
+ else { this.advance(); }
2066
+ this.skipNewlines();
2067
+ }
2068
+ if (this.at(T.DEDENT)) this.advance();
2069
+ return new ASTNode('I18nDecl', { dir, defaultLang, langs });
2070
+ }
1938
2071
  }
package/src/tokens.js CHANGED
@@ -93,6 +93,11 @@ export const T = {
93
93
  GRAPHQL: 'GRAPHQL',
94
94
  DESKTOP: 'DESKTOP',
95
95
  SCREEN: 'SCREEN',
96
+ OAUTH: 'OAUTH',
97
+ PAY: 'PAY',
98
+ STORAGE: 'STORAGE',
99
+ PDF: 'PDF',
100
+ I18N: 'I18N',
96
101
 
97
102
  // Operators
98
103
  ASSIGN: 'ASSIGN',
@@ -214,6 +219,11 @@ export const KEYWORDS = {
214
219
  'graphql': T.GRAPHQL,
215
220
  'desktop': T.DESKTOP,
216
221
  'screen': T.SCREEN,
222
+ 'oauth': T.OAUTH,
223
+ 'pay': T.PAY,
224
+ 'storage': T.STORAGE,
225
+ 'pdf': T.PDF,
226
+ 'i18n': T.I18N,
217
227
  'true': T.BOOL,
218
228
  'false': T.BOOL,
219
229
  'null': T.NULL,
@@ -87,7 +87,7 @@
87
87
  "name": "keyword.control.naide"
88
88
  },
89
89
  "keywords-server": {
90
- "match": "\\b(server|bot|slash|get|post|put|del|patch|mid|cors|limit|auth|crud|static|ws|sse|group|upload|cookie|session|view|cache|validate|openapi|error|on|page|cli|mail|graphql|desktop|screen)\\b",
90
+ "match": "\\b(server|bot|slash|get|post|put|del|patch|mid|cors|limit|auth|crud|static|ws|sse|group|upload|cookie|session|view|cache|validate|openapi|error|on|page|cli|mail|graphql|desktop|screen|oauth|pay|storage|pdf|i18n)\\b",
91
91
  "name": "keyword.other.naide"
92
92
  },
93
93
  "keywords-declaration": {