thyscript 1.0.0 โ†’ 2.0.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.
@@ -652,6 +652,36 @@ const __thy_sab = typeof SharedArrayBuffer !== 'undefined' ? new SharedArrayBuff
652
652
  this._expressAppDeclared = true;
653
653
  const route = node.route || `/${node.name}`;
654
654
  const name = (node.name || 'html_page').replace(/[^a-zA-Z0-9_]/g, '_');
655
+ if (node.file) {
656
+ this._emit(`// HtmlClass from external file: ${node.file}`);
657
+ this._emit(`const __get_html_${name} = () => {`);
658
+ this._push();
659
+ this._emit(`const _fs = require('fs'); const _p = require('path');`);
660
+ this._emit(`const p1 = _p.resolve(__dirname, ${JSON.stringify(node.file)});`);
661
+ this._emit(`const p2 = _p.resolve(process.cwd(), ${JSON.stringify(node.file)});`);
662
+ this._emit(`if (_fs.existsSync(p1)) return _fs.readFileSync(p1, 'utf8');`);
663
+ this._emit(`if (_fs.existsSync(p2)) return _fs.readFileSync(p2, 'utf8');`);
664
+ this._emit(`return \`<html><body><h1>404: HTML file ${node.file} not found</h1></body></html>\`;`);
665
+ this._pop();
666
+ this._emit(`};`);
667
+ this._emit(`if (typeof response !== 'undefined' && response && typeof response.send === 'function') {`);
668
+ this._push();
669
+ this._emit(`response.setHeader('Content-Type', 'text/html; charset=utf-8');`);
670
+ this._emit(`response.send(__get_html_${name}());`);
671
+ this._pop();
672
+ this._emit(`} else if (typeof __thy_app !== 'undefined' && __thy_app) {`);
673
+ this._push();
674
+ this._emit(`__thy_app.get('${route}', (req, res) => {`);
675
+ this._push();
676
+ this._emit(`res.setHeader('Content-Type', 'text/html; charset=utf-8');`);
677
+ this._emit(`res.send(__get_html_${name}());`);
678
+ this._pop();
679
+ this._emit(`});`);
680
+ this._pop();
681
+ this._emit(`}`);
682
+ return;
683
+ }
684
+
655
685
  const safeHtml = (node.html || '').replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\${/g, '\\${');
656
686
  this._emit(`// HtmlClass: ${node.name}`);
657
687
  this._emit(`const __html_${name} = \`${safeHtml}\`;`);
@@ -855,13 +885,18 @@ const __thy_sab = typeof SharedArrayBuffer !== 'undefined' ? new SharedArrayBuff
855
885
 
856
886
  _genImportDecl(node) {
857
887
  const src = node.source;
858
- if (node.default) {
859
- this._emit(`const ${node.default} = require(${JSON.stringify(src)});`);
888
+ const defaultName = node.default || (node.isDefault && node.specifiers && node.specifiers[0]);
889
+ if (defaultName) {
890
+ this._emit(`const ${defaultName} = require(${JSON.stringify(src)});`);
860
891
  } else if (node.named && node.named.length) {
861
892
  const bindings = node.named.map(n => typeof n === 'string' ? n : `${n.imported}: ${n.local}`).join(', ');
862
893
  this._emit(`const { ${bindings} } = require(${JSON.stringify(src)});`);
863
- } else if (node.namespace) {
864
- this._emit(`const ${node.namespace} = require(${JSON.stringify(src)});`);
894
+ } else if (node.specifiers && node.specifiers.length && node.specifiers[0] !== '*') {
895
+ const bindings = node.specifiers.join(', ');
896
+ this._emit(`const { ${bindings} } = require(${JSON.stringify(src)});`);
897
+ } else if (node.namespace || (node.specifiers && node.specifiers[0] === '*')) {
898
+ const ns = node.namespace || '_mod';
899
+ this._emit(`const ${ns} = require(${JSON.stringify(src)});`);
865
900
  } else {
866
901
  this._emit(`require(${JSON.stringify(src)});`);
867
902
  }
@@ -882,13 +917,14 @@ const __thy_sab = typeof SharedArrayBuffer !== 'undefined' ? new SharedArrayBuff
882
917
 
883
918
  _genServerDecl(node) {
884
919
  this._expressAppDeclared = true;
885
- const port = node.port ? this._genExpr(node.port) : '3000';
886
- const host = node.host ? this._genExpr(node.host) : "'0.0.0.0'";
920
+ const port = node.port ? this._genExpr(node.port) : "(process.env.PORT || 3000)";
921
+ const host = node.host ? this._genExpr(node.host) : "(process.env.HOST || '0.0.0.0')";
887
922
  const name = node.name || '__thy_server';
888
923
  this._emit(`// [ThyScript] Server declaration`);
889
924
  this._emit(`if (!__thy_express) throw new Error('[ThyScript] express is not installed. Run: npm install express');`);
890
925
  this._emit(`__thy_app.use(require('express').json());`);
891
926
  this._emit(`__thy_app.use(require('express').urlencoded({ extended: true }));`);
927
+ this._emit(`__thy_app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS'); res.setHeader('Access-Control-Allow-Headers', '*'); if (req.method === 'OPTIONS') return res.sendStatus(200); next(); });`);
892
928
  // Body
893
929
  this._genBlockOrList(node.body);
894
930
  // listen
@@ -918,6 +954,8 @@ const __thy_sab = typeof SharedArrayBuffer !== 'undefined' ? new SharedArrayBuff
918
954
  this._emit(`__thy_app.${method}(${path_}, ${isAsync ? 'async ' : ''}(${reqVar}, ${resVar}) => {`);
919
955
  this._push();
920
956
  this._emit(`const req = ${reqVar}; const res = ${resVar};`);
957
+ this._emit(`res.renderHtml = (fp, data = {}) => { const _fs = require('fs'); const _p = require('path'); const p = _fs.existsSync(_p.resolve(process.cwd(), fp)) ? _p.resolve(process.cwd(), fp) : _p.resolve(__dirname, fp); let c = _fs.readFileSync(p, 'utf8'); for (const [k, v] of Object.entries(data)) { c = c.replace(new RegExp('{{\\\\s*' + k + '\\\\s*}}', 'g'), v); } res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(c); };`);
958
+ this._emit(`if (!res.sendFile) { res.sendFile = (fp) => { const _fs = require('fs'); const _p = require('path'); const p = _fs.existsSync(_p.resolve(process.cwd(), fp)) ? _p.resolve(process.cwd(), fp) : _p.resolve(__dirname, fp); res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(_fs.readFileSync(p, 'utf8')); }; }`);
921
959
  this._genBlockOrList(node.body);
922
960
  this._pop();
923
961
  this._emit(`});`);
package/src/index.js CHANGED
@@ -31,7 +31,7 @@ const StringLib = require('./runtime/stdlib/StringLib');
31
31
  const ArrayLib = require('./runtime/stdlib/ArrayLib');
32
32
  const FileLib = require('./runtime/stdlib/FileLib');
33
33
 
34
- const VERSION = '1.0.0';
34
+ const VERSION = '2.0.0';
35
35
 
36
36
  /**
37
37
  * Compile ThyScript source code to optimized Node.js JavaScript
@@ -518,6 +518,21 @@ class Parser {
518
518
  const name = this._expectIdentifierName('Expected HTML class name');
519
519
  this.expect(TokenType.RPAREN, "Expected ')'");
520
520
  this.skipNewlines();
521
+
522
+ // Support external HTML file: classHtml(Name) from "index.html"
523
+ if (this.match(TokenType.KW_FROM)) {
524
+ this.skipNewlines();
525
+ const filePath = this._expectString('Expected HTML file path string');
526
+ this.skipNewlines();
527
+ if (this.match(TokenType.COLON)) {
528
+ this.skipNewlines();
529
+ this._expectEnd();
530
+ }
531
+ const node = AST.HtmlClassDecl(name, '', tok.line, tok.col);
532
+ node.file = filePath;
533
+ return node;
534
+ }
535
+
521
536
  this.expect(TokenType.COLON, "Expected ':' after classHtml declaration");
522
537
 
523
538
  // If the lexer provided a single raw HTML STRING token:
@@ -527,7 +542,11 @@ class Parser {
527
542
  this.advance();
528
543
  this.skipNewlines();
529
544
  this._expectEnd();
530
- return AST.HtmlClassDecl(name, html, tok.line, tok.col);
545
+ const node = AST.HtmlClassDecl(name, html, tok.line, tok.col);
546
+ if (/\.(html|htm|tpl)$/i.test(html.trim()) && !html.includes('<')) {
547
+ node.file = html.trim();
548
+ }
549
+ return node;
531
550
  }
532
551
 
533
552
  // Collect raw HTML lines until `end()`
@@ -545,7 +564,12 @@ class Parser {
545
564
  this.advance();
546
565
  }
547
566
 
548
- return AST.HtmlClassDecl(name, htmlLines.join('\n'), tok.line, tok.col);
567
+ const htmlContent = htmlLines.join('\n');
568
+ const node = AST.HtmlClassDecl(name, htmlContent, tok.line, tok.col);
569
+ if (/\.(html|htm|tpl)$/i.test(htmlContent.trim()) && !htmlContent.includes('<')) {
570
+ node.file = htmlContent.trim();
571
+ }
572
+ return node;
549
573
  }
550
574
 
551
575
  // โ”€โ”€ if / elif / else โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@@ -777,12 +801,26 @@ class Parser {
777
801
  return AST.TryCatch(body, errorVar, handler, finalizer, tok.line, tok.col);
778
802
  }
779
803
 
780
- // โ”€โ”€ import name from 'module' โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
804
+ // โ”€โ”€ import name from 'module' or import "npm-package" โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
781
805
  _parseImport() {
782
806
  const tok = this.current();
783
807
  this.advance(); // consume `import`
784
808
  this.skipNewlines();
785
809
 
810
+ // 1. Direct npm package import: import "packageName" or import "packageName" as name
811
+ if (this.check(TokenType.STRING)) {
812
+ const srcToken = this.current();
813
+ this.advance();
814
+ const source = srcToken.value;
815
+ let varName = source.split('/').pop().replace(/[^a-zA-Z0-9_]/g, '_');
816
+ this.skipNewlines();
817
+ if (this.match(TokenType.KW_AS)) {
818
+ this.skipNewlines();
819
+ varName = this._expectIdentifierName('Expected alias name after as');
820
+ }
821
+ return AST.ImportDecl([varName], source, true, tok.line, tok.col);
822
+ }
823
+
786
824
  const specifiers = [];
787
825
  let isDefault = false;
788
826
 
@@ -92,7 +92,7 @@ class ControlPanel {
92
92
  },
93
93
  server: {
94
94
  nodeVersion: process.version,
95
- thyscriptVersion: '1.0.0',
95
+ thyscriptVersion: '2.0.0',
96
96
  uptimeSeconds: Math.floor((Date.now() - this.startTime) / 1000),
97
97
  activeServicesCount: this.activeServices.size,
98
98
  historyCount: this.runHistory.length,
@@ -807,7 +807,7 @@ class ControlPanel {
807
807
  <div>
808
808
  <h1>ThyScript Control Panel</h1>
809
809
  </div>
810
- <span class="badge">v1.0.0 HOST SERVER</span>
810
+ <span class="badge">v2.0.0 HOST SERVER</span>
811
811
  </div>
812
812
  <div class="header-actions">
813
813
  <div class="status-pill">
@@ -223,7 +223,7 @@ class ServerEngine {
223
223
  * @returns {ServerEngine}
224
224
  */
225
225
  addMiddleware(fn) {
226
- if (!this.app) throw new Error('Call create() first');
226
+ if (!this.app) this.create();
227
227
  this.app.use(fn);
228
228
  return this;
229
229
  }
@@ -234,7 +234,7 @@ class ServerEngine {
234
234
  * @returns {ServerEngine}
235
235
  */
236
236
  enableCORS(options = {}) {
237
- if (!this.app) throw new Error('Call create() first');
237
+ if (!this.app) this.create();
238
238
  const defaultOptions = {
239
239
  origin: '*',
240
240
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
@@ -251,7 +251,7 @@ class ServerEngine {
251
251
  * @returns {ServerEngine}
252
252
  */
253
253
  enableCompression() {
254
- if (!this.app) throw new Error('Call create() first');
254
+ if (!this.app) this.create();
255
255
  this.app.use(compression());
256
256
  return this;
257
257
  }
@@ -264,7 +264,7 @@ class ServerEngine {
264
264
  * @returns {ServerEngine}
265
265
  */
266
266
  enableStaticFiles(dir, mountPath = '/', options = {}) {
267
- if (!this.app) throw new Error('Call create() first');
267
+ if (!this.app) this.create();
268
268
  const staticOptions = Object.assign({
269
269
  maxAge: '1d',
270
270
  etag: true,
@@ -281,7 +281,7 @@ class ServerEngine {
281
281
  * @returns {ServerEngine}
282
282
  */
283
283
  enableSSL(cert, key) {
284
- if (!this.app) throw new Error('Call create() first');
284
+ if (!this.app) this.create();
285
285
  const certData = typeof cert === 'string' ? fs.readFileSync(cert) : cert;
286
286
  const keyData = typeof key === 'string' ? fs.readFileSync(key) : key;
287
287
  this.server = https.createServer({ cert: certData, key: keyData }, this.app);
@@ -300,7 +300,7 @@ class ServerEngine {
300
300
  * @returns {ServerEngine}
301
301
  */
302
302
  addRoute(method, routePath, ...handlers) {
303
- if (!this.app) throw new Error('Call create() first');
303
+ if (!this.app) this.create();
304
304
  const m = method.toLowerCase();
305
305
  if (typeof this.app[m] !== 'function') {
306
306
  throw new Error(`Invalid HTTP method: ${method}`);
@@ -329,7 +329,7 @@ class ServerEngine {
329
329
  */
330
330
  enableWebSocket(options = {}) {
331
331
  if (!socketIo) throw new Error('socket.io is not installed. Run: npm install socket.io');
332
- if (!this.server) throw new Error('Call create() first');
332
+ if (!this.server) this.create();
333
333
 
334
334
  const ioOptions = Object.assign({
335
335
  cors: { origin: '*', methods: ['GET', 'POST'] },
@@ -430,7 +430,7 @@ class ServerEngine {
430
430
  * @returns {ServerEngine}
431
431
  */
432
432
  enableSSE(sseRoutePath = '/events') {
433
- if (!this.app) throw new Error('Call create() first');
433
+ if (!this.app) this.create();
434
434
  this.app.get(sseRoutePath, (req, res) => {
435
435
  const clientId = `sse_${Date.now()}_${Math.random().toString(36).slice(2)}`;
436
436
 
@@ -621,15 +621,16 @@ class ServerEngine {
621
621
  * @param {number} [port] - Overrides the port given in create().
622
622
  * @returns {Promise<ServerEngine>}
623
623
  */
624
- start(port) {
624
+ start(port, host = '0.0.0.0') {
625
625
  return new Promise((resolve, reject) => {
626
- if (!this.server) throw new Error('Call create() first');
627
626
  const listenPort = port || this.defaultPort || 3000;
628
- this.server.listen(listenPort, () => {
627
+ if (!this.server) this.create(listenPort);
628
+ const listenHost = host || '0.0.0.0';
629
+ this.server.listen(listenPort, listenHost, () => {
629
630
  this.isRunning = true;
630
631
  this.startTime = Date.now();
631
632
  this._startMetricsTicker();
632
- console.log(`[ServerEngine] Server running on port ${listenPort} (PID ${process.pid})`);
633
+ console.log(`[ServerEngine] Server running on http://${listenHost}:${listenPort} (PID ${process.pid})`);
633
634
  resolve(this);
634
635
  });
635
636
  this.server.on('error', reject);
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const { spawnSync } = require('child_process');
5
+
6
+ console.log('๐Ÿงช Starting ThyScript v2.0 Test Suite...\n');
7
+
8
+ const tests = [
9
+ 'tests/test1_math.thy',
10
+ 'tests/test2_oop.thy',
11
+ 'tests/test4_functions.thy',
12
+ ];
13
+
14
+ let allPassed = true;
15
+
16
+ for (const t of tests) {
17
+ process.stdout.write(`โ–ถ Running ${t}... `);
18
+ const result = spawnSync('node', ['bin/thy-node.js', t, '--no-banner', '--no-stats'], {
19
+ cwd: path.resolve(__dirname, '..'),
20
+ encoding: 'utf8',
21
+ });
22
+
23
+ if (result.status === 0) {
24
+ console.log('โœ… PASSED');
25
+ } else {
26
+ console.log('โŒ FAILED');
27
+ console.error(result.stdout);
28
+ console.error(result.stderr);
29
+ allPassed = false;
30
+ }
31
+ }
32
+
33
+ console.log('\nโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€');
34
+ if (allPassed) {
35
+ console.log('๐ŸŽ‰ ALL THYSCRIPT CORE TESTS PASSED SUCCESSFULLY!');
36
+ process.exit(0);
37
+ } else {
38
+ console.error('๐Ÿ’ฅ SOME TESTS FAILED.');
39
+ process.exit(1);
40
+ }
@@ -0,0 +1,22 @@
1
+ // ThyScript v2.0 Feature Verification Test
2
+ import "chalk"
3
+
4
+ print(chalk.green("โœ” Successfully loaded npm library via import 'chalk'!"))
5
+
6
+ // External HTML class
7
+ classHtml(Home) from "public/home.html"
8
+
9
+ // Start server on port 4321 without any create()
10
+ server(4321):
11
+ print("๐Ÿš€ ThyScript v2 Server initialized on port 4321")
12
+ end()
13
+
14
+ route get "/":
15
+ res.renderHtml("public/home.html", { title: "ThyScript v2 External HTML", user: "Dede Engineer" })
16
+ end()
17
+
18
+ route get "/api/v2/status":
19
+ res.json({ version: "2.0.0", status: "active", engine: "ThyScript JIT v2" })
20
+ end()
21
+
22
+ print("Server is listening on port 4321!")