@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 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
- // 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+$/, '');
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 n = normalizeWs(current);
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 n = normalizeWs(current);
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 n = normalizeWs(current);
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 n = normalizeWs(current);
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
- if (await check(preferredPort))
1837
- return preferredPort;
1838
- if (await check(preferredPort + 1))
1839
- return preferredPort + 1;
1840
- throw new CLIError(`TardisError: no open port found\n Tried ${preferredPort} and ${preferredPort + 1}`);
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();