@devansharora18/tardisjs 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +199 -0
- package/dist/bin/create-tardis-app.js +51 -27
- package/dist/bin/create-tardis-app.js.map +1 -1
- package/dist/bin/tardis.js +51 -27
- package/dist/bin/tardis.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# tardisjs
|
|
2
|
+
|
|
3
|
+
**Smaller on the outside.** A blueprint-first frontend framework that compiles `.tardis` files to vanilla JavaScript.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx create-tardis-app my-app
|
|
9
|
+
cd my-app
|
|
10
|
+
npm install
|
|
11
|
+
npm run dev
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or initialize in an existing directory:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx tardis init
|
|
18
|
+
npx tardis dev
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## What It Looks Like
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
blueprint Counter {
|
|
25
|
+
state {
|
|
26
|
+
count: number = 0
|
|
27
|
+
}
|
|
28
|
+
methods {
|
|
29
|
+
increment: () => $update(state.count, state.count + 1)
|
|
30
|
+
decrement: () => $update(state.count, state.count - 1)
|
|
31
|
+
reset: () => $update(state.count, 0)
|
|
32
|
+
}
|
|
33
|
+
style(tailwind) {
|
|
34
|
+
value: "text-6xl font-bold text-center"
|
|
35
|
+
actions: "flex gap-4 justify-center mt-8"
|
|
36
|
+
btn: "rounded-lg bg-cyan-400 px-5 py-2 text-black font-medium"
|
|
37
|
+
}
|
|
38
|
+
ui {
|
|
39
|
+
<main>
|
|
40
|
+
<p class={"value"}>{state.count}</p>
|
|
41
|
+
<div class={"actions"}>
|
|
42
|
+
<button class={"btn"} @click={methods.decrement}>-1</button>
|
|
43
|
+
<button class={"btn"} @click={methods.increment}>+1</button>
|
|
44
|
+
<button class={"btn"} @click={methods.reset}>Reset</button>
|
|
45
|
+
</div>
|
|
46
|
+
</main>
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
- **~5KB runtime** — ships minimal code to the browser
|
|
54
|
+
- **No virtual DOM** — direct DOM updates with Proxy-based reactivity
|
|
55
|
+
- **Blueprint syntax** — structured DSL with props, state, computed, methods, events, styles, and UI
|
|
56
|
+
- **Compiler-driven** — lexer, parser, and code generator transform `.tardis` files to JavaScript modules
|
|
57
|
+
- **File-based routing** — `pages/index.tardis` → `/`, `pages/[slug].tardis` → `/:slug`
|
|
58
|
+
- **Built-in dev server** — HMR via WebSocket, auto port discovery
|
|
59
|
+
- **Zero runtime dependencies** — the runtime has no external deps
|
|
60
|
+
- **Tailwind integration** — style tokens map directly to Tailwind classes
|
|
61
|
+
|
|
62
|
+
## Project Structure
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
my-app/
|
|
66
|
+
pages/
|
|
67
|
+
index.tardis # Routes derived from file structure
|
|
68
|
+
about.tardis
|
|
69
|
+
blog/[id].tardis # Dynamic route: /blog/:id
|
|
70
|
+
components/
|
|
71
|
+
Button.tardis # Reusable components
|
|
72
|
+
public/ # Static assets (copied to dist)
|
|
73
|
+
tardis.config.js # Project configuration
|
|
74
|
+
package.json
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## CLI Commands
|
|
78
|
+
|
|
79
|
+
| Command | Description |
|
|
80
|
+
|---------|-------------|
|
|
81
|
+
| `tardis init` | Scaffold a project in the current directory |
|
|
82
|
+
| `tardis dev` | Dev server with hot reload |
|
|
83
|
+
| `tardis build` | Production build to `dist/` |
|
|
84
|
+
| `tardis preview` | Serve built output on port 4000 |
|
|
85
|
+
| `create-tardis-app <name>` | Create a new project in a subdirectory |
|
|
86
|
+
|
|
87
|
+
## Configuration
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
// tardis.config.js
|
|
91
|
+
export default {
|
|
92
|
+
pages: './pages',
|
|
93
|
+
components: './components',
|
|
94
|
+
outDir: './dist',
|
|
95
|
+
port: 3000,
|
|
96
|
+
title: 'my app',
|
|
97
|
+
head: [
|
|
98
|
+
'<script src="https://cdn.tailwindcss.com"></script>',
|
|
99
|
+
],
|
|
100
|
+
staticDir: './public',
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Blueprint Anatomy
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
blueprint ComponentName {
|
|
108
|
+
props { } // Typed component inputs
|
|
109
|
+
state { } // Reactive local state (Proxy-based)
|
|
110
|
+
computed { } // Derived values from state
|
|
111
|
+
methods { } // Event handlers and logic
|
|
112
|
+
events { } // Lifecycle: onMount, onDestroy, onUpdate
|
|
113
|
+
style(tailwind) { } // Named class tokens
|
|
114
|
+
ui { } // HTML-like template with reactive bindings
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Every section is optional. Pages typically omit `props`; leaf components may omit `state`.
|
|
119
|
+
|
|
120
|
+
## Runtime API
|
|
121
|
+
|
|
122
|
+
| Function | Purpose |
|
|
123
|
+
|----------|---------|
|
|
124
|
+
| `$update(ref, value)` | Update state and trigger bindings |
|
|
125
|
+
| `$toggle(ref)` | Toggle boolean state |
|
|
126
|
+
| `$reset(state, initial)` | Reset state to defaults |
|
|
127
|
+
| `$batch(fn)` | Batch updates into single render cycle |
|
|
128
|
+
| `$navigate(path)` | Client-side navigation |
|
|
129
|
+
| `$params` | Read dynamic route parameters |
|
|
130
|
+
| `$back()` / `$forward()` | History traversal |
|
|
131
|
+
| `$fetch(url, opts?)` | Network request helper |
|
|
132
|
+
| `$if` / `$each` / `$show` | Conditional and list rendering |
|
|
133
|
+
| `$portal(el, target)` | Render into external DOM target |
|
|
134
|
+
| `$(selector)` | jQuery-inspired DOM query |
|
|
135
|
+
|
|
136
|
+
## VS Code Extension
|
|
137
|
+
|
|
138
|
+
Syntax highlighting for `.tardis` files is available in `vscode-extension/`. Install the `.vsix` file:
|
|
139
|
+
|
|
140
|
+
1. Open VS Code
|
|
141
|
+
2. Go to Extensions → `...` menu → Install from VSIX
|
|
142
|
+
3. Select `vscode-extension/tardisjs-syntax-0.1.0.vsix`
|
|
143
|
+
|
|
144
|
+
## Development
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
git clone https://github.com/devansharora18/tardisjs.git
|
|
148
|
+
cd tardisjs
|
|
149
|
+
npm install
|
|
150
|
+
npm run build # Build compiler + runtime + CLI
|
|
151
|
+
npm test # Run test suite
|
|
152
|
+
npm run typecheck # Type checking
|
|
153
|
+
npm run dev # Watch mode
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Build Scripts
|
|
157
|
+
|
|
158
|
+
| Script | Description |
|
|
159
|
+
|--------|-------------|
|
|
160
|
+
| `npm run build` | Full build (clean + lib + binaries) |
|
|
161
|
+
| `npm run build:lib` | Compiler and runtime only |
|
|
162
|
+
| `npm run build:bin` | CLI binaries only |
|
|
163
|
+
| `npm run dev` | Rollup watch mode |
|
|
164
|
+
| `npm test` | Vitest single run |
|
|
165
|
+
| `npm run test:watch` | Vitest watch mode |
|
|
166
|
+
| `npm run typecheck` | TypeScript check |
|
|
167
|
+
| `npm run publish:check` | Full quality gate |
|
|
168
|
+
|
|
169
|
+
## Examples
|
|
170
|
+
|
|
171
|
+
Working examples are in `examples/`:
|
|
172
|
+
|
|
173
|
+
- **`examples/counter/`** — Counter with increment, decrement, and reset
|
|
174
|
+
- **`examples/todo/`** — Todo list application
|
|
175
|
+
|
|
176
|
+
## How It Works
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
.tardis source
|
|
180
|
+
→ lex() Tokenize input
|
|
181
|
+
→ parse() Build AST
|
|
182
|
+
→ compile() Generate JavaScript
|
|
183
|
+
→ TypeScript transpile
|
|
184
|
+
→ Runtime import rewriting
|
|
185
|
+
→ Browser-ready JS module
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The compiler transforms blueprint sections into imperative DOM code that references the `$runtime` object. State uses JavaScript Proxies for fine-grained reactivity — only bindings that read a changed key are re-evaluated.
|
|
189
|
+
|
|
190
|
+
## Contributing
|
|
191
|
+
|
|
192
|
+
1. Create a branch scoped to one concern
|
|
193
|
+
2. Add or update tests next to affected subsystem
|
|
194
|
+
3. Run `npm run typecheck && npm test` before review
|
|
195
|
+
4. Provide minimal reproduction for parser/compiler defects
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
MIT
|
|
@@ -869,7 +869,7 @@ function compileUI(raw, componentName, scriptCode) {
|
|
|
869
869
|
const lines = [];
|
|
870
870
|
lines.push(` // ui`);
|
|
871
871
|
lines.push(` const _root = (() => {`);
|
|
872
|
-
lines.push(compileUINode(raw.trim(), 2));
|
|
872
|
+
lines.push(compileUINode(raw.trim(), 2, false));
|
|
873
873
|
lines.push(` })()`);
|
|
874
874
|
if (scriptCode) {
|
|
875
875
|
lines.push(``);
|
|
@@ -911,14 +911,14 @@ function compileScript(ast) {
|
|
|
911
911
|
lines.push(` })`);
|
|
912
912
|
return lines.join('\n');
|
|
913
913
|
}
|
|
914
|
-
function compileUINode(raw, depth) {
|
|
914
|
+
function compileUINode(raw, depth, preserveWs) {
|
|
915
915
|
const indent = " ".repeat(depth);
|
|
916
916
|
const lines = [];
|
|
917
917
|
// $if
|
|
918
918
|
const ifMatch = raw.match(/^\$if\((.+?)\)\s*\{([\s\S]*)\}/);
|
|
919
919
|
if (ifMatch) {
|
|
920
920
|
const condition = rewriteRefs(ifMatch[1].trim());
|
|
921
|
-
const inner = compileUINode(ifMatch[2].trim(), depth + 1);
|
|
921
|
+
const inner = compileUINode(ifMatch[2].trim(), depth + 1, preserveWs);
|
|
922
922
|
lines.push(`${indent}return $runtime.if(() => ${condition}, () => {`);
|
|
923
923
|
lines.push(inner);
|
|
924
924
|
lines.push(`${indent}})`);
|
|
@@ -929,7 +929,7 @@ function compileUINode(raw, depth) {
|
|
|
929
929
|
if (eachMatch) {
|
|
930
930
|
const arrayRef = rewriteRefs(eachMatch[1].trim());
|
|
931
931
|
const itemVar = eachMatch[2];
|
|
932
|
-
const inner = compileUINode(eachMatch[3].trim(), depth + 1);
|
|
932
|
+
const inner = compileUINode(eachMatch[3].trim(), depth + 1, preserveWs);
|
|
933
933
|
lines.push(`${indent}return $runtime.each(() => ${arrayRef}, (${itemVar}) => {`);
|
|
934
934
|
lines.push(inner);
|
|
935
935
|
lines.push(`${indent}})`);
|
|
@@ -939,7 +939,7 @@ function compileUINode(raw, depth) {
|
|
|
939
939
|
const showMatch = raw.match(/^\$show\((.+?)\)\s*\{([\s\S]*)\}/);
|
|
940
940
|
if (showMatch) {
|
|
941
941
|
const condition = rewriteRefs(showMatch[1].trim());
|
|
942
|
-
const inner = compileUINode(showMatch[2].trim(), depth + 1);
|
|
942
|
+
const inner = compileUINode(showMatch[2].trim(), depth + 1, preserveWs);
|
|
943
943
|
lines.push(`${indent}return $runtime.show(() => ${condition}, () => {`);
|
|
944
944
|
lines.push(inner);
|
|
945
945
|
lines.push(`${indent}})`);
|
|
@@ -950,7 +950,7 @@ function compileUINode(raw, depth) {
|
|
|
950
950
|
if (chainMatch) {
|
|
951
951
|
const innerEl = chainMatch[1].trim();
|
|
952
952
|
const chainStr = chainMatch[2].trim();
|
|
953
|
-
const elCode = compileElement(innerEl, depth);
|
|
953
|
+
const elCode = compileElement(innerEl, depth, preserveWs);
|
|
954
954
|
const chains = parseChains(chainStr);
|
|
955
955
|
lines.push(`${indent}const _chained = (() => {`);
|
|
956
956
|
lines.push(elCode);
|
|
@@ -961,9 +961,9 @@ function compileUINode(raw, depth) {
|
|
|
961
961
|
lines.push(`${indent}return _chained`);
|
|
962
962
|
return lines.join("\n");
|
|
963
963
|
}
|
|
964
|
-
return compileElement(raw, depth);
|
|
964
|
+
return compileElement(raw, depth, preserveWs);
|
|
965
965
|
}
|
|
966
|
-
function compileElement(raw, depth) {
|
|
966
|
+
function compileElement(raw, depth, preserveWs) {
|
|
967
967
|
const indent = " ".repeat(depth);
|
|
968
968
|
const lines = [];
|
|
969
969
|
const trimmed = raw.trim();
|
|
@@ -1011,22 +1011,34 @@ function compileElement(raw, depth) {
|
|
|
1011
1011
|
lines.push(`${indent}return $runtime.component('${tag}', { ${compileComponentProps(attrsRaw)} })`);
|
|
1012
1012
|
return lines.join("\n");
|
|
1013
1013
|
}
|
|
1014
|
+
// preserve whitespace inside <pre> elements
|
|
1015
|
+
const childPreserveWs = preserveWs || tag === 'pre';
|
|
1014
1016
|
const innerContent = findInnerContent(trimmed, tag, openTagEndIdx + 1);
|
|
1015
1017
|
lines.push(`${indent}const _el_${depth} = document.createElement('${tag}')`);
|
|
1016
1018
|
for (const attr of parseAttributes(attrsRaw)) {
|
|
1017
1019
|
lines.push(...compileAttr(attr, depth, indent));
|
|
1018
1020
|
}
|
|
1019
|
-
const children = splitChildren(innerContent);
|
|
1021
|
+
const children = splitChildren(innerContent, childPreserveWs);
|
|
1020
1022
|
let childIndex = 0;
|
|
1021
1023
|
for (let ci = 0; ci < children.length; ci++) {
|
|
1022
1024
|
const child = children[ci];
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
ct =
|
|
1025
|
+
let ct;
|
|
1026
|
+
const isChildElement = child.trimStart().startsWith('<') ||
|
|
1027
|
+
child.trimStart().startsWith('$') ||
|
|
1028
|
+
child.trimStart().startsWith('{<');
|
|
1029
|
+
if (childPreserveWs || isChildElement) {
|
|
1030
|
+
// preserve whitespace (inside <pre>, or element children that handle their own ws)
|
|
1031
|
+
ct = child;
|
|
1032
|
+
}
|
|
1033
|
+
else {
|
|
1034
|
+
// normalize whitespace: trim first child's leading, last child's trailing
|
|
1035
|
+
// but preserve boundary spaces for middle children (e.g. "text " before <span>)
|
|
1036
|
+
ct = child.replace(/\s*\n\s*/g, ' ');
|
|
1037
|
+
if (ci === 0)
|
|
1038
|
+
ct = ct.replace(/^\s+/, '');
|
|
1039
|
+
if (ci === children.length - 1)
|
|
1040
|
+
ct = ct.replace(/\s+$/, '');
|
|
1041
|
+
}
|
|
1030
1042
|
if (!ct)
|
|
1031
1043
|
continue;
|
|
1032
1044
|
if (ct.startsWith('{') && ct.endsWith('}') && !ct.startsWith('{<')) {
|
|
@@ -1047,7 +1059,7 @@ function compileElement(raw, depth) {
|
|
|
1047
1059
|
}
|
|
1048
1060
|
const childVar = `_child_${depth}_${childIndex}`;
|
|
1049
1061
|
lines.push(`${indent}const ${childVar} = (() => {`);
|
|
1050
|
-
lines.push(compileUINode(ct, depth + 1));
|
|
1062
|
+
lines.push(compileUINode(ct, depth + 1, childPreserveWs));
|
|
1051
1063
|
lines.push(`${indent}})()`);
|
|
1052
1064
|
lines.push(`${indent}if (${childVar} instanceof Node) _el_${depth}.appendChild(${childVar})`);
|
|
1053
1065
|
}
|
|
@@ -1293,18 +1305,22 @@ function normalizeWs(s) {
|
|
|
1293
1305
|
// but preserve meaningful spaces at word/element boundaries
|
|
1294
1306
|
return s.replace(/\s*\n\s*/g, ' ');
|
|
1295
1307
|
}
|
|
1296
|
-
function splitChildren(raw) {
|
|
1308
|
+
function splitChildren(raw, preserveWs = false) {
|
|
1297
1309
|
const parts = [];
|
|
1298
1310
|
let depth = 0;
|
|
1299
1311
|
let current = "";
|
|
1300
1312
|
let i = 0;
|
|
1313
|
+
// Only normalize text-only fragments; leave element children as-is
|
|
1314
|
+
// so that <pre> and similar tags can preserve their internal whitespace
|
|
1315
|
+
const ws = (s, isElement) => (preserveWs || isElement) ? s : normalizeWs(s);
|
|
1301
1316
|
while (i < raw.length) {
|
|
1302
1317
|
const ch = raw[i];
|
|
1303
1318
|
if (ch === "<") {
|
|
1304
1319
|
if (raw[i + 1] === "/") {
|
|
1305
1320
|
if (depth === 0) {
|
|
1306
1321
|
// closing tag for PARENT element — push current and skip tag
|
|
1307
|
-
const
|
|
1322
|
+
const isEl = current.trimStart().startsWith('<');
|
|
1323
|
+
const n = ws(current, isEl);
|
|
1308
1324
|
if (n.trim())
|
|
1309
1325
|
parts.push(n);
|
|
1310
1326
|
current = "";
|
|
@@ -1325,7 +1341,8 @@ function splitChildren(raw) {
|
|
|
1325
1341
|
current += raw[i]; // the >
|
|
1326
1342
|
i++;
|
|
1327
1343
|
}
|
|
1328
|
-
const
|
|
1344
|
+
const isEl = current.trimStart().startsWith('<');
|
|
1345
|
+
const n = ws(current, isEl);
|
|
1329
1346
|
if (n.trim())
|
|
1330
1347
|
parts.push(n);
|
|
1331
1348
|
current = "";
|
|
@@ -1349,14 +1366,20 @@ function splitChildren(raw) {
|
|
|
1349
1366
|
if (next.startsWith("<") ||
|
|
1350
1367
|
next.startsWith("$") ||
|
|
1351
1368
|
next.startsWith("{")) {
|
|
1352
|
-
const
|
|
1369
|
+
const isEl = current.trimStart().startsWith('<') ||
|
|
1370
|
+
current.trimStart().startsWith('$') ||
|
|
1371
|
+
current.trimStart().startsWith('{<');
|
|
1372
|
+
const n = ws(current, isEl);
|
|
1353
1373
|
if (n.trim())
|
|
1354
1374
|
parts.push(n);
|
|
1355
1375
|
current = "";
|
|
1356
1376
|
}
|
|
1357
1377
|
}
|
|
1358
1378
|
}
|
|
1359
|
-
const
|
|
1379
|
+
const isEl = current.trimStart().startsWith('<') ||
|
|
1380
|
+
current.trimStart().startsWith('$') ||
|
|
1381
|
+
current.trimStart().startsWith('{<');
|
|
1382
|
+
const n = ws(current, isEl);
|
|
1360
1383
|
if (n.trim())
|
|
1361
1384
|
parts.push(n);
|
|
1362
1385
|
return parts.filter(p => p.trim().length > 0);
|
|
@@ -1833,11 +1856,12 @@ async function findAvailablePort(preferredPort) {
|
|
|
1833
1856
|
tester.listen(port);
|
|
1834
1857
|
});
|
|
1835
1858
|
}
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1859
|
+
const startPort = Math.max(1, preferredPort);
|
|
1860
|
+
for (let port = startPort; port <= 65535; port++) {
|
|
1861
|
+
if (await check(port))
|
|
1862
|
+
return port;
|
|
1863
|
+
}
|
|
1864
|
+
throw new CLIError(`TardisError: no open port found\n Tried ports ${startPort}-65535`);
|
|
1841
1865
|
}
|
|
1842
1866
|
async function buildProject(cwd = process.cwd()) {
|
|
1843
1867
|
const start = Date.now();
|