@devansharora18/tardisjs 0.0.2
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/LICENSE +21 -0
- package/README.md +0 -0
- package/dist/bin/bin/create-tardis-app.d.ts +2 -0
- package/dist/bin/bin/tardis.d.ts +15 -0
- package/dist/bin/create-tardis-app.d.ts +2 -0
- package/dist/bin/create-tardis-app.js +2102 -0
- package/dist/bin/create-tardis-app.js.map +1 -0
- package/dist/bin/src/compiler/compiler.d.ts +3 -0
- package/dist/bin/src/lexer/lexer.d.ts +2 -0
- package/dist/bin/src/lexer/tokens.d.ts +8 -0
- package/dist/bin/src/parser/errors.d.ts +6 -0
- package/dist/bin/src/parser/parser.d.ts +3 -0
- package/dist/bin/src/parser/types.d.ts +67 -0
- package/dist/bin/src/runtime/events.d.ts +9 -0
- package/dist/bin/src/runtime/fetch.d.ts +11 -0
- package/dist/bin/src/runtime/index.d.ts +52 -0
- package/dist/bin/src/runtime/registry.d.ts +17 -0
- package/dist/bin/src/runtime/render.d.ts +11 -0
- package/dist/bin/src/runtime/router.d.ts +15 -0
- package/dist/bin/src/runtime/selector.d.ts +67 -0
- package/dist/bin/src/runtime/state.d.ts +9 -0
- package/dist/bin/src/runtime/styles.d.ts +9 -0
- package/dist/bin/tardis.d.ts +15 -0
- package/dist/bin/tardis.js +2086 -0
- package/dist/bin/tardis.js.map +1 -0
- package/dist/index.cjs +3 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/dist/runtime/bin/create-tardis-app.d.ts +2 -0
- package/dist/runtime/bin/tardis.d.ts +15 -0
- package/dist/runtime/index.cjs +829 -0
- package/dist/runtime/index.cjs.map +1 -0
- package/dist/runtime/index.mjs +791 -0
- package/dist/runtime/index.mjs.map +1 -0
- package/dist/runtime/src/compiler/compiler.d.ts +3 -0
- package/dist/runtime/src/lexer/lexer.d.ts +2 -0
- package/dist/runtime/src/lexer/tokens.d.ts +8 -0
- package/dist/runtime/src/parser/errors.d.ts +6 -0
- package/dist/runtime/src/parser/parser.d.ts +3 -0
- package/dist/runtime/src/parser/types.d.ts +67 -0
- package/dist/runtime/src/runtime/events.d.ts +9 -0
- package/dist/runtime/src/runtime/fetch.d.ts +11 -0
- package/dist/runtime/src/runtime/index.d.ts +52 -0
- package/dist/runtime/src/runtime/registry.d.ts +17 -0
- package/dist/runtime/src/runtime/render.d.ts +11 -0
- package/dist/runtime/src/runtime/router.d.ts +15 -0
- package/dist/runtime/src/runtime/selector.d.ts +67 -0
- package/dist/runtime/src/runtime/state.d.ts +9 -0
- package/dist/runtime/src/runtime/styles.d.ts +9 -0
- package/dist/src/compiler/compiler.d.ts +3 -0
- package/dist/src/lexer/lexer.d.ts +2 -0
- package/dist/src/lexer/tokens.d.ts +8 -0
- package/dist/src/parser/errors.d.ts +6 -0
- package/dist/src/parser/parser.d.ts +3 -0
- package/dist/src/parser/types.d.ts +67 -0
- package/dist/src/runtime/events.d.ts +9 -0
- package/dist/src/runtime/fetch.d.ts +11 -0
- package/dist/src/runtime/index.d.ts +52 -0
- package/dist/src/runtime/registry.d.ts +17 -0
- package/dist/src/runtime/render.d.ts +11 -0
- package/dist/src/runtime/router.d.ts +15 -0
- package/dist/src/runtime/selector.d.ts +67 -0
- package/dist/src/runtime/state.d.ts +9 -0
- package/dist/src/runtime/styles.d.ts +9 -0
- package/package.json +67 -0
|
@@ -0,0 +1,2102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import fsp from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import http from 'node:http';
|
|
6
|
+
import net from 'node:net';
|
|
7
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import chokidar from 'chokidar';
|
|
10
|
+
import { WebSocketServer } from 'ws';
|
|
11
|
+
|
|
12
|
+
const KEYWORDS = {
|
|
13
|
+
blueprint: 'BLUEPRINT',
|
|
14
|
+
props: 'PROPS',
|
|
15
|
+
state: 'STATE',
|
|
16
|
+
computed: 'COMPUTED',
|
|
17
|
+
methods: 'METHODS',
|
|
18
|
+
events: 'EVENTS',
|
|
19
|
+
style: 'STYLE',
|
|
20
|
+
ui: 'UI',
|
|
21
|
+
script: 'SCRIPT',
|
|
22
|
+
true: 'BOOLEAN',
|
|
23
|
+
false: 'BOOLEAN',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function lex(source) {
|
|
27
|
+
const tokens = [];
|
|
28
|
+
let i = 0;
|
|
29
|
+
let line = 1;
|
|
30
|
+
let col = 1;
|
|
31
|
+
function peek(offset = 0) {
|
|
32
|
+
return source[i + offset] ?? '';
|
|
33
|
+
}
|
|
34
|
+
function advance() {
|
|
35
|
+
const ch = source[i++];
|
|
36
|
+
if (ch === '\n') {
|
|
37
|
+
line++;
|
|
38
|
+
col = 1;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
col++;
|
|
42
|
+
}
|
|
43
|
+
return ch;
|
|
44
|
+
}
|
|
45
|
+
function skipWhitespace() {
|
|
46
|
+
while (i < source.length && (peek() === ' ' || peek() === '\t' || peek() === '\r')) {
|
|
47
|
+
advance();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function skipComment() {
|
|
51
|
+
if (peek() === '/' && peek(1) === '/') {
|
|
52
|
+
while (i < source.length && peek() !== '\n') {
|
|
53
|
+
advance();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function readString() {
|
|
58
|
+
advance();
|
|
59
|
+
let value = '';
|
|
60
|
+
while (i < source.length && peek() !== '"') {
|
|
61
|
+
if (peek() === '\\' && peek(1) === '"') {
|
|
62
|
+
advance();
|
|
63
|
+
value += advance();
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
if (peek() === '\n')
|
|
67
|
+
line++;
|
|
68
|
+
value += advance();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (peek() === '"')
|
|
72
|
+
advance();
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
function readIdent() {
|
|
76
|
+
let value = '';
|
|
77
|
+
while (i < source.length && /[a-zA-Z0-9_$]/.test(peek())) {
|
|
78
|
+
value += advance();
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
function readNumber() {
|
|
83
|
+
let value = '';
|
|
84
|
+
while (i < source.length && /[0-9.]/.test(peek())) {
|
|
85
|
+
value += advance();
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
function readStyleMode() {
|
|
90
|
+
advance();
|
|
91
|
+
let value = '';
|
|
92
|
+
while (i < source.length && peek() !== ')') {
|
|
93
|
+
value += advance();
|
|
94
|
+
}
|
|
95
|
+
if (peek() === ')')
|
|
96
|
+
advance();
|
|
97
|
+
return value.trim();
|
|
98
|
+
}
|
|
99
|
+
function readRawBlock() {
|
|
100
|
+
let depth = 1;
|
|
101
|
+
let value = '';
|
|
102
|
+
while (i < source.length && depth > 0) {
|
|
103
|
+
const ch = peek();
|
|
104
|
+
if (ch === '{')
|
|
105
|
+
depth++;
|
|
106
|
+
if (ch === '}') {
|
|
107
|
+
depth--;
|
|
108
|
+
if (depth === 0)
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
if (ch === '\n')
|
|
112
|
+
line++;
|
|
113
|
+
value += advance();
|
|
114
|
+
}
|
|
115
|
+
if (peek() === '}')
|
|
116
|
+
advance();
|
|
117
|
+
return value.trim();
|
|
118
|
+
}
|
|
119
|
+
function readRawExpr() {
|
|
120
|
+
let value = '';
|
|
121
|
+
while (i < source.length && '*+-/><!=&%^~?'.includes(peek())) {
|
|
122
|
+
value += advance();
|
|
123
|
+
}
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
let lastKeyword = null;
|
|
127
|
+
while (i < source.length) {
|
|
128
|
+
skipWhitespace();
|
|
129
|
+
skipComment();
|
|
130
|
+
if (i >= source.length)
|
|
131
|
+
break;
|
|
132
|
+
const startLine = line;
|
|
133
|
+
const startCol = col;
|
|
134
|
+
const emit = (type, value) => {
|
|
135
|
+
tokens.push({ type, value, line: startLine, col: startCol });
|
|
136
|
+
};
|
|
137
|
+
const ch = peek();
|
|
138
|
+
if (ch === '\n') {
|
|
139
|
+
advance();
|
|
140
|
+
emit('NEWLINE', '\n');
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (ch === '{') {
|
|
144
|
+
advance();
|
|
145
|
+
if (lastKeyword === 'UI' || lastKeyword === 'SCRIPT' || lastKeyword === 'STYLE') {
|
|
146
|
+
const raw = readRawBlock();
|
|
147
|
+
emit('RAW_JSX', raw);
|
|
148
|
+
lastKeyword = null;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
emit('LBRACE', '{');
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (ch === '}') {
|
|
155
|
+
advance();
|
|
156
|
+
emit('RBRACE', '}');
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (ch === ':') {
|
|
160
|
+
advance();
|
|
161
|
+
emit('COLON', ':');
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (ch === '|') {
|
|
165
|
+
advance();
|
|
166
|
+
emit('PIPE', '|');
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (ch === '.') {
|
|
170
|
+
advance();
|
|
171
|
+
emit('DOT', '.');
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (ch === '(') {
|
|
175
|
+
advance();
|
|
176
|
+
emit('LPAREN', '(');
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (ch === ')') {
|
|
180
|
+
advance();
|
|
181
|
+
emit('RPAREN', ')');
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (ch === ',') {
|
|
185
|
+
advance();
|
|
186
|
+
emit('COMMA', ',');
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (ch === '=') {
|
|
190
|
+
if (peek(1) === '>') {
|
|
191
|
+
advance();
|
|
192
|
+
advance();
|
|
193
|
+
emit('ARROW', '=>');
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
advance();
|
|
197
|
+
emit('EQUALS', '=');
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if ('*+-/><!=&%^~?'.includes(ch)) {
|
|
202
|
+
const raw = readRawExpr();
|
|
203
|
+
emit('RAW_EXPR', raw);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (ch === '"') {
|
|
207
|
+
const str = readString();
|
|
208
|
+
emit('STRING', str);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (/[0-9]/.test(ch)) {
|
|
212
|
+
const num = readNumber();
|
|
213
|
+
emit('NUMBER', num);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (/[a-zA-Z_$]/.test(ch)) {
|
|
217
|
+
const ident = readIdent();
|
|
218
|
+
const keywordType = KEYWORDS[ident];
|
|
219
|
+
if (keywordType) {
|
|
220
|
+
emit(keywordType, ident);
|
|
221
|
+
lastKeyword = keywordType;
|
|
222
|
+
if (keywordType === 'STYLE' && peek() === '(') {
|
|
223
|
+
const modeStartLine = line;
|
|
224
|
+
const modeStartCol = col;
|
|
225
|
+
const mode = readStyleMode();
|
|
226
|
+
tokens.push({ type: 'STYLE_MODE', value: mode, line: modeStartLine, col: modeStartCol });
|
|
227
|
+
lastKeyword = null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
emit('IDENT', ident);
|
|
232
|
+
}
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
advance();
|
|
236
|
+
}
|
|
237
|
+
tokens.push({ type: 'EOF', value: '', line, col });
|
|
238
|
+
return tokens;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
class TardisError extends Error {
|
|
242
|
+
constructor(message, file = 'unknown', line = 0, col = 0) {
|
|
243
|
+
super(`\nTardisError: ${file} line ${line} col ${col}\n ${message}\n`);
|
|
244
|
+
this.file = file;
|
|
245
|
+
this.line = line;
|
|
246
|
+
this.col = col;
|
|
247
|
+
this.name = 'TardisError';
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function parse(tokens, file = 'unknown') {
|
|
252
|
+
let cursor = 0;
|
|
253
|
+
// ── helpers ────────────────────────────────────────────────────────────
|
|
254
|
+
function peek(offset = 0) {
|
|
255
|
+
return tokens[cursor + offset] ?? { type: 'EOF', value: '', line: 0, col: 0 };
|
|
256
|
+
}
|
|
257
|
+
function advance() {
|
|
258
|
+
const token = tokens[cursor];
|
|
259
|
+
cursor++;
|
|
260
|
+
return token;
|
|
261
|
+
}
|
|
262
|
+
function expect(type) {
|
|
263
|
+
const token = peek();
|
|
264
|
+
if (token.type !== type) {
|
|
265
|
+
throw new TardisError(`Expected ${type} but got ${token.type} ("${token.value}")`, file, token.line, token.col);
|
|
266
|
+
}
|
|
267
|
+
return advance();
|
|
268
|
+
}
|
|
269
|
+
function skipNewlines() {
|
|
270
|
+
while (peek().type === 'NEWLINE')
|
|
271
|
+
advance();
|
|
272
|
+
}
|
|
273
|
+
function check(type) {
|
|
274
|
+
return peek().type === type;
|
|
275
|
+
}
|
|
276
|
+
// ── expression builder ─────────────────────────────────────────────────
|
|
277
|
+
// reads tokens until a stopping condition and builds a raw expression string
|
|
278
|
+
// preserves spaces around operators and commas
|
|
279
|
+
function buildExpr(stopAt) {
|
|
280
|
+
let expr = '';
|
|
281
|
+
while (!stopAt.includes(peek().type) && !check('EOF')) {
|
|
282
|
+
const tok = peek();
|
|
283
|
+
if (tok.type === 'RAW_EXPR') {
|
|
284
|
+
expr += ` ${tok.value} `;
|
|
285
|
+
}
|
|
286
|
+
else if (tok.type === 'COMMA') {
|
|
287
|
+
expr += ', ';
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
expr += tok.value;
|
|
291
|
+
}
|
|
292
|
+
advance();
|
|
293
|
+
}
|
|
294
|
+
return expr.trim();
|
|
295
|
+
}
|
|
296
|
+
// reads a raw block body tracking brace depth
|
|
297
|
+
// used for method bodies and event handlers
|
|
298
|
+
function buildBody() {
|
|
299
|
+
let raw = '';
|
|
300
|
+
let depth = 0;
|
|
301
|
+
while (!check('EOF')) {
|
|
302
|
+
const tok = peek();
|
|
303
|
+
if (tok.value === '{')
|
|
304
|
+
depth++;
|
|
305
|
+
if (tok.value === '}') {
|
|
306
|
+
if (depth === 0)
|
|
307
|
+
break;
|
|
308
|
+
depth--;
|
|
309
|
+
}
|
|
310
|
+
if (tok.type === 'NEWLINE' && depth === 0)
|
|
311
|
+
break;
|
|
312
|
+
if (tok.type === 'RAW_EXPR')
|
|
313
|
+
raw += ` ${tok.value} `;
|
|
314
|
+
else if (tok.type === 'COMMA')
|
|
315
|
+
raw += ', ';
|
|
316
|
+
else
|
|
317
|
+
raw += tok.value;
|
|
318
|
+
advance();
|
|
319
|
+
}
|
|
320
|
+
return raw.trim();
|
|
321
|
+
}
|
|
322
|
+
// ── did you mean ───────────────────────────────────────────────────────
|
|
323
|
+
function didYouMean(input, options) {
|
|
324
|
+
function distance(a, b) {
|
|
325
|
+
const dp = Array.from({ length: a.length + 1 }, (_, i) => Array.from({ length: b.length + 1 }, (_, j) => i === 0 ? j : j === 0 ? i : 0));
|
|
326
|
+
for (let i = 1; i <= a.length; i++) {
|
|
327
|
+
for (let j = 1; j <= b.length; j++) {
|
|
328
|
+
dp[i][j] = a[i - 1] === b[j - 1]
|
|
329
|
+
? dp[i - 1][j - 1]
|
|
330
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return dp[a.length][b.length];
|
|
334
|
+
}
|
|
335
|
+
const closest = options.reduce((best, opt) => {
|
|
336
|
+
const d = distance(input, opt);
|
|
337
|
+
return d < best.dist ? { opt, dist: d } : best;
|
|
338
|
+
}, { opt: '', dist: Infinity });
|
|
339
|
+
return closest.dist <= 2 ? closest.opt : null;
|
|
340
|
+
}
|
|
341
|
+
// ── type and default parser ────────────────────────────────────────────
|
|
342
|
+
function parsePropTypeAndDefault() {
|
|
343
|
+
// union type: "primary" | "ghost" | "danger"
|
|
344
|
+
if (check('STRING') && peek(1).type === 'PIPE') {
|
|
345
|
+
const unionValues = [];
|
|
346
|
+
while (check('STRING')) {
|
|
347
|
+
unionValues.push(advance().value);
|
|
348
|
+
if (check('PIPE'))
|
|
349
|
+
advance();
|
|
350
|
+
}
|
|
351
|
+
let defaultVal = null;
|
|
352
|
+
if (check('EQUALS')) {
|
|
353
|
+
advance();
|
|
354
|
+
defaultVal = expect('STRING').value;
|
|
355
|
+
}
|
|
356
|
+
return { type: unionValues, defaultVal };
|
|
357
|
+
}
|
|
358
|
+
// primitive type
|
|
359
|
+
const typeTok = expect('IDENT');
|
|
360
|
+
const typeName = typeTok.value;
|
|
361
|
+
const validTypes = ['string', 'number', 'boolean', 'array', 'object', 'function'];
|
|
362
|
+
if (!validTypes.includes(typeName)) {
|
|
363
|
+
const suggestion = didYouMean(typeName, validTypes);
|
|
364
|
+
throw new TardisError(`Unknown type "${typeName}"${suggestion ? ` — did you mean "${suggestion}"?` : ''}`, file, typeTok.line, typeTok.col);
|
|
365
|
+
}
|
|
366
|
+
if (typeName === 'function') {
|
|
367
|
+
return { type: 'function', defaultVal: null };
|
|
368
|
+
}
|
|
369
|
+
let defaultVal = null;
|
|
370
|
+
if (check('EQUALS')) {
|
|
371
|
+
advance(); // consume =
|
|
372
|
+
if (check('STRING')) {
|
|
373
|
+
defaultVal = advance().value;
|
|
374
|
+
}
|
|
375
|
+
else if (check('NUMBER')) {
|
|
376
|
+
defaultVal = parseFloat(advance().value);
|
|
377
|
+
}
|
|
378
|
+
else if (check('BOOLEAN')) {
|
|
379
|
+
defaultVal = advance().value === 'true';
|
|
380
|
+
}
|
|
381
|
+
else if (check('LBRACE')) {
|
|
382
|
+
// [] or {} default
|
|
383
|
+
advance(); // consume {
|
|
384
|
+
if (check('RBRACE')) {
|
|
385
|
+
advance(); // consume }
|
|
386
|
+
defaultVal = typeName === 'array' ? '[]' : '{}';
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
// expression reference like props.initial or state.count
|
|
391
|
+
defaultVal = buildExpr(['NEWLINE', 'RBRACE', 'EOF']);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return { type: typeName, defaultVal };
|
|
395
|
+
}
|
|
396
|
+
// ── method signature parser ────────────────────────────────────────────
|
|
397
|
+
function parseMethodSignature(raw, tok) {
|
|
398
|
+
const arrowMatch = raw.match(/^\(([^)]*)\)\s*=>\s*(.+)$/s);
|
|
399
|
+
if (!arrowMatch) {
|
|
400
|
+
throw new TardisError(`Invalid method syntax — expected "(params) => body" but got "${raw}"`, file, tok.line, tok.col);
|
|
401
|
+
}
|
|
402
|
+
const paramStr = arrowMatch[1].trim();
|
|
403
|
+
const body = arrowMatch[2].trim();
|
|
404
|
+
const params = paramStr
|
|
405
|
+
? paramStr.split(',').map(p => p.trim()).filter(Boolean)
|
|
406
|
+
: [];
|
|
407
|
+
return { params, body };
|
|
408
|
+
}
|
|
409
|
+
// ── section parsers ────────────────────────────────────────────────────
|
|
410
|
+
function parseProps() {
|
|
411
|
+
expect('PROPS');
|
|
412
|
+
skipNewlines();
|
|
413
|
+
expect('LBRACE');
|
|
414
|
+
skipNewlines();
|
|
415
|
+
const props = [];
|
|
416
|
+
while (!check('RBRACE') && !check('EOF')) {
|
|
417
|
+
skipNewlines();
|
|
418
|
+
if (check('RBRACE'))
|
|
419
|
+
break;
|
|
420
|
+
const nameTok = expect('IDENT');
|
|
421
|
+
expect('COLON');
|
|
422
|
+
const { type, defaultVal } = parsePropTypeAndDefault();
|
|
423
|
+
props.push({
|
|
424
|
+
name: nameTok.value,
|
|
425
|
+
type,
|
|
426
|
+
default: defaultVal,
|
|
427
|
+
line: nameTok.line,
|
|
428
|
+
col: nameTok.col,
|
|
429
|
+
});
|
|
430
|
+
skipNewlines();
|
|
431
|
+
}
|
|
432
|
+
expect('RBRACE');
|
|
433
|
+
return props;
|
|
434
|
+
}
|
|
435
|
+
function parseState() {
|
|
436
|
+
expect('STATE');
|
|
437
|
+
skipNewlines();
|
|
438
|
+
expect('LBRACE');
|
|
439
|
+
skipNewlines();
|
|
440
|
+
const state = [];
|
|
441
|
+
while (!check('RBRACE') && !check('EOF')) {
|
|
442
|
+
skipNewlines();
|
|
443
|
+
if (check('RBRACE'))
|
|
444
|
+
break;
|
|
445
|
+
const nameTok = expect('IDENT');
|
|
446
|
+
expect('COLON');
|
|
447
|
+
const { type, defaultVal } = parsePropTypeAndDefault();
|
|
448
|
+
state.push({
|
|
449
|
+
name: nameTok.value,
|
|
450
|
+
type,
|
|
451
|
+
default: defaultVal,
|
|
452
|
+
line: nameTok.line,
|
|
453
|
+
col: nameTok.col,
|
|
454
|
+
});
|
|
455
|
+
skipNewlines();
|
|
456
|
+
}
|
|
457
|
+
expect('RBRACE');
|
|
458
|
+
return state;
|
|
459
|
+
}
|
|
460
|
+
function parseComputed() {
|
|
461
|
+
expect('COMPUTED');
|
|
462
|
+
skipNewlines();
|
|
463
|
+
expect('LBRACE');
|
|
464
|
+
skipNewlines();
|
|
465
|
+
const computed = [];
|
|
466
|
+
while (!check('RBRACE') && !check('EOF')) {
|
|
467
|
+
skipNewlines();
|
|
468
|
+
if (check('RBRACE'))
|
|
469
|
+
break;
|
|
470
|
+
const nameTok = expect('IDENT');
|
|
471
|
+
expect('COLON');
|
|
472
|
+
const expr = buildExpr(['NEWLINE', 'RBRACE', 'EOF']);
|
|
473
|
+
computed.push({
|
|
474
|
+
name: nameTok.value,
|
|
475
|
+
expr,
|
|
476
|
+
line: nameTok.line,
|
|
477
|
+
col: nameTok.col,
|
|
478
|
+
});
|
|
479
|
+
skipNewlines();
|
|
480
|
+
}
|
|
481
|
+
expect('RBRACE');
|
|
482
|
+
return computed;
|
|
483
|
+
}
|
|
484
|
+
function parseMethods() {
|
|
485
|
+
expect('METHODS');
|
|
486
|
+
skipNewlines();
|
|
487
|
+
expect('LBRACE');
|
|
488
|
+
skipNewlines();
|
|
489
|
+
const methods = [];
|
|
490
|
+
while (!check('RBRACE') && !check('EOF')) {
|
|
491
|
+
skipNewlines();
|
|
492
|
+
if (check('RBRACE'))
|
|
493
|
+
break;
|
|
494
|
+
const nameTok = expect('IDENT');
|
|
495
|
+
expect('COLON');
|
|
496
|
+
const raw = buildBody();
|
|
497
|
+
const { params, body } = parseMethodSignature(raw, nameTok);
|
|
498
|
+
methods.push({
|
|
499
|
+
name: nameTok.value,
|
|
500
|
+
params,
|
|
501
|
+
body,
|
|
502
|
+
line: nameTok.line,
|
|
503
|
+
col: nameTok.col,
|
|
504
|
+
});
|
|
505
|
+
skipNewlines();
|
|
506
|
+
}
|
|
507
|
+
expect('RBRACE');
|
|
508
|
+
return methods;
|
|
509
|
+
}
|
|
510
|
+
function parseEvents() {
|
|
511
|
+
expect('EVENTS');
|
|
512
|
+
skipNewlines();
|
|
513
|
+
expect('LBRACE');
|
|
514
|
+
skipNewlines();
|
|
515
|
+
const events = {
|
|
516
|
+
onMount: null,
|
|
517
|
+
onDestroy: null,
|
|
518
|
+
onUpdate: null,
|
|
519
|
+
};
|
|
520
|
+
while (!check('RBRACE') && !check('EOF')) {
|
|
521
|
+
skipNewlines();
|
|
522
|
+
if (check('RBRACE'))
|
|
523
|
+
break;
|
|
524
|
+
const nameTok = expect('IDENT');
|
|
525
|
+
expect('COLON');
|
|
526
|
+
const raw = buildBody();
|
|
527
|
+
const key = nameTok.value;
|
|
528
|
+
if (key in events) {
|
|
529
|
+
events[key] = raw;
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
throw new TardisError(`Unknown event "${nameTok.value}" — valid events are onMount, onDestroy, onUpdate`, file, nameTok.line, nameTok.col);
|
|
533
|
+
}
|
|
534
|
+
skipNewlines();
|
|
535
|
+
}
|
|
536
|
+
expect('RBRACE');
|
|
537
|
+
return events;
|
|
538
|
+
}
|
|
539
|
+
function parseStyle() {
|
|
540
|
+
expect('STYLE');
|
|
541
|
+
// raw CSS mode: style { ... } without (mode)
|
|
542
|
+
if (check('RAW_JSX')) {
|
|
543
|
+
const rawTok = expect('RAW_JSX');
|
|
544
|
+
return { mode: 'raw', rules: [], raw: rawTok.value };
|
|
545
|
+
}
|
|
546
|
+
const modeTok = expect('STYLE_MODE');
|
|
547
|
+
const mode = modeTok.value;
|
|
548
|
+
if (mode !== 'tailwind' && mode !== 'css') {
|
|
549
|
+
throw new TardisError(`Unknown style mode "${mode}" — valid modes are tailwind and css`, file, modeTok.line, modeTok.col);
|
|
550
|
+
}
|
|
551
|
+
skipNewlines();
|
|
552
|
+
expect('LBRACE');
|
|
553
|
+
skipNewlines();
|
|
554
|
+
const rules = [];
|
|
555
|
+
while (!check('RBRACE') && !check('EOF')) {
|
|
556
|
+
skipNewlines();
|
|
557
|
+
if (check('RBRACE'))
|
|
558
|
+
break;
|
|
559
|
+
// read key — can be dotted like variant.primary or disabled.true
|
|
560
|
+
let key = '';
|
|
561
|
+
const keyTok = peek();
|
|
562
|
+
while (!check('COLON') && !check('NEWLINE') && !check('EOF')) {
|
|
563
|
+
key += peek().value;
|
|
564
|
+
advance();
|
|
565
|
+
}
|
|
566
|
+
expect('COLON');
|
|
567
|
+
const valueTok = expect('STRING');
|
|
568
|
+
rules.push({
|
|
569
|
+
key: key.trim(),
|
|
570
|
+
value: valueTok.value,
|
|
571
|
+
line: keyTok.line,
|
|
572
|
+
col: keyTok.col,
|
|
573
|
+
});
|
|
574
|
+
skipNewlines();
|
|
575
|
+
}
|
|
576
|
+
expect('RBRACE');
|
|
577
|
+
return { mode, rules };
|
|
578
|
+
}
|
|
579
|
+
function parseUI() {
|
|
580
|
+
const uiTok = expect('UI');
|
|
581
|
+
skipNewlines();
|
|
582
|
+
const rawTok = expect('RAW_JSX');
|
|
583
|
+
return {
|
|
584
|
+
raw: rawTok.value,
|
|
585
|
+
line: uiTok.line,
|
|
586
|
+
col: uiTok.col,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
function parseScript() {
|
|
590
|
+
const scriptTok = expect('SCRIPT');
|
|
591
|
+
skipNewlines();
|
|
592
|
+
const rawTok = expect('RAW_JSX');
|
|
593
|
+
return {
|
|
594
|
+
raw: rawTok.value,
|
|
595
|
+
line: scriptTok.line,
|
|
596
|
+
col: scriptTok.col,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
// ── main blueprint parser ──────────────────────────────────────────────
|
|
600
|
+
skipNewlines();
|
|
601
|
+
const blueprintTok = expect('BLUEPRINT');
|
|
602
|
+
const nameTok = expect('IDENT');
|
|
603
|
+
skipNewlines();
|
|
604
|
+
expect('LBRACE');
|
|
605
|
+
skipNewlines();
|
|
606
|
+
let props = [];
|
|
607
|
+
let state = [];
|
|
608
|
+
let computed = [];
|
|
609
|
+
let methods = [];
|
|
610
|
+
let events = { onMount: null, onDestroy: null, onUpdate: null };
|
|
611
|
+
let style = null;
|
|
612
|
+
let ui = null;
|
|
613
|
+
let script = null;
|
|
614
|
+
while (!check('RBRACE') && !check('EOF')) {
|
|
615
|
+
skipNewlines();
|
|
616
|
+
if (check('RBRACE'))
|
|
617
|
+
break;
|
|
618
|
+
const tok = peek();
|
|
619
|
+
if (check('PROPS')) {
|
|
620
|
+
props = parseProps();
|
|
621
|
+
skipNewlines();
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
if (check('STATE')) {
|
|
625
|
+
state = parseState();
|
|
626
|
+
skipNewlines();
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
if (check('COMPUTED')) {
|
|
630
|
+
computed = parseComputed();
|
|
631
|
+
skipNewlines();
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
if (check('METHODS')) {
|
|
635
|
+
methods = parseMethods();
|
|
636
|
+
skipNewlines();
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if (check('EVENTS')) {
|
|
640
|
+
events = parseEvents();
|
|
641
|
+
skipNewlines();
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
if (check('STYLE')) {
|
|
645
|
+
style = parseStyle();
|
|
646
|
+
skipNewlines();
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
if (check('UI')) {
|
|
650
|
+
ui = parseUI();
|
|
651
|
+
skipNewlines();
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
if (check('SCRIPT')) {
|
|
655
|
+
script = parseScript();
|
|
656
|
+
skipNewlines();
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
throw new TardisError(`Unexpected token "${tok.value}" — expected a section keyword (props, state, computed, methods, events, style, ui, script)`, file, tok.line, tok.col);
|
|
660
|
+
}
|
|
661
|
+
expect('RBRACE');
|
|
662
|
+
if (!ui) {
|
|
663
|
+
throw new TardisError(`Blueprint "${nameTok.value}" is missing a ui block — every blueprint must have one`, file, nameTok.line, nameTok.col);
|
|
664
|
+
}
|
|
665
|
+
return {
|
|
666
|
+
name: nameTok.value,
|
|
667
|
+
props,
|
|
668
|
+
state,
|
|
669
|
+
computed,
|
|
670
|
+
methods,
|
|
671
|
+
events,
|
|
672
|
+
style,
|
|
673
|
+
ui,
|
|
674
|
+
script,
|
|
675
|
+
line: blueprintTok.line,
|
|
676
|
+
col: blueprintTok.col,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function compile(ast) {
|
|
681
|
+
const lines = [];
|
|
682
|
+
lines.push(`// generated from ${ast.name}.tardis — do not edit`);
|
|
683
|
+
lines.push(`// edit the .tardis source file instead`);
|
|
684
|
+
lines.push(`import { $runtime } from 'tardisjs/runtime'`);
|
|
685
|
+
lines.push(``);
|
|
686
|
+
lines.push(compilePropsType(ast.name, ast.props));
|
|
687
|
+
lines.push(``);
|
|
688
|
+
lines.push(`export function ${ast.name}(props: ${ast.name}Props = {}) {`);
|
|
689
|
+
lines.push(compilePropsDefaults(ast.props));
|
|
690
|
+
lines.push(``);
|
|
691
|
+
if (ast.state.length > 0) {
|
|
692
|
+
lines.push(compileState(ast.state));
|
|
693
|
+
lines.push(``);
|
|
694
|
+
}
|
|
695
|
+
if (ast.computed.length > 0) {
|
|
696
|
+
lines.push(compileComputed(ast.computed));
|
|
697
|
+
lines.push(``);
|
|
698
|
+
}
|
|
699
|
+
if (ast.methods.length > 0) {
|
|
700
|
+
lines.push(compileMethods(ast.methods));
|
|
701
|
+
lines.push(``);
|
|
702
|
+
}
|
|
703
|
+
if (ast.events.onMount || ast.events.onDestroy || ast.events.onUpdate) {
|
|
704
|
+
lines.push(compileEvents(ast));
|
|
705
|
+
lines.push(``);
|
|
706
|
+
}
|
|
707
|
+
if (ast.style && ast.style.mode !== 'raw') {
|
|
708
|
+
lines.push(compileStyle(ast.style));
|
|
709
|
+
lines.push(``);
|
|
710
|
+
}
|
|
711
|
+
const registrations = [];
|
|
712
|
+
if (ast.state.length > 0)
|
|
713
|
+
registrations.push("state: _state");
|
|
714
|
+
if (ast.methods.length > 0)
|
|
715
|
+
registrations.push("methods: _methods");
|
|
716
|
+
lines.push(` $runtime.register('${ast.name}', { ${registrations.join(", ")} })`);
|
|
717
|
+
lines.push(``);
|
|
718
|
+
let scriptCode = ast.script ? compileScript(ast) : undefined;
|
|
719
|
+
if (ast.style && ast.style.mode === 'raw' && ast.style.raw) {
|
|
720
|
+
const cssInjection = compileRawStyle(ast.style.raw);
|
|
721
|
+
scriptCode = scriptCode ? `${scriptCode}\n${cssInjection}` : cssInjection;
|
|
722
|
+
}
|
|
723
|
+
lines.push(compileUI(ast.ui.raw, ast.name, scriptCode));
|
|
724
|
+
lines.push(`}`);
|
|
725
|
+
return lines.join("\n");
|
|
726
|
+
}
|
|
727
|
+
// ── props type ─────────────────────────────────────────────────────────────
|
|
728
|
+
function compilePropsType(name, props) {
|
|
729
|
+
if (props.length === 0) {
|
|
730
|
+
return `type ${name}Props = Record<string, never>`;
|
|
731
|
+
}
|
|
732
|
+
const fields = props.map((p) => {
|
|
733
|
+
const tsType = propTypeToTS(p.type);
|
|
734
|
+
const optional = p.default !== null || p.type === "function" ? "?" : "";
|
|
735
|
+
return ` ${p.name}${optional}: ${tsType}`;
|
|
736
|
+
});
|
|
737
|
+
return `type ${name}Props = {\n${fields.join("\n")}\n}`;
|
|
738
|
+
}
|
|
739
|
+
function propTypeToTS(type) {
|
|
740
|
+
if (Array.isArray(type)) {
|
|
741
|
+
return type.map((v) => `"${v}"`).join(" | ");
|
|
742
|
+
}
|
|
743
|
+
switch (type) {
|
|
744
|
+
case "string":
|
|
745
|
+
return "string";
|
|
746
|
+
case "number":
|
|
747
|
+
return "number";
|
|
748
|
+
case "boolean":
|
|
749
|
+
return "boolean";
|
|
750
|
+
case "array":
|
|
751
|
+
return "unknown[]";
|
|
752
|
+
case "object":
|
|
753
|
+
return "Record<string, unknown>";
|
|
754
|
+
case "function":
|
|
755
|
+
return "(...args: unknown[]) => unknown";
|
|
756
|
+
default:
|
|
757
|
+
return "unknown";
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
// ── props defaults ─────────────────────────────────────────────────────────
|
|
761
|
+
function compilePropsDefaults(props) {
|
|
762
|
+
if (props.length === 0) {
|
|
763
|
+
return ` const _props = { ...props }`;
|
|
764
|
+
}
|
|
765
|
+
const defaults = props
|
|
766
|
+
.filter((p) => p.default !== null)
|
|
767
|
+
.map((p) => ` ${p.name}: ${formatDefault(p.default, p.type)}`);
|
|
768
|
+
if (defaults.length === 0) {
|
|
769
|
+
return ` const _props = { ...props }`;
|
|
770
|
+
}
|
|
771
|
+
return [
|
|
772
|
+
` const _props = {`,
|
|
773
|
+
defaults.join(",\n"),
|
|
774
|
+
` ...props`,
|
|
775
|
+
` }`,
|
|
776
|
+
].join("\n");
|
|
777
|
+
}
|
|
778
|
+
function formatDefault(val, type) {
|
|
779
|
+
if (val === null)
|
|
780
|
+
return "undefined";
|
|
781
|
+
if (typeof val === "number")
|
|
782
|
+
return String(val);
|
|
783
|
+
if (typeof val === "boolean")
|
|
784
|
+
return String(val);
|
|
785
|
+
if (val === "[]")
|
|
786
|
+
return "[]";
|
|
787
|
+
if (val === "{}")
|
|
788
|
+
return "{}";
|
|
789
|
+
if (typeof val === "string" && val.startsWith("props."))
|
|
790
|
+
return val.replace("props.", "_props.");
|
|
791
|
+
if (typeof val === "string" && val.startsWith("state."))
|
|
792
|
+
return val.replace("state.", "_state.");
|
|
793
|
+
if (Array.isArray(type) || type === "string")
|
|
794
|
+
return `"${val}"`;
|
|
795
|
+
return String(val);
|
|
796
|
+
}
|
|
797
|
+
// ── state ──────────────────────────────────────────────────────────────────
|
|
798
|
+
function compileState(state) {
|
|
799
|
+
const entries = state.map((s) => ` ${s.name}: ${formatDefault(s.default, s.type)}`);
|
|
800
|
+
return [
|
|
801
|
+
` const _state = $runtime.state({`,
|
|
802
|
+
entries.join(",\n"),
|
|
803
|
+
` })`,
|
|
804
|
+
].join("\n");
|
|
805
|
+
}
|
|
806
|
+
// ── computed ───────────────────────────────────────────────────────────────
|
|
807
|
+
function compileComputed(computed) {
|
|
808
|
+
const getters = computed.map((c) => {
|
|
809
|
+
const expr = rewriteRefs(c.expr);
|
|
810
|
+
return ` get ${c.name}() { return ${expr} }`;
|
|
811
|
+
});
|
|
812
|
+
return [` const _computed = {`, getters.join(",\n"), ` }`].join("\n");
|
|
813
|
+
}
|
|
814
|
+
// ── methods ────────────────────────────────────────────────────────────────
|
|
815
|
+
function compileMethods(methods) {
|
|
816
|
+
const fns = methods.map((m) => {
|
|
817
|
+
const params = m.params.join(", ");
|
|
818
|
+
const body = rewriteRefs(m.body);
|
|
819
|
+
return ` ${m.name}: (${params}) => ${body}`;
|
|
820
|
+
});
|
|
821
|
+
return [` const _methods = {`, fns.join(",\n"), ` }`].join("\n");
|
|
822
|
+
}
|
|
823
|
+
// ── events ─────────────────────────────────────────────────────────────────
|
|
824
|
+
function compileEvents(ast) {
|
|
825
|
+
const lines = [];
|
|
826
|
+
lines.push(` $runtime.events({`);
|
|
827
|
+
if (ast.events.onMount)
|
|
828
|
+
lines.push(` onMount: () => { ${rewriteRefs(ast.events.onMount)} },`);
|
|
829
|
+
if (ast.events.onDestroy)
|
|
830
|
+
lines.push(` onDestroy: () => { ${rewriteRefs(ast.events.onDestroy)} },`);
|
|
831
|
+
if (ast.events.onUpdate)
|
|
832
|
+
lines.push(` onUpdate: () => { ${rewriteRefs(ast.events.onUpdate)} },`);
|
|
833
|
+
lines.push(` })`);
|
|
834
|
+
return lines.join("\n");
|
|
835
|
+
}
|
|
836
|
+
// ── style ──────────────────────────────────────────────────────────────────
|
|
837
|
+
function compileStyle(style) {
|
|
838
|
+
const rules = style.rules.map((r) => ` "${r.key}": "${r.value}"`);
|
|
839
|
+
return [
|
|
840
|
+
` const _styles = $runtime.styles("${style.mode}", {`,
|
|
841
|
+
rules.join(",\n"),
|
|
842
|
+
` }, _props, _state)`,
|
|
843
|
+
].join("\n");
|
|
844
|
+
}
|
|
845
|
+
function compileRawStyle(raw) {
|
|
846
|
+
const escaped = raw.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
847
|
+
const lines = [];
|
|
848
|
+
lines.push(` // raw style`);
|
|
849
|
+
lines.push(` ;(() => {`);
|
|
850
|
+
lines.push(` const _style = document.createElement('style')`);
|
|
851
|
+
lines.push(` _style.textContent = \`${escaped}\``);
|
|
852
|
+
lines.push(` document.head.appendChild(_style)`);
|
|
853
|
+
lines.push(` })()`);
|
|
854
|
+
return lines.join('\n');
|
|
855
|
+
}
|
|
856
|
+
// ── ref rewriter ───────────────────────────────────────────────────────────
|
|
857
|
+
function rewriteRefs(expr) {
|
|
858
|
+
const rewritten = expr
|
|
859
|
+
.replace(/(?<![_a-zA-Z])state\./g, "_state.")
|
|
860
|
+
.replace(/(?<![_a-zA-Z])props\./g, "_props.")
|
|
861
|
+
.replace(/(?<![_a-zA-Z])computed\./g, "_computed.")
|
|
862
|
+
.replace(/(?<![_a-zA-Z])methods\./g, "_methods.");
|
|
863
|
+
return rewritten
|
|
864
|
+
.replace(/\$update\(\s*_state\.([a-zA-Z_$][\w$]*)\s*,/g, "$runtime.update(_state, '$1',")
|
|
865
|
+
.replace(/\$toggle\(\s*_state\.([a-zA-Z_$][\w$]*)\s*\)/g, "$runtime.toggle(_state, '$1')");
|
|
866
|
+
}
|
|
867
|
+
// ── ui compiler ────────────────────────────────────────────────────────────
|
|
868
|
+
function compileUI(raw, componentName, scriptCode) {
|
|
869
|
+
const lines = [];
|
|
870
|
+
lines.push(` // ui`);
|
|
871
|
+
lines.push(` const _root = (() => {`);
|
|
872
|
+
lines.push(compileUINode(raw.trim(), 2));
|
|
873
|
+
lines.push(` })()`);
|
|
874
|
+
if (scriptCode) {
|
|
875
|
+
lines.push(``);
|
|
876
|
+
lines.push(scriptCode);
|
|
877
|
+
}
|
|
878
|
+
lines.push(` return _root`);
|
|
879
|
+
return lines.join("\n");
|
|
880
|
+
}
|
|
881
|
+
function rewriteElementSelectors(raw) {
|
|
882
|
+
// {<tag id="x" class="y"></tag>} or {<tag id="x" class="y" />} → $el.querySelector('tag#x.y')
|
|
883
|
+
return raw.replace(/\{<(\w+)((?:\s+[\w-]+="[^"]*")*)\s*(?:\/>|>\s*<\/\1>)\}/g, (_match, tag, attrsRaw) => {
|
|
884
|
+
let selector = tag;
|
|
885
|
+
const idMatch = attrsRaw.match(/\bid="([^"]*)"/);
|
|
886
|
+
if (idMatch)
|
|
887
|
+
selector = `${tag}#${idMatch[1]}`;
|
|
888
|
+
const classMatch = attrsRaw.match(/\bclass="([^"]*)"/);
|
|
889
|
+
if (classMatch) {
|
|
890
|
+
selector += classMatch[1].split(/\s+/).map(c => `.${c}`).join('');
|
|
891
|
+
}
|
|
892
|
+
return `$el.querySelector('${selector}')`;
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
function compileScript(ast) {
|
|
896
|
+
const lines = [];
|
|
897
|
+
lines.push(` // script`);
|
|
898
|
+
lines.push(` requestAnimationFrame(() => {`);
|
|
899
|
+
lines.push(` const $el = _root`);
|
|
900
|
+
lines.push(` const props = _props`);
|
|
901
|
+
if (ast.state.length > 0)
|
|
902
|
+
lines.push(` const state = _state`);
|
|
903
|
+
if (ast.methods.length > 0)
|
|
904
|
+
lines.push(` const methods = _methods`);
|
|
905
|
+
if (ast.computed.length > 0)
|
|
906
|
+
lines.push(` const computed = _computed`);
|
|
907
|
+
const transformed = rewriteElementSelectors(ast.script.raw);
|
|
908
|
+
for (const line of transformed.split('\n')) {
|
|
909
|
+
lines.push(` ${line}`);
|
|
910
|
+
}
|
|
911
|
+
lines.push(` })`);
|
|
912
|
+
return lines.join('\n');
|
|
913
|
+
}
|
|
914
|
+
function compileUINode(raw, depth) {
|
|
915
|
+
const indent = " ".repeat(depth);
|
|
916
|
+
const lines = [];
|
|
917
|
+
// $if
|
|
918
|
+
const ifMatch = raw.match(/^\$if\((.+?)\)\s*\{([\s\S]*)\}/);
|
|
919
|
+
if (ifMatch) {
|
|
920
|
+
const condition = rewriteRefs(ifMatch[1].trim());
|
|
921
|
+
const inner = compileUINode(ifMatch[2].trim(), depth + 1);
|
|
922
|
+
lines.push(`${indent}return $runtime.if(() => ${condition}, () => {`);
|
|
923
|
+
lines.push(inner);
|
|
924
|
+
lines.push(`${indent}})`);
|
|
925
|
+
return lines.join("\n");
|
|
926
|
+
}
|
|
927
|
+
// $each
|
|
928
|
+
const eachMatch = raw.match(/^\$each\((.+?),\s*\((\w+)\)\s*=>\s*\{([\s\S]*)\}\s*\)/);
|
|
929
|
+
if (eachMatch) {
|
|
930
|
+
const arrayRef = rewriteRefs(eachMatch[1].trim());
|
|
931
|
+
const itemVar = eachMatch[2];
|
|
932
|
+
const inner = compileUINode(eachMatch[3].trim(), depth + 1);
|
|
933
|
+
lines.push(`${indent}return $runtime.each(() => ${arrayRef}, (${itemVar}) => {`);
|
|
934
|
+
lines.push(inner);
|
|
935
|
+
lines.push(`${indent}})`);
|
|
936
|
+
return lines.join("\n");
|
|
937
|
+
}
|
|
938
|
+
// $show
|
|
939
|
+
const showMatch = raw.match(/^\$show\((.+?)\)\s*\{([\s\S]*)\}/);
|
|
940
|
+
if (showMatch) {
|
|
941
|
+
const condition = rewriteRefs(showMatch[1].trim());
|
|
942
|
+
const inner = compileUINode(showMatch[2].trim(), depth + 1);
|
|
943
|
+
lines.push(`${indent}return $runtime.show(() => ${condition}, () => {`);
|
|
944
|
+
lines.push(inner);
|
|
945
|
+
lines.push(`${indent}})`);
|
|
946
|
+
return lines.join("\n");
|
|
947
|
+
}
|
|
948
|
+
// {} chain syntax
|
|
949
|
+
const chainMatch = raw.match(/^\{([\s\S]+?)\}((?:\s*\.\w+\([^)]*\))+)/);
|
|
950
|
+
if (chainMatch) {
|
|
951
|
+
const innerEl = chainMatch[1].trim();
|
|
952
|
+
const chainStr = chainMatch[2].trim();
|
|
953
|
+
const elCode = compileElement(innerEl, depth);
|
|
954
|
+
const chains = parseChains(chainStr);
|
|
955
|
+
lines.push(`${indent}const _chained = (() => {`);
|
|
956
|
+
lines.push(elCode);
|
|
957
|
+
lines.push(`${indent}})()`);
|
|
958
|
+
for (const chain of chains) {
|
|
959
|
+
lines.push(`${indent}$runtime.chain(_chained, '${chain.event}', ${rewriteRefs(chain.handler)})`);
|
|
960
|
+
}
|
|
961
|
+
lines.push(`${indent}return _chained`);
|
|
962
|
+
return lines.join("\n");
|
|
963
|
+
}
|
|
964
|
+
return compileElement(raw, depth);
|
|
965
|
+
}
|
|
966
|
+
function compileElement(raw, depth) {
|
|
967
|
+
const indent = " ".repeat(depth);
|
|
968
|
+
const lines = [];
|
|
969
|
+
const trimmed = raw.trim();
|
|
970
|
+
// self-closing tag — find end accounting for braces
|
|
971
|
+
const selfCloseEnd = findSelfCloseEnd(trimmed);
|
|
972
|
+
if (selfCloseEnd >= 0) {
|
|
973
|
+
const tagStr = trimmed.slice(0, selfCloseEnd + 1);
|
|
974
|
+
const tagNameMatch = tagStr.match(/^<([a-zA-Z][a-zA-Z0-9]*)/);
|
|
975
|
+
if (tagNameMatch) {
|
|
976
|
+
const tag = tagNameMatch[1];
|
|
977
|
+
const attrsRaw = tagStr.slice(tag.length + 1, -2).trim(); // between name and />
|
|
978
|
+
if (/^[A-Z]/.test(tag)) {
|
|
979
|
+
lines.push(`${indent}return $runtime.component('${tag}', { ${compileComponentProps(attrsRaw)} })`);
|
|
980
|
+
return lines.join("\n");
|
|
981
|
+
}
|
|
982
|
+
lines.push(`${indent}const _el_${depth} = document.createElement('${tag}')`);
|
|
983
|
+
for (const attr of parseAttributes(attrsRaw)) {
|
|
984
|
+
lines.push(...compileAttr(attr, depth, indent));
|
|
985
|
+
}
|
|
986
|
+
lines.push(`${indent}return _el_${depth}`);
|
|
987
|
+
return lines.join("\n");
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
// opening tag — find end accounting for braces
|
|
991
|
+
const openTagEndIdx = findOpenTagEnd(trimmed);
|
|
992
|
+
if (openTagEndIdx < 0) {
|
|
993
|
+
// plain text or expression
|
|
994
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
995
|
+
lines.push(`${indent}return $runtime.text(() => ${rewriteRefs(trimmed.slice(1, -1).trim())})`);
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
lines.push(`${indent}return $runtime.text(${JSON.stringify(decodeEntities(trimmed))})`);
|
|
999
|
+
}
|
|
1000
|
+
return lines.join("\n");
|
|
1001
|
+
}
|
|
1002
|
+
const openTagStr = trimmed.slice(0, openTagEndIdx + 1);
|
|
1003
|
+
const tagNameMatch = openTagStr.match(/^<([a-zA-Z][a-zA-Z0-9]*)/);
|
|
1004
|
+
if (!tagNameMatch) {
|
|
1005
|
+
lines.push(`${indent}return $runtime.text(${JSON.stringify(decodeEntities(trimmed))})`);
|
|
1006
|
+
return lines.join("\n");
|
|
1007
|
+
}
|
|
1008
|
+
const tag = tagNameMatch[1];
|
|
1009
|
+
const attrsRaw = openTagStr.slice(tag.length + 1, -1).trim();
|
|
1010
|
+
if (/^[A-Z]/.test(tag)) {
|
|
1011
|
+
lines.push(`${indent}return $runtime.component('${tag}', { ${compileComponentProps(attrsRaw)} })`);
|
|
1012
|
+
return lines.join("\n");
|
|
1013
|
+
}
|
|
1014
|
+
const innerContent = findInnerContent(trimmed, tag, openTagEndIdx + 1);
|
|
1015
|
+
lines.push(`${indent}const _el_${depth} = document.createElement('${tag}')`);
|
|
1016
|
+
for (const attr of parseAttributes(attrsRaw)) {
|
|
1017
|
+
lines.push(...compileAttr(attr, depth, indent));
|
|
1018
|
+
}
|
|
1019
|
+
const children = splitChildren(innerContent);
|
|
1020
|
+
let childIndex = 0;
|
|
1021
|
+
for (let ci = 0; ci < children.length; ci++) {
|
|
1022
|
+
const child = children[ci];
|
|
1023
|
+
// normalize whitespace: trim first child's leading, last child's trailing
|
|
1024
|
+
// but preserve boundary spaces for middle children (e.g. "text " before <span>)
|
|
1025
|
+
let ct = child.replace(/\s*\n\s*/g, ' ');
|
|
1026
|
+
if (ci === 0)
|
|
1027
|
+
ct = ct.replace(/^\s+/, '');
|
|
1028
|
+
if (ci === children.length - 1)
|
|
1029
|
+
ct = ct.replace(/\s+$/, '');
|
|
1030
|
+
if (!ct)
|
|
1031
|
+
continue;
|
|
1032
|
+
if (ct.startsWith('{') && ct.endsWith('}') && !ct.startsWith('{<')) {
|
|
1033
|
+
const expr = rewriteRefs(ct.slice(1, -1).trim());
|
|
1034
|
+
lines.push(`${indent}const _text_${depth} = document.createTextNode('')`);
|
|
1035
|
+
lines.push(`${indent}$runtime.bind(_text_${depth}, 'textContent', () => String(${expr}))`);
|
|
1036
|
+
lines.push(`${indent}_el_${depth}.appendChild(_text_${depth})`);
|
|
1037
|
+
}
|
|
1038
|
+
else if (ct.trim().startsWith('<') ||
|
|
1039
|
+
ct.trim().startsWith('{<') ||
|
|
1040
|
+
ct.trim().startsWith('$if(') ||
|
|
1041
|
+
ct.trim().startsWith('$each(') ||
|
|
1042
|
+
ct.trim().startsWith('$show(')) {
|
|
1043
|
+
// emit leading whitespace as a text node (preserves spaces between text and inline elements)
|
|
1044
|
+
const wsMatch = ct.match(/^(\s+)/);
|
|
1045
|
+
if (wsMatch) {
|
|
1046
|
+
lines.push(`${indent}_el_${depth}.appendChild(document.createTextNode(" "))`);
|
|
1047
|
+
}
|
|
1048
|
+
const childVar = `_child_${depth}_${childIndex}`;
|
|
1049
|
+
lines.push(`${indent}const ${childVar} = (() => {`);
|
|
1050
|
+
lines.push(compileUINode(ct, depth + 1));
|
|
1051
|
+
lines.push(`${indent}})()`);
|
|
1052
|
+
lines.push(`${indent}if (${childVar} instanceof Node) _el_${depth}.appendChild(${childVar})`);
|
|
1053
|
+
}
|
|
1054
|
+
else {
|
|
1055
|
+
const interpolationRe = /\{([^{}]+)\}/g;
|
|
1056
|
+
let cursor = 0;
|
|
1057
|
+
let interpolationIndex = 0;
|
|
1058
|
+
let hasInterpolation = false;
|
|
1059
|
+
let match;
|
|
1060
|
+
while ((match = interpolationRe.exec(ct)) !== null) {
|
|
1061
|
+
hasInterpolation = true;
|
|
1062
|
+
const textBefore = ct.slice(cursor, match.index);
|
|
1063
|
+
if (textBefore) {
|
|
1064
|
+
lines.push(`${indent}_el_${depth}.appendChild(document.createTextNode(${JSON.stringify(decodeEntities(textBefore))}))`);
|
|
1065
|
+
}
|
|
1066
|
+
const textVar = `_text_${depth}_${childIndex}_${interpolationIndex}`;
|
|
1067
|
+
const expr = rewriteRefs(match[1].trim());
|
|
1068
|
+
lines.push(`${indent}const ${textVar} = document.createTextNode('')`);
|
|
1069
|
+
lines.push(`${indent}$runtime.bind(${textVar}, 'textContent', () => String(${expr}))`);
|
|
1070
|
+
lines.push(`${indent}_el_${depth}.appendChild(${textVar})`);
|
|
1071
|
+
cursor = match.index + match[0].length;
|
|
1072
|
+
interpolationIndex++;
|
|
1073
|
+
}
|
|
1074
|
+
if (hasInterpolation) {
|
|
1075
|
+
const tail = ct.slice(cursor);
|
|
1076
|
+
if (tail) {
|
|
1077
|
+
lines.push(`${indent}_el_${depth}.appendChild(document.createTextNode(${JSON.stringify(decodeEntities(tail))}))`);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
else {
|
|
1081
|
+
lines.push(`${indent}_el_${depth}.appendChild(document.createTextNode(${JSON.stringify(decodeEntities(ct))}))`);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
childIndex++;
|
|
1085
|
+
}
|
|
1086
|
+
lines.push(`${indent}return _el_${depth}`);
|
|
1087
|
+
return lines.join("\n");
|
|
1088
|
+
}
|
|
1089
|
+
function compileAttr(attr, depth, indent) {
|
|
1090
|
+
const lines = [];
|
|
1091
|
+
if (attr.name.startsWith("@")) {
|
|
1092
|
+
const event = attr.name.slice(1);
|
|
1093
|
+
const handler = rewriteRefs(isReactive(attr.value) ? stripBraces(attr.value) : attr.value);
|
|
1094
|
+
lines.push(`${indent}_el_${depth}.addEventListener('${event}', ${handler})`);
|
|
1095
|
+
}
|
|
1096
|
+
else if (attr.name === "class") {
|
|
1097
|
+
if (isReactive(attr.value)) {
|
|
1098
|
+
const expr = rewriteRefs(stripBraces(attr.value));
|
|
1099
|
+
lines.push(`${indent}$runtime.bindClass(_el_${depth}, () => _styles ? $runtime.resolveStyles(_styles, ${expr}) : ${expr})`);
|
|
1100
|
+
}
|
|
1101
|
+
else {
|
|
1102
|
+
lines.push(`${indent}_el_${depth}.className = ${JSON.stringify(attr.value)}`);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
else if (isReactive(attr.value)) {
|
|
1106
|
+
const expr = rewriteRefs(stripBraces(attr.value));
|
|
1107
|
+
lines.push(`${indent}$runtime.bindAttr(_el_${depth}, '${attr.name}', () => ${expr})`);
|
|
1108
|
+
}
|
|
1109
|
+
else {
|
|
1110
|
+
lines.push(`${indent}_el_${depth}.setAttribute('${attr.name}', ${JSON.stringify(attr.value)})`);
|
|
1111
|
+
}
|
|
1112
|
+
return lines;
|
|
1113
|
+
}
|
|
1114
|
+
// ── attribute parser ───────────────────────────────────────────────────────
|
|
1115
|
+
function parseAttributes(raw) {
|
|
1116
|
+
const attrs = [];
|
|
1117
|
+
let i = 0;
|
|
1118
|
+
while (i < raw.length) {
|
|
1119
|
+
while (i < raw.length && /\s/.test(raw[i]))
|
|
1120
|
+
i++;
|
|
1121
|
+
if (i >= raw.length)
|
|
1122
|
+
break;
|
|
1123
|
+
let name = "";
|
|
1124
|
+
while (i < raw.length && !/[\s=]/.test(raw[i]))
|
|
1125
|
+
name += raw[i++];
|
|
1126
|
+
if (!name) {
|
|
1127
|
+
i++;
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
while (i < raw.length && /\s/.test(raw[i]))
|
|
1131
|
+
i++;
|
|
1132
|
+
if (raw[i] !== "=")
|
|
1133
|
+
continue;
|
|
1134
|
+
i++;
|
|
1135
|
+
while (i < raw.length && /\s/.test(raw[i]))
|
|
1136
|
+
i++;
|
|
1137
|
+
if (raw[i] === '"') {
|
|
1138
|
+
i++;
|
|
1139
|
+
let value = "";
|
|
1140
|
+
while (i < raw.length && raw[i] !== '"')
|
|
1141
|
+
value += raw[i++];
|
|
1142
|
+
i++;
|
|
1143
|
+
attrs.push({ name, value });
|
|
1144
|
+
}
|
|
1145
|
+
else if (raw[i] === "{") {
|
|
1146
|
+
let depth = 0;
|
|
1147
|
+
let expr = "";
|
|
1148
|
+
while (i < raw.length) {
|
|
1149
|
+
const ch = raw[i];
|
|
1150
|
+
if (ch === "{")
|
|
1151
|
+
depth++;
|
|
1152
|
+
if (ch === "}") {
|
|
1153
|
+
depth--;
|
|
1154
|
+
if (depth === 0) {
|
|
1155
|
+
expr += ch;
|
|
1156
|
+
i++;
|
|
1157
|
+
break;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
expr += ch;
|
|
1161
|
+
i++;
|
|
1162
|
+
}
|
|
1163
|
+
attrs.push({ name, value: expr });
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
return attrs;
|
|
1167
|
+
}
|
|
1168
|
+
// ── chain parser ───────────────────────────────────────────────────────────
|
|
1169
|
+
function parseChains(raw) {
|
|
1170
|
+
const chains = [];
|
|
1171
|
+
const re = /\.(\w+)\(([^)]*)\)/g;
|
|
1172
|
+
let m;
|
|
1173
|
+
const eventMap = {
|
|
1174
|
+
click: "click",
|
|
1175
|
+
hover: "mouseenter",
|
|
1176
|
+
blur: "blur",
|
|
1177
|
+
focus: "focus",
|
|
1178
|
+
keydown: "keydown",
|
|
1179
|
+
keyup: "keyup",
|
|
1180
|
+
change: "change",
|
|
1181
|
+
submit: "submit",
|
|
1182
|
+
scroll: "scroll",
|
|
1183
|
+
drag: "dragstart",
|
|
1184
|
+
mount: "__mount",
|
|
1185
|
+
destroy: "__destroy",
|
|
1186
|
+
};
|
|
1187
|
+
while ((m = re.exec(raw)) !== null) {
|
|
1188
|
+
chains.push({ event: eventMap[m[1]] ?? m[1], handler: m[2].trim() });
|
|
1189
|
+
}
|
|
1190
|
+
return chains;
|
|
1191
|
+
}
|
|
1192
|
+
// ── component props compiler ───────────────────────────────────────────────
|
|
1193
|
+
function compileComponentProps(raw) {
|
|
1194
|
+
return parseAttributes(raw)
|
|
1195
|
+
.map((a) => {
|
|
1196
|
+
if (isReactive(a.value))
|
|
1197
|
+
return `${a.name}: () => ${rewriteRefs(stripBraces(a.value))}`;
|
|
1198
|
+
return `${a.name}: ${JSON.stringify(a.value)}`;
|
|
1199
|
+
})
|
|
1200
|
+
.join(", ");
|
|
1201
|
+
}
|
|
1202
|
+
// ── helpers ────────────────────────────────────────────────────────────────
|
|
1203
|
+
function isReactive(value) {
|
|
1204
|
+
return value.startsWith("{") && value.endsWith("}");
|
|
1205
|
+
}
|
|
1206
|
+
function stripBraces(value) {
|
|
1207
|
+
return value.slice(1, -1).trim();
|
|
1208
|
+
}
|
|
1209
|
+
// find end index of opening tag > accounting for {} inside attribute values
|
|
1210
|
+
function findOpenTagEnd(raw) {
|
|
1211
|
+
const end = findFirstTagEnd(raw);
|
|
1212
|
+
if (end < 0)
|
|
1213
|
+
return -1;
|
|
1214
|
+
return isSelfClosingTag(raw, end) ? -1 : end;
|
|
1215
|
+
}
|
|
1216
|
+
// find end index of self-closing tag /> accounting for {} inside attribute values
|
|
1217
|
+
function findSelfCloseEnd(raw) {
|
|
1218
|
+
const end = findFirstTagEnd(raw);
|
|
1219
|
+
if (end < 0)
|
|
1220
|
+
return -1;
|
|
1221
|
+
return isSelfClosingTag(raw, end) ? end : -1;
|
|
1222
|
+
}
|
|
1223
|
+
function findFirstTagEnd(raw) {
|
|
1224
|
+
if (!raw.startsWith("<"))
|
|
1225
|
+
return -1;
|
|
1226
|
+
let i = 1;
|
|
1227
|
+
let bracesDepth = 0;
|
|
1228
|
+
let quote = null;
|
|
1229
|
+
while (i < raw.length) {
|
|
1230
|
+
const ch = raw[i];
|
|
1231
|
+
if (quote) {
|
|
1232
|
+
if (ch === quote)
|
|
1233
|
+
quote = null;
|
|
1234
|
+
i++;
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
if (ch === '"' || ch === "'") {
|
|
1238
|
+
quote = ch;
|
|
1239
|
+
}
|
|
1240
|
+
else if (ch === "{") {
|
|
1241
|
+
bracesDepth++;
|
|
1242
|
+
}
|
|
1243
|
+
else if (ch === "}") {
|
|
1244
|
+
bracesDepth--;
|
|
1245
|
+
}
|
|
1246
|
+
else if (ch === ">" && bracesDepth === 0) {
|
|
1247
|
+
return i;
|
|
1248
|
+
}
|
|
1249
|
+
i++;
|
|
1250
|
+
}
|
|
1251
|
+
return -1;
|
|
1252
|
+
}
|
|
1253
|
+
function isSelfClosingTag(raw, end) {
|
|
1254
|
+
let i = end - 1;
|
|
1255
|
+
while (i >= 0 && /\s/.test(raw[i]))
|
|
1256
|
+
i--;
|
|
1257
|
+
return raw[i] === "/";
|
|
1258
|
+
}
|
|
1259
|
+
function findInnerContent(raw, tag, startFrom) {
|
|
1260
|
+
let depth = 1;
|
|
1261
|
+
let i = startFrom;
|
|
1262
|
+
while (i < raw.length && depth > 0) {
|
|
1263
|
+
if (raw.startsWith(`<${tag}`, i) &&
|
|
1264
|
+
/[\s>/]/.test(raw[i + tag.length + 1] ?? "")) {
|
|
1265
|
+
depth++;
|
|
1266
|
+
i += tag.length + 2;
|
|
1267
|
+
}
|
|
1268
|
+
else if (raw.startsWith(`</${tag}>`, i)) {
|
|
1269
|
+
depth--;
|
|
1270
|
+
if (depth === 0)
|
|
1271
|
+
break;
|
|
1272
|
+
i += tag.length + 3;
|
|
1273
|
+
}
|
|
1274
|
+
else {
|
|
1275
|
+
i++;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
return raw.slice(startFrom, i).trim();
|
|
1279
|
+
}
|
|
1280
|
+
function decodeEntities(s) {
|
|
1281
|
+
return s
|
|
1282
|
+
.replace(/&#(\d+);/g, (_m, code) => String.fromCharCode(Number(code)))
|
|
1283
|
+
.replace(/&#x([0-9a-fA-F]+);/g, (_m, hex) => String.fromCharCode(parseInt(hex, 16)))
|
|
1284
|
+
.replace(/&/g, '&')
|
|
1285
|
+
.replace(/</g, '<')
|
|
1286
|
+
.replace(/>/g, '>')
|
|
1287
|
+
.replace(/"/g, '"')
|
|
1288
|
+
.replace(/'/g, "'")
|
|
1289
|
+
.replace(/ /g, '\u00A0');
|
|
1290
|
+
}
|
|
1291
|
+
function normalizeWs(s) {
|
|
1292
|
+
// collapse newlines (and surrounding whitespace) to a single space
|
|
1293
|
+
// but preserve meaningful spaces at word/element boundaries
|
|
1294
|
+
return s.replace(/\s*\n\s*/g, ' ');
|
|
1295
|
+
}
|
|
1296
|
+
function splitChildren(raw) {
|
|
1297
|
+
const parts = [];
|
|
1298
|
+
let depth = 0;
|
|
1299
|
+
let current = "";
|
|
1300
|
+
let i = 0;
|
|
1301
|
+
while (i < raw.length) {
|
|
1302
|
+
const ch = raw[i];
|
|
1303
|
+
if (ch === "<") {
|
|
1304
|
+
if (raw[i + 1] === "/") {
|
|
1305
|
+
if (depth === 0) {
|
|
1306
|
+
// closing tag for PARENT element — push current and skip tag
|
|
1307
|
+
const n = normalizeWs(current);
|
|
1308
|
+
if (n.trim())
|
|
1309
|
+
parts.push(n);
|
|
1310
|
+
current = "";
|
|
1311
|
+
while (i < raw.length && raw[i] !== ">")
|
|
1312
|
+
i++;
|
|
1313
|
+
i++;
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
depth--;
|
|
1317
|
+
if (depth === 0) {
|
|
1318
|
+
// just closed a CHILD element back to top level
|
|
1319
|
+
// accumulate through closing tag end, then split
|
|
1320
|
+
while (i < raw.length && raw[i] !== ">") {
|
|
1321
|
+
current += raw[i];
|
|
1322
|
+
i++;
|
|
1323
|
+
}
|
|
1324
|
+
if (i < raw.length) {
|
|
1325
|
+
current += raw[i]; // the >
|
|
1326
|
+
i++;
|
|
1327
|
+
}
|
|
1328
|
+
const n = normalizeWs(current);
|
|
1329
|
+
if (n.trim())
|
|
1330
|
+
parts.push(n);
|
|
1331
|
+
current = "";
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
else if (raw[i + 1] !== "!") {
|
|
1336
|
+
if (!isSelfClosingFrom(raw, i)) {
|
|
1337
|
+
depth++;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
if (ch === "{")
|
|
1342
|
+
depth++;
|
|
1343
|
+
if (ch === "}")
|
|
1344
|
+
depth--;
|
|
1345
|
+
current += ch;
|
|
1346
|
+
i++;
|
|
1347
|
+
if (depth === 0 && current.trim()) {
|
|
1348
|
+
const next = raw.slice(i).trimStart();
|
|
1349
|
+
if (next.startsWith("<") ||
|
|
1350
|
+
next.startsWith("$") ||
|
|
1351
|
+
next.startsWith("{")) {
|
|
1352
|
+
const n = normalizeWs(current);
|
|
1353
|
+
if (n.trim())
|
|
1354
|
+
parts.push(n);
|
|
1355
|
+
current = "";
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
const n = normalizeWs(current);
|
|
1360
|
+
if (n.trim())
|
|
1361
|
+
parts.push(n);
|
|
1362
|
+
return parts.filter(p => p.trim().length > 0);
|
|
1363
|
+
}
|
|
1364
|
+
// look-ahead to determine if a tag starting at tagStart is self-closing (ends with />)
|
|
1365
|
+
function isSelfClosingFrom(raw, tagStart) {
|
|
1366
|
+
let j = tagStart + 1;
|
|
1367
|
+
let inQuote = null;
|
|
1368
|
+
let braceDepth = 0;
|
|
1369
|
+
while (j < raw.length) {
|
|
1370
|
+
const c = raw[j];
|
|
1371
|
+
if (inQuote) {
|
|
1372
|
+
if (c === inQuote)
|
|
1373
|
+
inQuote = null;
|
|
1374
|
+
}
|
|
1375
|
+
else if (c === '"' || c === "'") {
|
|
1376
|
+
inQuote = c;
|
|
1377
|
+
}
|
|
1378
|
+
else if (c === "{") {
|
|
1379
|
+
braceDepth++;
|
|
1380
|
+
}
|
|
1381
|
+
else if (c === "}") {
|
|
1382
|
+
braceDepth--;
|
|
1383
|
+
}
|
|
1384
|
+
else if (c === ">" && braceDepth === 0) {
|
|
1385
|
+
let k = j - 1;
|
|
1386
|
+
while (k > tagStart && /\s/.test(raw[k]))
|
|
1387
|
+
k--;
|
|
1388
|
+
return raw[k] === "/";
|
|
1389
|
+
}
|
|
1390
|
+
j++;
|
|
1391
|
+
}
|
|
1392
|
+
return false;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
class CLIError extends Error {
|
|
1396
|
+
constructor(message) {
|
|
1397
|
+
super(message);
|
|
1398
|
+
this.name = 'CLIError';
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
const STARTER_INDEX = `blueprint Home {
|
|
1402
|
+
state {
|
|
1403
|
+
count: number = 0
|
|
1404
|
+
}
|
|
1405
|
+
methods {
|
|
1406
|
+
increment: () => $update(state.count, state.count + 1)
|
|
1407
|
+
decrement: () => $update(state.count, state.count - 1)
|
|
1408
|
+
reset: () => $update(state.count, 0)
|
|
1409
|
+
}
|
|
1410
|
+
style(tailwind) {
|
|
1411
|
+
page: "min-h-screen bg-black text-zinc-100 antialiased"
|
|
1412
|
+
|
|
1413
|
+
container: "relative mx-auto flex min-h-screen w-full max-w-3xl items-center justify-center px-6"
|
|
1414
|
+
|
|
1415
|
+
bgGlow: "absolute inset-0 -z-10 bg-[radial-gradient(circle_at_center,rgba(0,255,255,0.08),transparent_60%)]"
|
|
1416
|
+
|
|
1417
|
+
card: "relative w-full rounded-2xl border border-white/10 bg-zinc-900/60 backdrop-blur-xl p-10 shadow-[0_0_60px_rgba(0,255,255,0.08)]"
|
|
1418
|
+
|
|
1419
|
+
label: "text-[10px] tracking-[0.2em] uppercase text-zinc-500"
|
|
1420
|
+
title: "mt-3 text-3xl font-semibold tracking-tight"
|
|
1421
|
+
subtitle: "mt-2 text-sm text-zinc-400"
|
|
1422
|
+
|
|
1423
|
+
valueWrap: "mt-10 rounded-xl border border-white/10 bg-black/40 px-6 py-8 text-center shadow-inner"
|
|
1424
|
+
value: "text-6xl font-semibold tracking-tight"
|
|
1425
|
+
|
|
1426
|
+
actions: "mt-8 flex items-center justify-center gap-4"
|
|
1427
|
+
|
|
1428
|
+
btnPrimary: "rounded-lg bg-cyan-400 px-5 py-2 text-sm font-medium text-black transition-all duration-150 hover:bg-cyan-300 active:scale-[0.96] shadow-[0_0_20px_rgba(0,255,255,0.3)]"
|
|
1429
|
+
|
|
1430
|
+
btnSecondary: "rounded-lg border border-white/10 bg-white/5 px-5 py-2 text-sm font-medium text-zinc-200 transition-all duration-150 hover:bg-white/10 active:scale-[0.96]"
|
|
1431
|
+
|
|
1432
|
+
note: "mt-8 text-center text-xs text-zinc-600"
|
|
1433
|
+
}
|
|
1434
|
+
ui {
|
|
1435
|
+
<main class={"page"}>
|
|
1436
|
+
<div class={"bgGlow"}></div>
|
|
1437
|
+
<section class={"container"}>
|
|
1438
|
+
<div class={"card"}>
|
|
1439
|
+
<p class={"label"}>TardisJS Starter</p>
|
|
1440
|
+
<h1 class={"title"}>Counter</h1>
|
|
1441
|
+
<p class={"subtitle"}>A simple starting point with clean defaults.</p>
|
|
1442
|
+
|
|
1443
|
+
<div class={"valueWrap"}>
|
|
1444
|
+
<p class={"value"}>{state.count}</p>
|
|
1445
|
+
</div>
|
|
1446
|
+
|
|
1447
|
+
<div class={"actions"}>
|
|
1448
|
+
<button class={"btnSecondary"} @click={methods.decrement}>-1</button>
|
|
1449
|
+
<button class={"btnPrimary"} @click={methods.increment}>+1</button>
|
|
1450
|
+
<button class={"btnSecondary"} @click={methods.reset}>Reset</button>
|
|
1451
|
+
</div>
|
|
1452
|
+
|
|
1453
|
+
<p class={"note"}>Edit <code>pages/index.tardis</code> and build from here.</p>
|
|
1454
|
+
</div>
|
|
1455
|
+
</section>
|
|
1456
|
+
</main>
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
`;
|
|
1460
|
+
const STARTER_BUTTON = `blueprint Button {
|
|
1461
|
+
props {
|
|
1462
|
+
label: string = "Action"
|
|
1463
|
+
variant: "primary" | "secondary" = "primary"
|
|
1464
|
+
onClick: function
|
|
1465
|
+
}
|
|
1466
|
+
style(tailwind) {
|
|
1467
|
+
primary: "inline-flex items-center justify-center rounded-lg border border-cyan-500 bg-cyan-500 px-4 py-2 text-sm font-medium text-zinc-950 transition hover:bg-cyan-400 active:translate-y-px active:bg-cyan-500"
|
|
1468
|
+
secondary: "inline-flex items-center justify-center rounded-lg border border-zinc-600 bg-zinc-900 px-4 py-2 text-sm font-medium text-zinc-200 transition hover:border-cyan-400 hover:text-cyan-300 active:translate-y-px"
|
|
1469
|
+
}
|
|
1470
|
+
ui {
|
|
1471
|
+
<button class={props.variant === "primary" ? "primary" : "secondary"} @click={props.onClick}>{props.label}</button>
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
`;
|
|
1475
|
+
const STARTER_CONFIG = `export default {
|
|
1476
|
+
pages: './pages',
|
|
1477
|
+
components: './components',
|
|
1478
|
+
outDir: './dist',
|
|
1479
|
+
port: 3000,
|
|
1480
|
+
title: 'tardis starter',
|
|
1481
|
+
head: [
|
|
1482
|
+
'<script src="https://cdn.tailwindcss.com"></script>',
|
|
1483
|
+
],
|
|
1484
|
+
}
|
|
1485
|
+
`;
|
|
1486
|
+
const STARTER_PACKAGE = `{
|
|
1487
|
+
"name": "my-tardis-app",
|
|
1488
|
+
"version": "0.1.0",
|
|
1489
|
+
"private": true,
|
|
1490
|
+
"type": "module",
|
|
1491
|
+
"scripts": {
|
|
1492
|
+
"dev": "tardis dev",
|
|
1493
|
+
"build": "tardis build",
|
|
1494
|
+
"preview": "tardis preview"
|
|
1495
|
+
},
|
|
1496
|
+
"dependencies": {
|
|
1497
|
+
"tardisjs": "latest"
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
`;
|
|
1501
|
+
const STARTER_GITIGNORE = `node_modules
|
|
1502
|
+
dist
|
|
1503
|
+
`;
|
|
1504
|
+
function toPosix(p) {
|
|
1505
|
+
return p.split(path.sep).join('/');
|
|
1506
|
+
}
|
|
1507
|
+
function replaceRuntimeImport(code) {
|
|
1508
|
+
return code.replace(/from\s+['"]tardisjs\/runtime['"]/g, "from '/tardis-runtime.js'");
|
|
1509
|
+
}
|
|
1510
|
+
let typescriptModulePromise = null;
|
|
1511
|
+
async function getTypeScriptModule() {
|
|
1512
|
+
if (!typescriptModulePromise) {
|
|
1513
|
+
typescriptModulePromise = import('typescript');
|
|
1514
|
+
}
|
|
1515
|
+
return typescriptModulePromise;
|
|
1516
|
+
}
|
|
1517
|
+
async function pathExists(targetPath) {
|
|
1518
|
+
try {
|
|
1519
|
+
await fsp.access(targetPath);
|
|
1520
|
+
return true;
|
|
1521
|
+
}
|
|
1522
|
+
catch {
|
|
1523
|
+
return false;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
async function ensureDir(dirPath) {
|
|
1527
|
+
await fsp.mkdir(dirPath, { recursive: true });
|
|
1528
|
+
}
|
|
1529
|
+
async function copyDirRecursive(src, dest) {
|
|
1530
|
+
const entries = await fsp.readdir(src, { withFileTypes: true });
|
|
1531
|
+
for (const entry of entries) {
|
|
1532
|
+
const srcPath = path.join(src, entry.name);
|
|
1533
|
+
const destPath = path.join(dest, entry.name);
|
|
1534
|
+
if (entry.isDirectory()) {
|
|
1535
|
+
await ensureDir(destPath);
|
|
1536
|
+
await copyDirRecursive(srcPath, destPath);
|
|
1537
|
+
}
|
|
1538
|
+
else {
|
|
1539
|
+
await fsp.copyFile(srcPath, destPath);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
async function listTardisFiles(dirPath) {
|
|
1544
|
+
if (!(await pathExists(dirPath)))
|
|
1545
|
+
return [];
|
|
1546
|
+
const entries = await fsp.readdir(dirPath, { withFileTypes: true });
|
|
1547
|
+
const files = [];
|
|
1548
|
+
for (const entry of entries) {
|
|
1549
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
1550
|
+
if (entry.isDirectory()) {
|
|
1551
|
+
files.push(...(await listTardisFiles(fullPath)));
|
|
1552
|
+
}
|
|
1553
|
+
else if (entry.isFile() && entry.name.endsWith('.tardis')) {
|
|
1554
|
+
files.push(fullPath);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
return files;
|
|
1558
|
+
}
|
|
1559
|
+
function pageFileToRoute(relativePath) {
|
|
1560
|
+
const noExt = relativePath.replace(/\.tardis$/, '');
|
|
1561
|
+
const segments = toPosix(noExt).split('/').filter(Boolean);
|
|
1562
|
+
const mapped = segments
|
|
1563
|
+
.map((segment) => {
|
|
1564
|
+
if (segment === 'index')
|
|
1565
|
+
return '';
|
|
1566
|
+
const dynamic = segment.match(/^\[(.+)\]$/);
|
|
1567
|
+
if (dynamic)
|
|
1568
|
+
return `:${dynamic[1]}`;
|
|
1569
|
+
return segment;
|
|
1570
|
+
})
|
|
1571
|
+
.filter((segment, index, arr) => !(segment === '' && index === arr.length - 1 && arr.length > 1));
|
|
1572
|
+
const route = `/${mapped.filter(Boolean).join('/')}`;
|
|
1573
|
+
return route === '' ? '/' : route;
|
|
1574
|
+
}
|
|
1575
|
+
async function loadConfig(cwd) {
|
|
1576
|
+
const configPath = path.join(cwd, 'tardis.config.js');
|
|
1577
|
+
if (!(await pathExists(configPath))) {
|
|
1578
|
+
throw new CLIError('TardisError: no tardis.config.js found\n Run "npx tardis init" to create a new project');
|
|
1579
|
+
}
|
|
1580
|
+
const mod = await import(`${pathToFileURL(configPath).href}?t=${Date.now()}`);
|
|
1581
|
+
const cfg = mod.default;
|
|
1582
|
+
if (!cfg) {
|
|
1583
|
+
throw new CLIError('TardisError: invalid tardis.config.js\n Ensure it exports a default config object');
|
|
1584
|
+
}
|
|
1585
|
+
return {
|
|
1586
|
+
pages: cfg.pages ?? './pages',
|
|
1587
|
+
components: cfg.components ?? './components',
|
|
1588
|
+
outDir: cfg.outDir ?? './dist',
|
|
1589
|
+
port: cfg.port ?? 3000,
|
|
1590
|
+
title: cfg.title,
|
|
1591
|
+
head: cfg.head ?? [],
|
|
1592
|
+
staticDir: cfg.staticDir ?? './public',
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
async function compileFile(source, filePath) {
|
|
1596
|
+
const ast = parse(lex(source), path.basename(filePath));
|
|
1597
|
+
const output = compile(ast);
|
|
1598
|
+
const ts = await getTypeScriptModule();
|
|
1599
|
+
const transpiled = ts.transpileModule(output, {
|
|
1600
|
+
compilerOptions: {
|
|
1601
|
+
target: ts.ScriptTarget.ES2020,
|
|
1602
|
+
module: ts.ModuleKind.ES2020,
|
|
1603
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
1604
|
+
},
|
|
1605
|
+
}).outputText;
|
|
1606
|
+
return {
|
|
1607
|
+
code: replaceRuntimeImport(transpiled),
|
|
1608
|
+
componentName: ast.name,
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
function buildClientIndexHtml(routes, devMode, config, componentArtifacts) {
|
|
1612
|
+
const imports = ["import { createRouter } from '/tardis-runtime.js'"];
|
|
1613
|
+
const componentRegs = [];
|
|
1614
|
+
if (componentArtifacts && componentArtifacts.length > 0) {
|
|
1615
|
+
for (const artifact of componentArtifacts) {
|
|
1616
|
+
const importPath = `/${toPosix(artifact.outputPath)}`;
|
|
1617
|
+
imports.push(`import { ${artifact.componentName} } from '${importPath}'`);
|
|
1618
|
+
componentRegs.push(` window.__tardis_${artifact.componentName} = ${artifact.componentName}`);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
const routeEntries = [];
|
|
1622
|
+
routes.forEach((route, idx) => {
|
|
1623
|
+
const alias = `${route.exportName}_${idx}`;
|
|
1624
|
+
imports.push(`import { ${route.exportName} as ${alias} } from '${route.importPath}'`);
|
|
1625
|
+
routeEntries.push(` { path: '${route.routePath}', component: ${alias} }`);
|
|
1626
|
+
});
|
|
1627
|
+
const hmrSnippet = devMode
|
|
1628
|
+
? `
|
|
1629
|
+
const socket = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws')
|
|
1630
|
+
socket.addEventListener('message', (event) => {
|
|
1631
|
+
try {
|
|
1632
|
+
const payload = JSON.parse(event.data)
|
|
1633
|
+
if (payload.type === 'reload') location.reload()
|
|
1634
|
+
} catch {
|
|
1635
|
+
location.reload()
|
|
1636
|
+
}
|
|
1637
|
+
})
|
|
1638
|
+
`
|
|
1639
|
+
: '';
|
|
1640
|
+
const headContent = (config?.head ?? []).length > 0
|
|
1641
|
+
? '\n' + (config?.head ?? []).map(h => `\t\t${h}`).join('\n')
|
|
1642
|
+
: '';
|
|
1643
|
+
const componentRegBlock = componentRegs.length > 0
|
|
1644
|
+
? '\n' + componentRegs.join('\n') + '\n'
|
|
1645
|
+
: '';
|
|
1646
|
+
return `<!doctype html>
|
|
1647
|
+
<html lang="en">
|
|
1648
|
+
<head>
|
|
1649
|
+
<meta charset="UTF-8" />
|
|
1650
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1651
|
+
<title>${config?.title ?? 'tardis app'}</title>${headContent}
|
|
1652
|
+
</head>
|
|
1653
|
+
<body>
|
|
1654
|
+
<div id="app"></div>
|
|
1655
|
+
<script type="module">
|
|
1656
|
+
${imports.map((line) => ` ${line}`).join('\n')}
|
|
1657
|
+
${componentRegBlock}
|
|
1658
|
+
const routes = [
|
|
1659
|
+
${routeEntries.join(',\n')}
|
|
1660
|
+
]
|
|
1661
|
+
|
|
1662
|
+
const router = createRouter(routes)
|
|
1663
|
+
router.start()
|
|
1664
|
+
${hmrSnippet}
|
|
1665
|
+
</script>
|
|
1666
|
+
</body>
|
|
1667
|
+
</html>
|
|
1668
|
+
`;
|
|
1669
|
+
}
|
|
1670
|
+
async function writeRuntimeModules(outDir) {
|
|
1671
|
+
const cliDir = path.dirname(fileURLToPath(import.meta.url));
|
|
1672
|
+
const frameworkRoot = path.resolve(cliDir, '..');
|
|
1673
|
+
const runtimeSrcDir = path.join(frameworkRoot, 'src', 'runtime');
|
|
1674
|
+
const runtimeOutDir = path.join(outDir, 'runtime');
|
|
1675
|
+
if (await pathExists(runtimeSrcDir)) {
|
|
1676
|
+
const runtimeFiles = (await fsp.readdir(runtimeSrcDir)).filter((f) => f.endsWith('.ts'));
|
|
1677
|
+
await ensureDir(runtimeOutDir);
|
|
1678
|
+
const ts = await getTypeScriptModule();
|
|
1679
|
+
for (const file of runtimeFiles) {
|
|
1680
|
+
const srcPath = path.join(runtimeSrcDir, file);
|
|
1681
|
+
const raw = await fsp.readFile(srcPath, 'utf8');
|
|
1682
|
+
const transpiled = ts.transpileModule(raw, {
|
|
1683
|
+
compilerOptions: {
|
|
1684
|
+
target: ts.ScriptTarget.ES2020,
|
|
1685
|
+
module: ts.ModuleKind.ES2020,
|
|
1686
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
1687
|
+
},
|
|
1688
|
+
}).outputText;
|
|
1689
|
+
const withExt = transpiled.replace(/from\s+['"](\.\/?[^'"]+)['"]/g, (full, importPath) => {
|
|
1690
|
+
if (importPath.endsWith('.js'))
|
|
1691
|
+
return full;
|
|
1692
|
+
return full.replace(importPath, `${importPath}.js`);
|
|
1693
|
+
});
|
|
1694
|
+
const jsName = file.replace(/\.ts$/, '.js');
|
|
1695
|
+
await fsp.writeFile(path.join(runtimeOutDir, jsName), withExt, 'utf8');
|
|
1696
|
+
}
|
|
1697
|
+
const entry = `export * from './runtime/index.js'\n`;
|
|
1698
|
+
await fsp.writeFile(path.join(outDir, 'tardis-runtime.js'), entry, 'utf8');
|
|
1699
|
+
await fsp.writeFile(path.join(outDir, 'runtime.js'), entry, 'utf8');
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
const runtimeDist = path.join(frameworkRoot, 'dist', 'runtime', 'index.mjs');
|
|
1703
|
+
if (await pathExists(runtimeDist)) {
|
|
1704
|
+
const built = await fsp.readFile(runtimeDist, 'utf8');
|
|
1705
|
+
await fsp.writeFile(path.join(outDir, 'tardis-runtime.js'), built, 'utf8');
|
|
1706
|
+
await fsp.writeFile(path.join(outDir, 'runtime.js'), built, 'utf8');
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
throw new CLIError('TardisError: runtime source not found\n Reinstall tardisjs or run package build');
|
|
1710
|
+
}
|
|
1711
|
+
async function getDirectorySizeBytes(dirPath) {
|
|
1712
|
+
if (!(await pathExists(dirPath)))
|
|
1713
|
+
return 0;
|
|
1714
|
+
const entries = await fsp.readdir(dirPath, { withFileTypes: true });
|
|
1715
|
+
let total = 0;
|
|
1716
|
+
for (const entry of entries) {
|
|
1717
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
1718
|
+
if (entry.isDirectory()) {
|
|
1719
|
+
total += await getDirectorySizeBytes(fullPath);
|
|
1720
|
+
}
|
|
1721
|
+
else if (entry.isFile()) {
|
|
1722
|
+
const stat = await fsp.stat(fullPath);
|
|
1723
|
+
total += stat.size;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
return total;
|
|
1727
|
+
}
|
|
1728
|
+
function formatBytes(bytes) {
|
|
1729
|
+
if (bytes < 1024)
|
|
1730
|
+
return `${bytes} B`;
|
|
1731
|
+
if (bytes < 1024 * 1024)
|
|
1732
|
+
return `${(bytes / 1024).toFixed(2)} KB`;
|
|
1733
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
1734
|
+
}
|
|
1735
|
+
async function collectArtifacts(cwd, config) {
|
|
1736
|
+
const pagesDir = path.resolve(cwd, config.pages);
|
|
1737
|
+
const componentsDir = path.resolve(cwd, config.components);
|
|
1738
|
+
const pageFiles = await listTardisFiles(pagesDir);
|
|
1739
|
+
const componentFiles = await listTardisFiles(componentsDir);
|
|
1740
|
+
const pageArtifacts = [];
|
|
1741
|
+
const componentArtifacts = [];
|
|
1742
|
+
const routes = [];
|
|
1743
|
+
for (const file of pageFiles) {
|
|
1744
|
+
const source = await fsp.readFile(file, 'utf8');
|
|
1745
|
+
const compiled = await compileFile(source, file);
|
|
1746
|
+
const rel = toPosix(path.relative(pagesDir, file)).replace(/\.tardis$/, '.js');
|
|
1747
|
+
const outPath = path.join('pages', rel);
|
|
1748
|
+
pageArtifacts.push({
|
|
1749
|
+
sourcePath: file,
|
|
1750
|
+
outputPath: outPath,
|
|
1751
|
+
outputCode: compiled.code,
|
|
1752
|
+
componentName: compiled.componentName,
|
|
1753
|
+
});
|
|
1754
|
+
routes.push({
|
|
1755
|
+
routePath: pageFileToRoute(toPosix(path.relative(pagesDir, file))),
|
|
1756
|
+
importPath: `/${toPosix(outPath)}`,
|
|
1757
|
+
exportName: compiled.componentName,
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
for (const file of componentFiles) {
|
|
1761
|
+
const source = await fsp.readFile(file, 'utf8');
|
|
1762
|
+
const compiled = await compileFile(source, file);
|
|
1763
|
+
const rel = toPosix(path.relative(componentsDir, file)).replace(/\.tardis$/, '.js');
|
|
1764
|
+
const outPath = path.join('components', rel);
|
|
1765
|
+
componentArtifacts.push({
|
|
1766
|
+
sourcePath: file,
|
|
1767
|
+
outputPath: outPath,
|
|
1768
|
+
outputCode: compiled.code,
|
|
1769
|
+
componentName: compiled.componentName,
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
return { pageArtifacts, componentArtifacts, routes };
|
|
1773
|
+
}
|
|
1774
|
+
async function initProject(cwd = process.cwd()) {
|
|
1775
|
+
await ensureDir(path.join(cwd, 'pages'));
|
|
1776
|
+
await ensureDir(path.join(cwd, 'components'));
|
|
1777
|
+
await fsp.writeFile(path.join(cwd, 'pages', 'index.tardis'), STARTER_INDEX, 'utf8');
|
|
1778
|
+
await fsp.writeFile(path.join(cwd, 'components', 'Button.tardis'), STARTER_BUTTON, 'utf8');
|
|
1779
|
+
await fsp.writeFile(path.join(cwd, 'tardis.config.js'), STARTER_CONFIG, 'utf8');
|
|
1780
|
+
await fsp.writeFile(path.join(cwd, 'package.json'), STARTER_PACKAGE, 'utf8');
|
|
1781
|
+
await fsp.writeFile(path.join(cwd, '.gitignore'), STARTER_GITIGNORE, 'utf8');
|
|
1782
|
+
console.log('✨ Tardis project initialized');
|
|
1783
|
+
}
|
|
1784
|
+
async function createTardisApp(projectName, cwd = process.cwd()) {
|
|
1785
|
+
const normalizedName = projectName.trim();
|
|
1786
|
+
if (!normalizedName) {
|
|
1787
|
+
throw new CLIError('TardisError: project name is required\n Usage: create-tardis-app <project-name>');
|
|
1788
|
+
}
|
|
1789
|
+
const targetDir = path.resolve(cwd, normalizedName);
|
|
1790
|
+
if (await pathExists(targetDir)) {
|
|
1791
|
+
throw new CLIError(`TardisError: directory already exists\n ${targetDir}`);
|
|
1792
|
+
}
|
|
1793
|
+
await ensureDir(targetDir);
|
|
1794
|
+
await initProject(targetDir);
|
|
1795
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
1796
|
+
if (await pathExists(packageJsonPath)) {
|
|
1797
|
+
const raw = await fsp.readFile(packageJsonPath, 'utf8');
|
|
1798
|
+
const pkg = JSON.parse(raw);
|
|
1799
|
+
pkg.name = normalizedName;
|
|
1800
|
+
await fsp.writeFile(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
|
|
1801
|
+
}
|
|
1802
|
+
console.log(`\n✅ Created ${normalizedName}`);
|
|
1803
|
+
console.log(`\nNext steps:`);
|
|
1804
|
+
console.log(` cd ${normalizedName}`);
|
|
1805
|
+
console.log(` npm install`);
|
|
1806
|
+
console.log(` npm run dev`);
|
|
1807
|
+
}
|
|
1808
|
+
async function findAvailablePort(preferredPort) {
|
|
1809
|
+
function check(port) {
|
|
1810
|
+
return new Promise((resolve) => {
|
|
1811
|
+
const tester = net.createServer();
|
|
1812
|
+
tester.once('error', () => resolve(false));
|
|
1813
|
+
tester.once('listening', () => {
|
|
1814
|
+
tester.close(() => resolve(true));
|
|
1815
|
+
});
|
|
1816
|
+
tester.listen(port);
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
if (await check(preferredPort))
|
|
1820
|
+
return preferredPort;
|
|
1821
|
+
if (await check(preferredPort + 1))
|
|
1822
|
+
return preferredPort + 1;
|
|
1823
|
+
throw new CLIError(`TardisError: no open port found\n Tried ${preferredPort} and ${preferredPort + 1}`);
|
|
1824
|
+
}
|
|
1825
|
+
async function buildProject(cwd = process.cwd()) {
|
|
1826
|
+
const start = Date.now();
|
|
1827
|
+
const config = await loadConfig(cwd);
|
|
1828
|
+
const outDir = path.resolve(cwd, config.outDir);
|
|
1829
|
+
await fsp.rm(outDir, { recursive: true, force: true });
|
|
1830
|
+
await ensureDir(outDir);
|
|
1831
|
+
const { pageArtifacts, componentArtifacts, routes } = await collectArtifacts(cwd, config);
|
|
1832
|
+
const allArtifacts = [...pageArtifacts, ...componentArtifacts];
|
|
1833
|
+
for (const artifact of allArtifacts) {
|
|
1834
|
+
const absoluteOut = path.join(outDir, artifact.outputPath);
|
|
1835
|
+
await ensureDir(path.dirname(absoluteOut));
|
|
1836
|
+
await fsp.writeFile(absoluteOut, artifact.outputCode, 'utf8');
|
|
1837
|
+
}
|
|
1838
|
+
await writeRuntimeModules(outDir);
|
|
1839
|
+
// Copy static files from staticDir
|
|
1840
|
+
const staticSrcDir = path.resolve(cwd, config.staticDir ?? 'public');
|
|
1841
|
+
if (await pathExists(staticSrcDir)) {
|
|
1842
|
+
await copyDirRecursive(staticSrcDir, outDir);
|
|
1843
|
+
}
|
|
1844
|
+
await fsp.writeFile(path.join(outDir, 'index.html'), buildClientIndexHtml(routes, false, config, componentArtifacts), 'utf8');
|
|
1845
|
+
const timeMs = Date.now() - start;
|
|
1846
|
+
const outputBytes = await getDirectorySizeBytes(outDir);
|
|
1847
|
+
console.log(`✅ Build complete`);
|
|
1848
|
+
console.log(`- files compiled: ${allArtifacts.length}`);
|
|
1849
|
+
console.log(`- time: ${timeMs}ms`);
|
|
1850
|
+
console.log(`- output size: ${formatBytes(outputBytes)}`);
|
|
1851
|
+
return { filesCompiled: allArtifacts.length, timeMs, outputBytes };
|
|
1852
|
+
}
|
|
1853
|
+
function contentType(filePath) {
|
|
1854
|
+
if (filePath.endsWith('.html'))
|
|
1855
|
+
return 'text/html; charset=utf-8';
|
|
1856
|
+
if (filePath.endsWith('.js'))
|
|
1857
|
+
return 'application/javascript; charset=utf-8';
|
|
1858
|
+
if (filePath.endsWith('.json'))
|
|
1859
|
+
return 'application/json; charset=utf-8';
|
|
1860
|
+
if (filePath.endsWith('.css'))
|
|
1861
|
+
return 'text/css; charset=utf-8';
|
|
1862
|
+
if (filePath.endsWith('.svg'))
|
|
1863
|
+
return 'image/svg+xml';
|
|
1864
|
+
if (filePath.endsWith('.png'))
|
|
1865
|
+
return 'image/png';
|
|
1866
|
+
if (filePath.endsWith('.jpg') || filePath.endsWith('.jpeg'))
|
|
1867
|
+
return 'image/jpeg';
|
|
1868
|
+
if (filePath.endsWith('.gif'))
|
|
1869
|
+
return 'image/gif';
|
|
1870
|
+
if (filePath.endsWith('.ico'))
|
|
1871
|
+
return 'image/x-icon';
|
|
1872
|
+
if (filePath.endsWith('.woff2'))
|
|
1873
|
+
return 'font/woff2';
|
|
1874
|
+
if (filePath.endsWith('.woff'))
|
|
1875
|
+
return 'font/woff';
|
|
1876
|
+
return 'text/plain; charset=utf-8';
|
|
1877
|
+
}
|
|
1878
|
+
async function createDevAssets(cwd, config) {
|
|
1879
|
+
const assets = new Map();
|
|
1880
|
+
const { pageArtifacts, componentArtifacts, routes } = await collectArtifacts(cwd, config);
|
|
1881
|
+
for (const artifact of [...pageArtifacts, ...componentArtifacts]) {
|
|
1882
|
+
assets.set(`/${toPosix(artifact.outputPath)}`, artifact.outputCode);
|
|
1883
|
+
}
|
|
1884
|
+
const tempOut = path.join(cwd, '.tardis-dev-runtime');
|
|
1885
|
+
await fsp.rm(tempOut, { recursive: true, force: true });
|
|
1886
|
+
await ensureDir(tempOut);
|
|
1887
|
+
await writeRuntimeModules(tempOut);
|
|
1888
|
+
const runtimeFiles = await fsp.readdir(tempOut);
|
|
1889
|
+
for (const file of runtimeFiles) {
|
|
1890
|
+
const full = path.join(tempOut, file);
|
|
1891
|
+
const stat = await fsp.stat(full);
|
|
1892
|
+
if (stat.isDirectory()) {
|
|
1893
|
+
const nested = await fsp.readdir(full);
|
|
1894
|
+
for (const child of nested) {
|
|
1895
|
+
const childPath = path.join(full, child);
|
|
1896
|
+
const childRaw = await fsp.readFile(childPath, 'utf8');
|
|
1897
|
+
assets.set(`/${toPosix(path.join(file, child))}`, childRaw);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
else {
|
|
1901
|
+
const raw = await fsp.readFile(full, 'utf8');
|
|
1902
|
+
assets.set(`/${toPosix(file)}`, raw);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
await fsp.rm(tempOut, { recursive: true, force: true });
|
|
1906
|
+
assets.set('/index.html', buildClientIndexHtml(routes, true, config, componentArtifacts));
|
|
1907
|
+
return assets;
|
|
1908
|
+
}
|
|
1909
|
+
async function devServer(cwd = process.cwd()) {
|
|
1910
|
+
const config = await loadConfig(cwd);
|
|
1911
|
+
const preferredPort = config.port ?? 3000;
|
|
1912
|
+
const port = await findAvailablePort(preferredPort);
|
|
1913
|
+
let assets = await createDevAssets(cwd, config);
|
|
1914
|
+
const server = http.createServer(async (req, res) => {
|
|
1915
|
+
const requestPath = req.url ? req.url.split('?')[0] : '/';
|
|
1916
|
+
const normalizedPath = requestPath && requestPath !== '/' ? requestPath : '/index.html';
|
|
1917
|
+
if (assets.has(normalizedPath)) {
|
|
1918
|
+
const body = assets.get(normalizedPath);
|
|
1919
|
+
res.writeHead(200, { 'Content-Type': contentType(normalizedPath) });
|
|
1920
|
+
res.end(body);
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
if (normalizedPath.startsWith('/pages/') || normalizedPath.startsWith('/components/') || normalizedPath.startsWith('/runtime/')) {
|
|
1924
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
1925
|
+
res.end('Not found');
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
// Try static files from staticDir
|
|
1929
|
+
const staticDir = path.resolve(cwd, config.staticDir ?? 'public');
|
|
1930
|
+
const staticFilePath = path.join(staticDir, normalizedPath);
|
|
1931
|
+
try {
|
|
1932
|
+
const fileStat = await fsp.stat(staticFilePath);
|
|
1933
|
+
if (fileStat.isFile()) {
|
|
1934
|
+
const body = await fsp.readFile(staticFilePath);
|
|
1935
|
+
res.writeHead(200, { 'Content-Type': contentType(normalizedPath) });
|
|
1936
|
+
res.end(body);
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
catch { }
|
|
1941
|
+
const fallback = assets.get('/index.html') ?? '';
|
|
1942
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
1943
|
+
res.end(fallback);
|
|
1944
|
+
});
|
|
1945
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
1946
|
+
server.on('upgrade', (req, socket, head) => {
|
|
1947
|
+
if ((req.url ?? '').split('?')[0] !== '/ws') {
|
|
1948
|
+
socket.destroy();
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
1952
|
+
wss.emit('connection', ws, req);
|
|
1953
|
+
});
|
|
1954
|
+
});
|
|
1955
|
+
const pagesAbs = path.resolve(cwd, config.pages);
|
|
1956
|
+
const componentsAbs = path.resolve(cwd, config.components);
|
|
1957
|
+
const watcher = chokidar.watch([pagesAbs, componentsAbs], {
|
|
1958
|
+
ignoreInitial: true,
|
|
1959
|
+
});
|
|
1960
|
+
watcher.on('all', async () => {
|
|
1961
|
+
try {
|
|
1962
|
+
assets = await createDevAssets(cwd, config);
|
|
1963
|
+
const payload = JSON.stringify({ type: 'reload' });
|
|
1964
|
+
for (const client of wss.clients) {
|
|
1965
|
+
if (client.readyState === client.OPEN) {
|
|
1966
|
+
client.send(payload);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
console.log('♻️ Recompiled');
|
|
1970
|
+
}
|
|
1971
|
+
catch (error) {
|
|
1972
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1973
|
+
}
|
|
1974
|
+
});
|
|
1975
|
+
server.listen(port, () => {
|
|
1976
|
+
if (port !== preferredPort) {
|
|
1977
|
+
console.log(`⚠️ Port ${preferredPort} in use, using ${port} instead`);
|
|
1978
|
+
}
|
|
1979
|
+
console.log(`🚀 Dev server running at http://localhost:${port}`);
|
|
1980
|
+
});
|
|
1981
|
+
const shutdown = async () => {
|
|
1982
|
+
await watcher.close();
|
|
1983
|
+
wss.close();
|
|
1984
|
+
server.close();
|
|
1985
|
+
process.exit(0);
|
|
1986
|
+
};
|
|
1987
|
+
process.on('SIGINT', shutdown);
|
|
1988
|
+
process.on('SIGTERM', shutdown);
|
|
1989
|
+
}
|
|
1990
|
+
async function previewProject(cwd = process.cwd()) {
|
|
1991
|
+
const outDir = path.join(cwd, 'dist');
|
|
1992
|
+
if (!(await pathExists(outDir))) {
|
|
1993
|
+
throw new CLIError('TardisError: dist directory not found\n Run "npx tardis build" first');
|
|
1994
|
+
}
|
|
1995
|
+
const port = 4000;
|
|
1996
|
+
const server = http.createServer(async (req, res) => {
|
|
1997
|
+
const incoming = req.url ? req.url.split('?')[0] : '/';
|
|
1998
|
+
const candidate = incoming === '/' ? '/index.html' : incoming;
|
|
1999
|
+
const filePath = path.join(outDir, candidate);
|
|
2000
|
+
try {
|
|
2001
|
+
const stat = await fsp.stat(filePath);
|
|
2002
|
+
if (stat.isDirectory()) {
|
|
2003
|
+
const idx = path.join(filePath, 'index.html');
|
|
2004
|
+
const body = await fsp.readFile(idx);
|
|
2005
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
2006
|
+
res.end(body);
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
const body = await fsp.readFile(filePath);
|
|
2010
|
+
res.writeHead(200, { 'Content-Type': contentType(filePath) });
|
|
2011
|
+
res.end(body);
|
|
2012
|
+
}
|
|
2013
|
+
catch {
|
|
2014
|
+
const fallback = path.join(outDir, 'index.html');
|
|
2015
|
+
if (await pathExists(fallback)) {
|
|
2016
|
+
const html = await fsp.readFile(fallback);
|
|
2017
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
2018
|
+
res.end(html);
|
|
2019
|
+
}
|
|
2020
|
+
else {
|
|
2021
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
2022
|
+
res.end('Not found');
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
});
|
|
2026
|
+
server.listen(port, () => {
|
|
2027
|
+
console.log(`🔎 Preview server at http://localhost:${port}`);
|
|
2028
|
+
});
|
|
2029
|
+
const shutdown = () => {
|
|
2030
|
+
server.close();
|
|
2031
|
+
process.exit(0);
|
|
2032
|
+
};
|
|
2033
|
+
process.on('SIGINT', shutdown);
|
|
2034
|
+
process.on('SIGTERM', shutdown);
|
|
2035
|
+
}
|
|
2036
|
+
function createProgram() {
|
|
2037
|
+
const program = new Command();
|
|
2038
|
+
program
|
|
2039
|
+
.name('tardis')
|
|
2040
|
+
.description('tardisjs CLI');
|
|
2041
|
+
program
|
|
2042
|
+
.command('init')
|
|
2043
|
+
.description('Initialize a new tardis app in current directory')
|
|
2044
|
+
.action(async () => {
|
|
2045
|
+
await initProject(process.cwd());
|
|
2046
|
+
});
|
|
2047
|
+
program
|
|
2048
|
+
.command('build')
|
|
2049
|
+
.description('Build tardis app for production')
|
|
2050
|
+
.action(async () => {
|
|
2051
|
+
await buildProject(process.cwd());
|
|
2052
|
+
});
|
|
2053
|
+
program
|
|
2054
|
+
.command('dev')
|
|
2055
|
+
.description('Run dev server with HMR')
|
|
2056
|
+
.action(async () => {
|
|
2057
|
+
await devServer(process.cwd());
|
|
2058
|
+
});
|
|
2059
|
+
program
|
|
2060
|
+
.command('preview')
|
|
2061
|
+
.description('Serve dist directory for preview')
|
|
2062
|
+
.action(async () => {
|
|
2063
|
+
await previewProject(process.cwd());
|
|
2064
|
+
});
|
|
2065
|
+
return program;
|
|
2066
|
+
}
|
|
2067
|
+
async function runCLI(argv = process.argv) {
|
|
2068
|
+
const program = createProgram();
|
|
2069
|
+
try {
|
|
2070
|
+
await program.parseAsync(argv);
|
|
2071
|
+
}
|
|
2072
|
+
catch (error) {
|
|
2073
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2074
|
+
console.error(msg);
|
|
2075
|
+
process.exit(1);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
if (process.argv[1] && fs.existsSync(process.argv[1])) {
|
|
2079
|
+
const current = pathToFileURL(process.argv[1]).href;
|
|
2080
|
+
if (import.meta.url === current) {
|
|
2081
|
+
runCLI();
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
async function run() {
|
|
2086
|
+
const projectName = process.argv[2];
|
|
2087
|
+
if (!projectName) {
|
|
2088
|
+
console.error('TardisError: project name is required');
|
|
2089
|
+
console.error(' Usage: create-tardis-app <project-name>');
|
|
2090
|
+
process.exit(1);
|
|
2091
|
+
}
|
|
2092
|
+
try {
|
|
2093
|
+
await createTardisApp(projectName, process.cwd());
|
|
2094
|
+
}
|
|
2095
|
+
catch (error) {
|
|
2096
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2097
|
+
console.error(msg);
|
|
2098
|
+
process.exit(1);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
run();
|
|
2102
|
+
//# sourceMappingURL=create-tardis-app.js.map
|