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.
- package/DOKUMEN.md +525 -0
- package/README.md +204 -0
- package/bin/thy-node.js +1 -1
- package/bin/thy.js +32 -21
- package/dist/calculator.thy.js +624 -0
- package/dist/thy-build.json +7 -0
- package/examples/calculator.thy +1 -0
- package/package.json +2 -2
- package/public/home.html +19 -0
- package/src/compiler/CodeGenerator.js +44 -6
- package/src/index.js +1 -1
- package/src/parser/Parser.js +41 -3
- package/src/server/ControlPanel.js +2 -2
- package/src/server/ServerEngine.js +13 -12
- package/tests/run-tests.js +40 -0
- package/tests/test_v2_features.thy +22 -0
|
@@ -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
|
-
|
|
859
|
-
|
|
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.
|
|
864
|
-
|
|
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) :
|
|
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 = '
|
|
34
|
+
const VERSION = '2.0.0';
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Compile ThyScript source code to optimized Node.js JavaScript
|
package/src/parser/Parser.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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: '
|
|
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">
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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.
|
|
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
|
|
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!")
|