tachyon-dom 0.1.0 → 0.1.1
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 +117 -274
- package/dist/cli.js +1 -1
- package/dist/compiler/expression.d.ts +4 -0
- package/dist/compiler/expression.js +11 -1
- package/dist/compiler/index.js +13 -4
- package/dist/compiler/parser.js +90 -7
- package/dist/compiler/sfc.js +6 -6
- package/dist/compiler/targets/client.js +24 -7
- package/dist/compiler.d.ts +1 -0
- package/dist/compiler.js +1 -0
- package/dist/index.d.ts +1 -8
- package/dist/index.js +1 -8
- package/dist/runtime/conditional.js +24 -12
- package/dist/runtime/list.js +5 -2
- package/dist/runtime/signal.d.ts +2 -0
- package/dist/runtime/signal.js +49 -2
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,268 +1,162 @@
|
|
|
1
1
|
# Tachyon DOM
|
|
2
2
|
|
|
3
|
-
Tachyon DOM is
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
The fixed syntax surface is documented in [`docs/syntax-spec.md`](docs/syntax-spec.md).
|
|
38
|
-
|
|
39
|
-
Routing is documented in [`docs/routing.md`](docs/routing.md), and runtime modules are summarized in [`docs/runtime.md`](docs/runtime.md).
|
|
40
|
-
|
|
41
|
-
The compiler output is intentionally close to the current direct DOM runtime: static markup stays static, dynamic text/class/event slots are recorded as explicit paths, and generated client code can import subpath helpers such as `tachyon-dom/runtime/text`.
|
|
42
|
-
|
|
43
|
-
The first server streaming adapter, `tachyon-dom/server/stream`, accepts sync or async HTML chunks and adapts them to `ReadableStream<Uint8Array>` or `Response` without building a single full HTML string first.
|
|
44
|
-
|
|
45
|
-
The first list runtime path, `tachyon-dom/runtime/list`, preserves keyed row elements across updates, moves reused elements into order, patches text/class bindings, removes stale rows, and keeps event handlers current through a mutable row scope. Row events are delegated through the list container so reused rows do not need one listener per row.
|
|
46
|
-
|
|
47
|
-
## App Layer
|
|
48
|
-
|
|
49
|
-
`tachyon-dom/app` provides a small app definition layer for examples and applications that should not need duplicated `main.ts`, `ssr.ts`, and HTML entry files. Define pages once, render SSR documents from the same definition, and let the Vite preset serve generated HTML in development and emit normalized HTML in production. With no option, development preserves tag formatting while production normalizes it. An explicit `htmlWhitespace` policy applies identically in both modes; legacy JavaScript values use the same runtime mapping, and invalid values fail during plugin creation before a server starts. The deprecated `minifyHtml` boolean also applies to both modes when explicitly set, while `htmlWhitespace` takes precedence:
|
|
50
|
-
|
|
51
|
-
Create a starter app with:
|
|
52
|
-
|
|
53
|
-
```sh
|
|
54
|
-
npm create tachyon-dom@latest my-app
|
|
55
|
-
pnpm create tachyon-dom my-app
|
|
3
|
+
Tachyon DOM is an experimental HTML-first compiler that turns static templates into direct DOM updates, using a small fine-grained runtime shared with SSR and streaming targets.
|
|
4
|
+
|
|
5
|
+
> Experimental status: the compiler and runtime are ready for evaluation, examples, and incremental adoption, but public APIs may change before a stable release.
|
|
6
|
+
|
|
7
|
+
## Why Tachyon DOM?
|
|
8
|
+
|
|
9
|
+
- HTML stays recognizable while dynamic fields become explicit DOM paths.
|
|
10
|
+
- Fine-grained updates write to cached text, attribute, class, and list targets without a virtual DOM.
|
|
11
|
+
- One parsed template feeds client, buffered SSR, and streaming server targets.
|
|
12
|
+
- Browser-safe runtime imports keep compiler and server dependencies out of client bundles.
|
|
13
|
+
|
|
14
|
+
## Quick Example
|
|
15
|
+
|
|
16
|
+
This `.td` component combines a signal-backed counter with a keyed list:
|
|
17
|
+
|
|
18
|
+
```html
|
|
19
|
+
<script setup lang="ts">
|
|
20
|
+
const count = createSignal(0);
|
|
21
|
+
const rows = createSignal([
|
|
22
|
+
{ id: 1, label: "Alpha" },
|
|
23
|
+
{ id: 2, label: "Beta" },
|
|
24
|
+
]);
|
|
25
|
+
const increment = (): void => count.update((value) => value + 1);
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<main>
|
|
29
|
+
<button on:click={increment}>{count}</button>
|
|
30
|
+
<ul>
|
|
31
|
+
<for each={rows} key={row.id}>
|
|
32
|
+
<li>{row.label}</li>
|
|
33
|
+
</for>
|
|
34
|
+
</ul>
|
|
35
|
+
</main>
|
|
56
36
|
```
|
|
57
37
|
|
|
58
|
-
|
|
59
|
-
import { defineApp } from "tachyon-dom/app";
|
|
60
|
-
import { tachyonApp, tachyonDom } from "tachyon-dom/vite";
|
|
61
|
-
|
|
62
|
-
export const app = defineApp({
|
|
63
|
-
pages: [
|
|
64
|
-
{
|
|
65
|
-
path: "/",
|
|
66
|
-
fileName: "index.html",
|
|
67
|
-
template: "<section><h1>{title}</h1></section>",
|
|
68
|
-
scope: { title: "Home" },
|
|
69
|
-
},
|
|
70
|
-
],
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
export default {
|
|
74
|
-
plugins: [tachyonDom({ reactive: true }), tachyonApp(app)],
|
|
75
|
-
};
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
Route-local template conventions are available through `pagesFromRouteFiles()`: `src/routes/index/page.td` maps to `/`, `src/routes/counter/page.td` maps to `/counter/`, `[id]` maps to `:id`, and `[...slug]` maps to a named wildcard. Generated starters keep these pages in `src/routes.generated.ts`; `tachyon-dom add page settings/profile` updates that registry, so the new URL is available to the app, Vite, tests, and SSR without a manual import.
|
|
79
|
-
|
|
80
|
-
`defineApp()` rejects every duplicate normalized route and output filename before compilation or serving. Path aliases such as `/`, `/index.html`, `/guide`, and `/guide/` share their normalized route keys. `renderAppDocument()` throws for an unknown path instead of falling back to home. SSR adapters should use `renderAppResponse()`, which returns `{ status, html }`; missing routes retain status 404 and render either the configured `notFound` page or the built-in not-found document.
|
|
81
|
-
|
|
82
|
-
`examples/full-app` is a multi-page browser example with SSR initial HTML for every page, a persistent layout, client-side routing, counters, keyed lists, forms, settings, and compiler/stream diagnostics. Run it with `pnpm example:full-app`, then open the Vite dev server root. Source pages are route-local `.td` templates, and the example Vite config generates dev/build HTML entries instead of keeping `index.html` files in source. Production output is available with `pnpm example:full-app:build`; the example config builds every generated page entry and safely condenses HTML tag syntax during build.
|
|
38
|
+
The server targets escape interpolated text. The client target records direct paths to the button text and list container, then updates only those targets when their signals change.
|
|
83
39
|
|
|
84
|
-
|
|
40
|
+
## What the Compiler Emits
|
|
85
41
|
|
|
86
|
-
|
|
42
|
+
The real output uses modular runtime imports and explicit cleanup ownership. Abbreviated, the hot path looks like this:
|
|
87
43
|
|
|
88
|
-
|
|
44
|
+
```js
|
|
45
|
+
const countText = __tachyonTextAt(root, [0, 0]);
|
|
46
|
+
cleanups.push(__tachyonEffect(() => __tachyonSetText(countText, __tachyonRead(scope.count))));
|
|
89
47
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
| Vite app plugin creation | Throws the migration error directly before a development server or build starts. |
|
|
96
|
-
| Buffered router | Resolves to the generic internal-error document with status 500 because route rendering owns an error boundary. |
|
|
97
|
-
| Streaming router | Rejects with the migration error before returning stream metadata or chunks. |
|
|
98
|
-
| Workers and Node buffered adapters | Return the buffered router's generic 500 response. |
|
|
99
|
-
| Lambda proxy buffered adapter | Returns the generic Lambda proxy response with status 500. |
|
|
100
|
-
| Workers, Node, and Lambda streaming adapters | Reject before committing a response when `streaming: true` selects the streaming router. |
|
|
101
|
-
|
|
102
|
-
## Recommended App Shape
|
|
103
|
-
|
|
104
|
-
For applications, keep route markup in route-local `.td` files and let Vite run the Tachyon DOM plugins on the main development path:
|
|
105
|
-
|
|
106
|
-
```text
|
|
107
|
-
src/
|
|
108
|
-
routes/
|
|
109
|
-
index/
|
|
110
|
-
page.td
|
|
111
|
-
page.td.d.ts
|
|
112
|
-
routes.generated.ts
|
|
113
|
-
client/
|
|
114
|
-
main.ts
|
|
115
|
-
app.ts
|
|
116
|
-
vite.config.ts
|
|
48
|
+
cleanups.push(
|
|
49
|
+
__tachyonEffect(() =>
|
|
50
|
+
__tachyonMountKeyedList(listRoot, [], __tachyonRead(scope.rows), listOptions),
|
|
51
|
+
),
|
|
52
|
+
);
|
|
117
53
|
```
|
|
118
54
|
|
|
119
|
-
|
|
55
|
+
Static markup remains in a reusable template. Events are attached once per created target, keyed rows retain their DOM nodes while moving, and the generated binding root disposes effects, resources, events, and controls together.
|
|
120
56
|
|
|
121
|
-
|
|
57
|
+
See the [syntax specification](docs/syntax-spec.md) for supported expressions and exact target behavior.
|
|
122
58
|
|
|
123
|
-
|
|
124
|
-
/// <reference types="tachyon-dom/td-modules" />
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
For project-wide setup, add `"tachyon-dom/td-modules"` to `compilerOptions.types` alongside `"vite/client"`. The type entry covers `.td`, `.td?client`, `.td?server`, `.td?stream`, and `.td?raw` imports. During normal Vite development and builds, `tachyonDom()` writes an adjacent `.td.d.ts` for each transformed template. These declarations preserve exported SFC `scope()` types, require every referenced template field, and type event handlers. The generated starter and `add page` command create the initial declarations so a clean project typechecks before its first Vite run. `tachyon-dom typegen` remains available for non-Vite tooling but is not required by the standard workflow.
|
|
128
|
-
|
|
129
|
-
## Typed Templates and Environment Validation
|
|
59
|
+
## Measured Size
|
|
130
60
|
|
|
131
|
-
`tachyon-dom
|
|
132
|
-
|
|
133
|
-
```ts
|
|
134
|
-
import { defineTemplate, templateScope, type TypedTemplate } from "tachyon-dom/typed";
|
|
61
|
+
`import { createSignal } from "tachyon-dom"` bundles to **600 bytes minified** in the current esbuild contract.
|
|
135
62
|
|
|
136
|
-
|
|
137
|
-
title: string;
|
|
138
|
-
count: number;
|
|
139
|
-
};
|
|
63
|
+
Quick example client bundle: 11519 bytes minified, 3838 bytes Brotli, including the counter, keyed-list binding code, and browser runtime. CI rejects TypeScript, parse5, compiler, server, app, and language-server modules from both browser metafiles.
|
|
140
64
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
void typed;
|
|
146
|
-
void scoped;
|
|
65
|
+
```sh
|
|
66
|
+
pnpm build
|
|
67
|
+
pnpm check:browser-entry
|
|
68
|
+
pnpm check:quick-example-size
|
|
147
69
|
```
|
|
148
70
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
```ts
|
|
152
|
-
import { defineEnvSchema, readEnv } from "tachyon-dom/env";
|
|
153
|
-
|
|
154
|
-
const schema = defineEnvSchema({
|
|
155
|
-
PUBLIC_APP_NAME: { default: "Tachyon App", public: true },
|
|
156
|
-
SESSION_SECRET: { required: true },
|
|
157
|
-
});
|
|
71
|
+
These numbers are produced from repository scripts, not estimated from source files. Subpath size budgets remain available through `pnpm check:size`.
|
|
158
72
|
|
|
159
|
-
|
|
160
|
-
if (!result.ok) {
|
|
161
|
-
throw new Error(result.error.map((error) => error.message).join("\n"));
|
|
162
|
-
}
|
|
73
|
+
## Keyed List Benchmark
|
|
163
74
|
|
|
164
|
-
|
|
165
|
-
result.value.env.SESSION_SECRET;
|
|
166
|
-
```
|
|
75
|
+
On this machine, Tachyon DOM performed within 3% of the comparison implementations across nine keyed-row operations.
|
|
167
76
|
|
|
168
|
-
|
|
77
|
+
| Compared with | Tachyon DOM result |
|
|
78
|
+
| --- | ---: |
|
|
79
|
+
| vanillajs-lite-keyed | 1.8% faster |
|
|
80
|
+
| solid-keyed | 0.9% faster |
|
|
81
|
+
| marko-keyed | 2.5% slower |
|
|
169
82
|
|
|
170
|
-
|
|
83
|
+
Results are the geometric mean of nine operations: row creation, replacement, partial updates, selection, swapping, removal, append, clear, and creation of many rows. Lower execution time is better.
|
|
171
84
|
|
|
172
|
-
The
|
|
85
|
+
The recorded run used:
|
|
173
86
|
|
|
174
|
-
|
|
87
|
+
- AMD Ryzen 9 9950X
|
|
88
|
+
- Chromium 149.0.7827.55
|
|
89
|
+
- Production Vite builds
|
|
90
|
+
- 2 warmup runs and 7 measured runs per operation
|
|
91
|
+
- 20% trimmed means to reduce outlier influence
|
|
175
92
|
|
|
176
|
-
|
|
93
|
+
The [complete JSON artifact](benchmark/local-compare/results/2026-07-13-readme-baseline.json) includes raw samples, variability, dependency versions, and clean Git provenance.
|
|
177
94
|
|
|
178
|
-
|
|
95
|
+
Reproduce the same benchmark contract locally:
|
|
179
96
|
|
|
180
|
-
|
|
97
|
+
```sh
|
|
98
|
+
pnpm bench:local
|
|
99
|
+
```
|
|
181
100
|
|
|
182
|
-
|
|
101
|
+
This is a repository-local comparison based on the `js-framework-benchmark` row-operation model. It is not an official upstream result or a general ranking of framework performance. See the [benchmark methodology](benchmark/README.md) for details.
|
|
183
102
|
|
|
184
|
-
|
|
103
|
+
## Install
|
|
185
104
|
|
|
186
|
-
|
|
105
|
+
Create a route-local starter:
|
|
187
106
|
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
new URL(request.url).searchParams.has("prod") ? "/client/main.js" : "/src/client/main.ts";
|
|
194
|
-
|
|
195
|
-
export default defineConfig({
|
|
196
|
-
plugins: [
|
|
197
|
-
tachyonDom({ reactive: true }),
|
|
198
|
-
tachyonSsr({
|
|
199
|
-
clientScript,
|
|
200
|
-
fetch: async (request, { clientScript }) => {
|
|
201
|
-
const user = new URL(request.url).searchParams.get("user") ?? "Guest";
|
|
202
|
-
return new Response(`<main>Hello ${user}</main><script type="module" src="${clientScript ?? ""}"></script>`, {
|
|
203
|
-
headers: { "content-type": "text/html; charset=utf-8" },
|
|
204
|
-
});
|
|
205
|
-
},
|
|
206
|
-
}),
|
|
207
|
-
],
|
|
208
|
-
});
|
|
107
|
+
```sh
|
|
108
|
+
npm create tachyon-dom@latest my-app
|
|
109
|
+
cd my-app
|
|
110
|
+
pnpm install
|
|
111
|
+
pnpm dev
|
|
209
112
|
```
|
|
210
113
|
|
|
211
|
-
|
|
114
|
+
The equivalent pnpm command is `pnpm create tachyon-dom my-app`. For an installed package, use `tachyon-dom init --template basic --out my-app` or select the `ssr` template.
|
|
212
115
|
|
|
213
|
-
|
|
116
|
+
The normal project shape keeps route markup in `src/routes/**/page.td`, client code in `src/client/main.ts`, and generated route registration in `src/routes.generated.ts`. Start with the [getting-started guide](docs/getting-started.md).
|
|
214
117
|
|
|
215
|
-
|
|
118
|
+
## Compiler and Runtime Boundaries
|
|
216
119
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
`tachyon-dom/testing` includes helpers for fast server-side assertions without starting Vite. Use `renderTdForTest(filePath, scope, { locale })` in Vitest when you want to assert the SSR HTML for a `.td` template directly:
|
|
120
|
+
The package root contains browser-safe reactive and runtime APIs such as `createSignal()`, `createMemo()`, `createResource()`, `createRoot()`, `onCleanup()`, and the client router. Heavier tools use explicit subpaths:
|
|
220
121
|
|
|
221
122
|
```ts
|
|
222
|
-
import {
|
|
223
|
-
import {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
userName: `<img src=x onerror=alert(1)>`,
|
|
229
|
-
}),
|
|
230
|
-
).resolves.toContain("<img src=x onerror=alert(1)>");
|
|
231
|
-
});
|
|
123
|
+
import { createSignal } from "tachyon-dom";
|
|
124
|
+
import { compileTemplate } from "tachyon-dom/compiler";
|
|
125
|
+
import { defineApp } from "tachyon-dom/app";
|
|
126
|
+
import { createRouter } from "tachyon-dom/router";
|
|
127
|
+
import { html } from "tachyon-dom/server/html";
|
|
128
|
+
import { tachyonDom } from "tachyon-dom/vite";
|
|
232
129
|
```
|
|
233
130
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
## Security Notes
|
|
131
|
+
This boundary keeps TypeScript, parse5, language-server, app, and server graphs away from a browser consumer that only needs reactivity.
|
|
237
132
|
|
|
238
|
-
|
|
133
|
+
## Documentation
|
|
239
134
|
|
|
240
|
-
|
|
135
|
+
- [Getting started](docs/getting-started.md): starter creation, project shape, route files, template types, and testing.
|
|
136
|
+
- [Syntax specification](docs/syntax-spec.md): HTML-first syntax, expressions, directives, components, hydration, and streaming constructs.
|
|
137
|
+
- [Runtime](docs/runtime.md): signals, ownership, DOM helpers, keyed lists, forms, enhancements, portals, and hydration.
|
|
138
|
+
- [App and Vite](docs/app-vite.md): app definitions, file routes, plugins, request-scoped SSR, logging, and packaging.
|
|
139
|
+
- [Routing](docs/routing.md): server routes, layouts, loaders, actions, streaming, and client navigation.
|
|
140
|
+
- [Server adapters](docs/adapters.md): Workers, Node, Lambda, static assets, origins, and deployment behavior.
|
|
141
|
+
- [Security](docs/security.md): escaping, trusted HTML, sanitizers, redirects, CSRF, hosts, proxies, and origins.
|
|
142
|
+
- [Whitespace migration](docs/migrations/whitespace.md): compiler versus document policies, legacy mappings, and boundary outcomes.
|
|
143
|
+
- [Benchmarks](benchmark/README.md): provenance, contracts, reproduction, and interpretation.
|
|
144
|
+
- [Releasing](docs/releasing.md): package verification, Trusted Publishing, and release commands.
|
|
145
|
+
- [Changelog](CHANGELOG.md): release history, breaking changes, fixes, and security notes.
|
|
241
146
|
|
|
242
|
-
|
|
243
|
-
import { escapeToHtml, trustedHtmlChunk, type RouteDefinition } from "tachyon-dom/router";
|
|
244
|
-
|
|
245
|
-
const route: RouteDefinition = {
|
|
246
|
-
path: "/search",
|
|
247
|
-
loader: ({ url }) => url.searchParams.get("q") ?? "",
|
|
248
|
-
render: () => "",
|
|
249
|
-
stream: async function* ({ data }) {
|
|
250
|
-
yield `<p>${trustedHtmlChunk(escapeToHtml(data))}</p>`;
|
|
251
|
-
},
|
|
252
|
-
};
|
|
253
|
-
```
|
|
147
|
+
## Security
|
|
254
148
|
|
|
255
|
-
`
|
|
149
|
+
Template interpolation and `tachyon-dom/server/html` escape text by default. `rawHtml()`, `trustedHtmlChunk()`, and route `stream()` chunks are explicit trust boundaries. Sanitize user-generated markup with a vetted runtime adapter, configure public origins and trusted proxy behavior, and apply method plus CSRF/Origin checks before direct form actions.
|
|
256
150
|
|
|
257
|
-
|
|
151
|
+
See the [security guide](docs/security.md) before deploying server-rendered or streamed applications.
|
|
258
152
|
|
|
259
|
-
|
|
153
|
+
## Project Status
|
|
260
154
|
|
|
261
|
-
The
|
|
155
|
+
The current compiler supports text, class, attribute, style, ref, model, and event bindings; keyed lists and conditionals; local stores and components; hydration boundaries; buffered SSR; and streaming targets. The runtime uses fine-grained reactive ownership so component cleanup stops effects, aborts resources, and detaches DOM bindings.
|
|
262
156
|
|
|
263
|
-
|
|
157
|
+
Tachyon DOM is still experimental. Evaluate compatibility, output, accessibility, security boundaries, and benchmark relevance for your application before production adoption.
|
|
264
158
|
|
|
265
|
-
##
|
|
159
|
+
## Development
|
|
266
160
|
|
|
267
161
|
```sh
|
|
268
162
|
pnpm install
|
|
@@ -271,69 +165,18 @@ pnpm build
|
|
|
271
165
|
pnpm lint
|
|
272
166
|
pnpm check:exports
|
|
273
167
|
pnpm check:size
|
|
274
|
-
pnpm
|
|
275
|
-
pnpm
|
|
276
|
-
pnpm bench:local:full
|
|
277
|
-
pnpm bench:local:gate
|
|
278
|
-
pnpm bench:local:smoke
|
|
168
|
+
pnpm check:browser-entry
|
|
169
|
+
pnpm check:quick-example-size
|
|
279
170
|
```
|
|
280
171
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
```sh
|
|
284
|
-
tachyon-dom compile view.td --target client --out view.js
|
|
285
|
-
tachyon-dom routes src/routes --out route-manifest.json
|
|
286
|
-
tachyon-dom typegen src/routes/index/page.td --out src/routes/index/page.td.ts
|
|
287
|
-
tachyon-dom add page settings/profile --routes-dir src/routes
|
|
288
|
-
npm create tachyon-dom@latest my-app
|
|
289
|
-
pnpm create tachyon-dom my-app
|
|
290
|
-
tachyon-dom init --template basic --out my-app
|
|
291
|
-
tachyon-dom dev --host 127.0.0.1 --port 5173
|
|
292
|
-
tachyon-dom build
|
|
293
|
-
tachyon-dom preview --host 127.0.0.1 --port 4173
|
|
294
|
-
tachyon-dom language-server --stdio
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
`npm create tachyon-dom@latest my-app` and `pnpm create tachyon-dom my-app` create a route-local starter with `src/routes/index/page.td`, `src/client/main.ts`, `src/app.ts`, `vite.config.ts`, `tsconfig.json`, `.gitignore`, a smoke test, CI workflow, `README.md`, and `package.json`. `tachyon-dom init --template basic --out my-app` is the equivalent installed-package command. Use `tachyon-dom init --template ssr --out my-app` when you also want a small `src/server.ts` SSR composition entry.
|
|
298
|
-
|
|
299
|
-
`tachyon-dom init` and `tachyon-dom add page` preserve existing files by default. A starter conflict aborts before any file is written. Pass `--force` only when you deliberately want to overwrite the listed managed files; the command reports every overwritten path.
|
|
300
|
-
|
|
301
|
-
`tachyon-dom add page` also reports the normalized route URL, adjacent declaration file, and generated registry it changed. For example, `users/[id]` reports `/users/:id/`, while `blog/[...slug]` reports `/blog/*slug/`.
|
|
302
|
-
|
|
303
|
-
Template files use the short `.td` extension. The Vite plugin and file router also accept `.tachyon` and `.tachyon.html` for compatibility.
|
|
304
|
-
|
|
305
|
-
`tachyon-dom language-server --stdio` starts a minimal Language Server Protocol server for editor integrations. The first version reports Tachyon template diagnostics for `.td`, `.tachyon`, and `.tachyon.html` documents; completion, hover, and semantic tokens are intentionally left to later editor-support slices.
|
|
306
|
-
|
|
307
|
-
`pnpm bench:local` builds every implementation in production mode (bundled and minified, matching what applications ship), serves them from a temporary Vite preview server, measures Tachyon DOM against local copies of the keyed vanilla benchmark implementations in Playwright Chromium, prints ratio tables, and writes JSON results under `benchmark/local-compare/results/`. Production is the canonical mode because dev-server module loading adds overhead that does not exist in a shipped app.
|
|
308
|
-
|
|
309
|
-
`pnpm bench:local:dev` runs the same comparison against the unbundled dev server for fast local iteration (no production build step).
|
|
310
|
-
|
|
311
|
-
`pnpm bench:local:full` runs the canonical production comparison with extra warmup and measured iterations for noisier performance investigations.
|
|
312
|
-
|
|
313
|
-
`pnpm bench:local:smoke` runs the same local comparison with one warmup and one measured iteration for manual benchmark smoke checks. The CI workflow exposes it through `workflow_dispatch`; normal push and pull request runs skip the benchmark.
|
|
314
|
-
|
|
315
|
-
`pnpm bench:local:gate` runs the local comparison with operation and memory regression thresholds.
|
|
316
|
-
|
|
317
|
-
The local benchmark prints row-operation timings plus auxiliary metrics for startup, JS heap usage, DOM node counts, and local source size. JSON output also keeps per-run values with mean, median, min, max, and p95.
|
|
318
|
-
|
|
319
|
-
`pnpm check:exports` runs `publint --strict` and `attw --pack --no-emoji` against the package exports and declaration files. `pnpm check:size` runs per-subpath size budgets for the root entry, selected runtime helpers including `runtime/signal`, `runtime/list`, `runtime/error-boundary`, the client router, `i18n`, and the server HTML helper. CI runs both gates after `pnpm build` and `pnpm verify:package`.
|
|
320
|
-
|
|
321
|
-
Version tags matching `v*` publish through GitHub Actions with `npm publish --access public` after the same build, package, exports, and size checks pass. npm provenance is intentionally omitted because this source repository is private.
|
|
322
|
-
|
|
323
|
-
## Example
|
|
172
|
+
Useful examples:
|
|
324
173
|
|
|
325
174
|
```sh
|
|
326
175
|
pnpm example:web
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
Open `http://127.0.0.1:5173/examples/web/` to try the browser example. It shows the compiled SSR preview, hydrate boundary markers, store updates, keyed list updates, stream chunks, and generated client module shape in one page.
|
|
330
|
-
|
|
331
|
-
Open `http://127.0.0.1:5173/examples/auth-todo/` for the authenticated todo example. Browser example HTML entries are generated by the Vite dev plugin, so these examples do not need hand-written `index.html` files.
|
|
332
|
-
|
|
333
|
-
```sh
|
|
176
|
+
pnpm example:full-app
|
|
334
177
|
pnpm example:stream
|
|
335
178
|
```
|
|
336
179
|
|
|
337
|
-
|
|
180
|
+
## License
|
|
338
181
|
|
|
339
|
-
|
|
182
|
+
MIT
|
package/dist/cli.js
CHANGED
|
@@ -344,7 +344,7 @@ const validateExpressionNode = (node) => {
|
|
|
344
344
|
}
|
|
345
345
|
return ok(node);
|
|
346
346
|
}
|
|
347
|
-
if (node.type === "literal") {
|
|
347
|
+
if (node.type === "literal" || node.type === "regex") {
|
|
348
348
|
return ok(node);
|
|
349
349
|
}
|
|
350
350
|
if (node.type === "array") {
|
|
@@ -454,6 +454,10 @@ const loadOxcParser = () => {
|
|
|
454
454
|
};
|
|
455
455
|
const oxcExpressionToNode = (node) => {
|
|
456
456
|
if (node.type === "Literal") {
|
|
457
|
+
const regex = "regex" in node ? node.regex : undefined;
|
|
458
|
+
if (regex) {
|
|
459
|
+
return ok({ type: "regex", pattern: regex.pattern, flags: regex.flags });
|
|
460
|
+
}
|
|
457
461
|
if (typeof node.value === "string" ||
|
|
458
462
|
typeof node.value === "number" ||
|
|
459
463
|
typeof node.value === "boolean" ||
|
|
@@ -657,6 +661,9 @@ export const evaluateExpressionNode = (node, scope) => {
|
|
|
657
661
|
if (node.type === "literal") {
|
|
658
662
|
return node.value;
|
|
659
663
|
}
|
|
664
|
+
if (node.type === "regex") {
|
|
665
|
+
return new RegExp(node.pattern, node.flags);
|
|
666
|
+
}
|
|
660
667
|
if (node.type === "array") {
|
|
661
668
|
return node.items.map((item) => evaluateExpressionNode(item, scope));
|
|
662
669
|
}
|
|
@@ -797,6 +804,9 @@ export const expressionNodeToJs = (node, locals = new Set(), scopeName = "scope"
|
|
|
797
804
|
if (node.type === "literal") {
|
|
798
805
|
return JSON.stringify(node.value);
|
|
799
806
|
}
|
|
807
|
+
if (node.type === "regex") {
|
|
808
|
+
return `new RegExp(${JSON.stringify(node.pattern)}, ${JSON.stringify(node.flags)})`;
|
|
809
|
+
}
|
|
800
810
|
if (node.type === "array") {
|
|
801
811
|
return `[${node.items.map((item) => expressionNodeToJs(item, locals, scopeName)).join(", ")}]`;
|
|
802
812
|
}
|
package/dist/compiler/index.js
CHANGED
|
@@ -5,11 +5,20 @@ import { lowerClientTemplate } from "./targets/client.js";
|
|
|
5
5
|
import { applyTemplateWhitespace } from "./whitespace.js";
|
|
6
6
|
const compileCacheLimit = 128;
|
|
7
7
|
const compileCache = new Map();
|
|
8
|
+
const deepFreeze = (value, seen = new WeakSet()) => {
|
|
9
|
+
if (value === null || typeof value !== "object" || seen.has(value))
|
|
10
|
+
return value;
|
|
11
|
+
seen.add(value);
|
|
12
|
+
for (const child of Object.values(value))
|
|
13
|
+
deepFreeze(child, seen);
|
|
14
|
+
return Object.freeze(value);
|
|
15
|
+
};
|
|
8
16
|
const rememberCompiledTemplate = (cacheKey, result) => {
|
|
17
|
+
const frozenResult = deepFreeze(result);
|
|
9
18
|
if (compileCache.has(cacheKey)) {
|
|
10
19
|
compileCache.delete(cacheKey);
|
|
11
20
|
}
|
|
12
|
-
compileCache.set(cacheKey,
|
|
21
|
+
compileCache.set(cacheKey, frozenResult);
|
|
13
22
|
while (compileCache.size > compileCacheLimit) {
|
|
14
23
|
const oldest = compileCache.keys().next().value;
|
|
15
24
|
if (oldest === undefined) {
|
|
@@ -17,7 +26,7 @@ const rememberCompiledTemplate = (cacheKey, result) => {
|
|
|
17
26
|
}
|
|
18
27
|
compileCache.delete(oldest);
|
|
19
28
|
}
|
|
20
|
-
return
|
|
29
|
+
return frozenResult;
|
|
21
30
|
};
|
|
22
31
|
export const compileTemplate = (source, options = {}) => {
|
|
23
32
|
const whitespace = options.whitespace ?? "preserve";
|
|
@@ -37,12 +46,12 @@ export const compileTemplate = (source, options = {}) => {
|
|
|
37
46
|
if (!irResult.ok) {
|
|
38
47
|
return rememberCompiledTemplate(cacheKey, err(irResult.error));
|
|
39
48
|
}
|
|
40
|
-
return rememberCompiledTemplate(cacheKey, ok({
|
|
49
|
+
return rememberCompiledTemplate(cacheKey, ok(deepFreeze({
|
|
41
50
|
source,
|
|
42
51
|
ir: irResult.value,
|
|
43
52
|
root: irResult.value.root,
|
|
44
53
|
client: lowerClientTemplate(irResult.value.root),
|
|
45
|
-
}));
|
|
54
|
+
})));
|
|
46
55
|
};
|
|
47
56
|
export * from "./types.js";
|
|
48
57
|
export { generateClientModule } from "./targets/client.js";
|
package/dist/compiler/parser.js
CHANGED
|
@@ -46,34 +46,117 @@ const readQuotedValue = (parser) => {
|
|
|
46
46
|
};
|
|
47
47
|
const readBracedValue = (parser) => {
|
|
48
48
|
let depth = 0;
|
|
49
|
-
let
|
|
49
|
+
let mode = "code";
|
|
50
|
+
let escaped = false;
|
|
51
|
+
let regexCharacterClass = false;
|
|
52
|
+
let canStartRegex = true;
|
|
53
|
+
const templateExpressionDepths = [];
|
|
50
54
|
const start = parser.offset;
|
|
51
55
|
while (parser.offset < parser.source.length) {
|
|
52
56
|
const char = parser.source[parser.offset];
|
|
53
|
-
const
|
|
54
|
-
if (
|
|
55
|
-
if (char ===
|
|
56
|
-
|
|
57
|
+
const next = parser.source[parser.offset + 1];
|
|
58
|
+
if (mode === "line-comment") {
|
|
59
|
+
if (char === "\n" || char === "\r")
|
|
60
|
+
mode = "code";
|
|
61
|
+
parser.offset++;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (mode === "block-comment") {
|
|
65
|
+
if (char === "*" && next === "/") {
|
|
66
|
+
mode = "code";
|
|
67
|
+
parser.offset += 2;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
parser.offset++;
|
|
71
|
+
}
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (mode === "single" || mode === "double") {
|
|
75
|
+
if (!escaped && char === (mode === "single" ? "'" : '"'))
|
|
76
|
+
mode = "code";
|
|
77
|
+
escaped = !escaped && char === "\\";
|
|
78
|
+
parser.offset++;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (mode === "regex") {
|
|
82
|
+
if (!escaped) {
|
|
83
|
+
if (char === "[")
|
|
84
|
+
regexCharacterClass = true;
|
|
85
|
+
if (char === "]")
|
|
86
|
+
regexCharacterClass = false;
|
|
87
|
+
if (char === "/" && !regexCharacterClass) {
|
|
88
|
+
mode = "code";
|
|
89
|
+
canStartRegex = false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
escaped = !escaped && char === "\\";
|
|
93
|
+
parser.offset++;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (mode === "template") {
|
|
97
|
+
if (!escaped && char === "`") {
|
|
98
|
+
mode = "code";
|
|
99
|
+
canStartRegex = false;
|
|
100
|
+
parser.offset++;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (!escaped && char === "$" && next === "{") {
|
|
104
|
+
depth++;
|
|
105
|
+
templateExpressionDepths.push(depth);
|
|
106
|
+
mode = "code";
|
|
107
|
+
canStartRegex = true;
|
|
108
|
+
parser.offset += 2;
|
|
109
|
+
continue;
|
|
57
110
|
}
|
|
111
|
+
escaped = !escaped && char === "\\";
|
|
112
|
+
parser.offset++;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (char === "/" && next === "/") {
|
|
116
|
+
mode = "line-comment";
|
|
117
|
+
parser.offset += 2;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (char === "/" && next === "*") {
|
|
121
|
+
mode = "block-comment";
|
|
122
|
+
parser.offset += 2;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (char === "/" && canStartRegex) {
|
|
126
|
+
mode = "regex";
|
|
127
|
+
escaped = false;
|
|
128
|
+
regexCharacterClass = false;
|
|
58
129
|
parser.offset++;
|
|
59
130
|
continue;
|
|
60
131
|
}
|
|
61
|
-
if (char === `"` || char === `'`) {
|
|
62
|
-
|
|
132
|
+
if (char === `"` || char === `'` || char === "`") {
|
|
133
|
+
mode = char === `"` ? "double" : char === `'` ? "single" : "template";
|
|
134
|
+
escaped = false;
|
|
63
135
|
parser.offset++;
|
|
64
136
|
continue;
|
|
65
137
|
}
|
|
66
138
|
if (char === "{") {
|
|
67
139
|
depth++;
|
|
140
|
+
canStartRegex = true;
|
|
68
141
|
}
|
|
69
142
|
else if (char === "}") {
|
|
70
143
|
depth--;
|
|
71
144
|
parser.offset++;
|
|
145
|
+
if (templateExpressionDepths.at(-1) === depth + 1) {
|
|
146
|
+
templateExpressionDepths.pop();
|
|
147
|
+
mode = "template";
|
|
148
|
+
escaped = false;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
72
151
|
if (depth === 0) {
|
|
73
152
|
return ok(parser.source.slice(start, parser.offset));
|
|
74
153
|
}
|
|
154
|
+
canStartRegex = false;
|
|
75
155
|
continue;
|
|
76
156
|
}
|
|
157
|
+
else if (!isWhitespace(char)) {
|
|
158
|
+
canStartRegex = "([,:;!?=+-*%&|^~<>".includes(char);
|
|
159
|
+
}
|
|
77
160
|
parser.offset++;
|
|
78
161
|
}
|
|
79
162
|
return parserError(parser, "Unclosed braced attribute value.");
|
package/dist/compiler/sfc.js
CHANGED
|
@@ -7,20 +7,20 @@ export const sfcSetupScopeName = "__tachyonSfcSetupScope";
|
|
|
7
7
|
const scriptOpenPattern = /<script\b([^>]*)>/gi;
|
|
8
8
|
const autoImports = {
|
|
9
9
|
batch: "tachyon-dom",
|
|
10
|
-
compileTachyonSfc: "tachyon-dom",
|
|
11
|
-
createClientRouter: "tachyon-dom",
|
|
10
|
+
compileTachyonSfc: "tachyon-dom/compiler",
|
|
11
|
+
createClientRouter: "tachyon-dom/runtime/router",
|
|
12
12
|
createMemo: "tachyon-dom",
|
|
13
13
|
createSignal: "tachyon-dom",
|
|
14
14
|
createStore: "tachyon-dom",
|
|
15
15
|
effect: "tachyon-dom",
|
|
16
16
|
enhanceForm: "tachyon-dom",
|
|
17
17
|
err: "tachyon-dom",
|
|
18
|
-
generateClientModule: "tachyon-dom",
|
|
19
|
-
generateServerStreamModule: "tachyon-dom",
|
|
18
|
+
generateClientModule: "tachyon-dom/compiler",
|
|
19
|
+
generateServerStreamModule: "tachyon-dom/compiler",
|
|
20
20
|
ok: "tachyon-dom",
|
|
21
21
|
readTextStreamChunks: "tachyon-dom",
|
|
22
|
-
renderServerTemplate: "tachyon-dom",
|
|
23
|
-
renderToReadableStream: "tachyon-dom",
|
|
22
|
+
renderServerTemplate: "tachyon-dom/compiler",
|
|
23
|
+
renderToReadableStream: "tachyon-dom/server/stream",
|
|
24
24
|
validateFormData: "tachyon-dom",
|
|
25
25
|
};
|
|
26
26
|
const autoImportPattern = new RegExp(`\\b(?:${Object.keys(autoImports)
|
|
@@ -257,6 +257,7 @@ const scopeName = (usesStore) => (usesStore ? "state" : "scope");
|
|
|
257
257
|
const runtimeNames = {
|
|
258
258
|
bindControl: "__tachyonBindControl",
|
|
259
259
|
createStore: "__tachyonCreateStore",
|
|
260
|
+
createRoot: "__tachyonCreateRoot",
|
|
260
261
|
delegate: "__tachyonDelegate",
|
|
261
262
|
effect: "__tachyonEffect",
|
|
262
263
|
elementAt: "__tachyonElementAt",
|
|
@@ -333,8 +334,8 @@ export const generateClientModule = (template, options = {}) => {
|
|
|
333
334
|
? `import { mountConditional as ${runtimeNames.mountConditional}, nodeAt as ${runtimeNames.nodeAt} } from "tachyon-dom/runtime/conditional";`
|
|
334
335
|
: `import { mountConditional as ${runtimeNames.mountConditional} } from "tachyon-dom/runtime/conditional";`);
|
|
335
336
|
}
|
|
336
|
-
if (needsSignal) {
|
|
337
|
-
lines.push(`import { effect as ${runtimeNames.effect}, read as ${runtimeNames.read} } from "tachyon-dom/runtime/signal";`);
|
|
337
|
+
if (needsSignal || hasDefaultScope) {
|
|
338
|
+
lines.push(`import { ${hasDefaultScope ? `createRoot as ${runtimeNames.createRoot}, ` : ""}${needsSignal ? `effect as ${runtimeNames.effect}, read as ${runtimeNames.read}` : ""} } from "tachyon-dom/runtime/signal";`);
|
|
338
339
|
}
|
|
339
340
|
if (needsStore) {
|
|
340
341
|
lines.push(`import { createStore as ${runtimeNames.createStore} } from "tachyon-dom/runtime/store";`);
|
|
@@ -348,7 +349,9 @@ export const generateClientModule = (template, options = {}) => {
|
|
|
348
349
|
lines.push(` return localScope && typeof localScope === "object" ? { ...localScope, ...inputScope } : inputScope;`);
|
|
349
350
|
lines.push(`};`);
|
|
350
351
|
}
|
|
351
|
-
lines.push(hasDefaultScope
|
|
352
|
+
lines.push(hasDefaultScope
|
|
353
|
+
? `export const bind = (root, inputScope = {}) => ${runtimeNames.createRoot}((__tachyonDisposeRoot) => {`
|
|
354
|
+
: `export const bind = (root, scope) => {`);
|
|
352
355
|
if (hasDefaultScope) {
|
|
353
356
|
lines.push(` const scope = __tachyonCreateScope(inputScope);`);
|
|
354
357
|
}
|
|
@@ -358,7 +361,7 @@ export const generateClientModule = (template, options = {}) => {
|
|
|
358
361
|
.join(", ");
|
|
359
362
|
lines.push(` const state = ${runtimeNames.createStore}({ ...scope, ${fields} });`);
|
|
360
363
|
}
|
|
361
|
-
if (reactive || needsEvent || needsModel) {
|
|
364
|
+
if (reactive || needsEvent || needsModel || hasDefaultScope) {
|
|
362
365
|
lines.push(` const cleanups = [];`);
|
|
363
366
|
}
|
|
364
367
|
let listIndex = 0;
|
|
@@ -438,12 +441,26 @@ export const generateClientModule = (template, options = {}) => {
|
|
|
438
441
|
lines.push(emitConditionalBinding(binding, reactive, sourceName, conditionalIndex++, targetName));
|
|
439
442
|
}
|
|
440
443
|
}
|
|
441
|
-
if (reactive || needsEvent || needsModel) {
|
|
444
|
+
if (reactive || needsEvent || needsModel || hasDefaultScope) {
|
|
442
445
|
lines.push(` return () => {`);
|
|
443
|
-
lines.push(`
|
|
446
|
+
lines.push(` let __tachyonCleanupError;`);
|
|
447
|
+
lines.push(` let __tachyonCleanupFailed = false;`);
|
|
448
|
+
lines.push(` for (const cleanup of cleanups) {`);
|
|
449
|
+
lines.push(` try { cleanup(); } catch (error) {`);
|
|
450
|
+
lines.push(` if (!__tachyonCleanupFailed) __tachyonCleanupError = error;`);
|
|
451
|
+
lines.push(` __tachyonCleanupFailed = true;`);
|
|
452
|
+
lines.push(` }`);
|
|
453
|
+
lines.push(` }`);
|
|
454
|
+
if (hasDefaultScope) {
|
|
455
|
+
lines.push(` try { __tachyonDisposeRoot(); } catch (error) {`);
|
|
456
|
+
lines.push(` if (!__tachyonCleanupFailed) __tachyonCleanupError = error;`);
|
|
457
|
+
lines.push(` __tachyonCleanupFailed = true;`);
|
|
458
|
+
lines.push(` }`);
|
|
459
|
+
}
|
|
460
|
+
lines.push(` if (__tachyonCleanupFailed) throw __tachyonCleanupError;`);
|
|
444
461
|
lines.push(` };`);
|
|
445
462
|
}
|
|
446
|
-
lines.push(`};`);
|
|
463
|
+
lines.push(hasDefaultScope ? `});` : `};`);
|
|
447
464
|
const code = `${lines.join("\n")}\n`;
|
|
448
465
|
const nextCache = cachedByOptions ?? new Map();
|
|
449
466
|
nextCache.set(cacheKey, code);
|
package/dist/compiler.d.ts
CHANGED
package/dist/compiler.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
import { err, ok } from "./result.js";
|
|
2
|
-
export * from "./app.js";
|
|
3
|
-
export { compileTachyonSfc } from "./compiler/sfc.js";
|
|
4
|
-
export { createI18n, localeMiddleware } from "./i18n.js";
|
|
5
|
-
export { generateClientModule, generateServerStreamModule, renderServerTemplate } from "./compiler/index.js";
|
|
6
2
|
export { enhanceForm, validateFormData } from "./runtime/form.js";
|
|
7
3
|
export { cleanupEnhancements, createEnhancementRegistry, enhance, registerEnhancement } from "./runtime/enhancement.js";
|
|
8
4
|
export { createClientRouter } from "./runtime/router.js";
|
|
9
5
|
export { createErrorBoundary } from "./runtime/error-boundary.js";
|
|
10
6
|
export { createKeyedRows } from "./runtime/keyed-rows.js";
|
|
11
|
-
export { batch, catchError, createMemo, createResource, createSignal, effect, untrack } from "./runtime/signal.js";
|
|
7
|
+
export { batch, catchError, createMemo, createResource, createRoot, createSignal, effect, onCleanup, untrack, } from "./runtime/signal.js";
|
|
12
8
|
export { createStore } from "./runtime/store.js";
|
|
13
9
|
export { readTextStreamChunks } from "./runtime/stream-client.js";
|
|
14
|
-
export { attr, booleanAttr, classList, html as serverHtml, join, rawHtml } from "./server/html.js";
|
|
15
|
-
export { formAction, formField, formState, preserveFormValues, redirectResponse } from "./server/form-action.js";
|
|
16
|
-
export { renderToReadableStream } from "./server/stream.js";
|
|
17
10
|
export { err, ok };
|
|
18
11
|
export { textAt } from "./runtime/text.js";
|
|
19
12
|
export type { KeyedRows, KeyedRowsOptions } from "./runtime/keyed-rows.js";
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
import { err, ok } from "./result.js";
|
|
2
|
-
export * from "./app.js";
|
|
3
|
-
export { compileTachyonSfc } from "./compiler/sfc.js";
|
|
4
|
-
export { createI18n, localeMiddleware } from "./i18n.js";
|
|
5
|
-
export { generateClientModule, generateServerStreamModule, renderServerTemplate } from "./compiler/index.js";
|
|
6
2
|
export { enhanceForm, validateFormData } from "./runtime/form.js";
|
|
7
3
|
export { cleanupEnhancements, createEnhancementRegistry, enhance, registerEnhancement } from "./runtime/enhancement.js";
|
|
8
4
|
export { createClientRouter } from "./runtime/router.js";
|
|
9
5
|
export { createErrorBoundary } from "./runtime/error-boundary.js";
|
|
10
6
|
export { createKeyedRows } from "./runtime/keyed-rows.js";
|
|
11
|
-
export { batch, catchError, createMemo, createResource, createSignal, effect, untrack } from "./runtime/signal.js";
|
|
7
|
+
export { batch, catchError, createMemo, createResource, createRoot, createSignal, effect, onCleanup, untrack, } from "./runtime/signal.js";
|
|
12
8
|
export { createStore } from "./runtime/store.js";
|
|
13
9
|
export { readTextStreamChunks } from "./runtime/stream-client.js";
|
|
14
|
-
export { attr, booleanAttr, classList, html as serverHtml, join, rawHtml } from "./server/html.js";
|
|
15
|
-
export { formAction, formField, formState, preserveFormValues, redirectResponse } from "./server/form-action.js";
|
|
16
|
-
export { renderToReadableStream } from "./server/stream.js";
|
|
17
10
|
export { err, ok };
|
|
18
11
|
export { textAt } from "./runtime/text.js";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { setClassPresence } from "./class.js";
|
|
2
2
|
import { setAttributeValue, setRef, setStyleValue } from "./attr.js";
|
|
3
3
|
import { delegate } from "./event.js";
|
|
4
4
|
import { bindControl, setControlValue } from "./form.js";
|
|
5
5
|
import { mountKeyedList } from "./list.js";
|
|
6
|
-
import { setText
|
|
6
|
+
import { setText } from "./text.js";
|
|
7
7
|
const states = new WeakMap();
|
|
8
8
|
export const nodeAt = (root, path) => {
|
|
9
9
|
let current = root;
|
|
@@ -60,6 +60,12 @@ const createNodes = (templateHtml) => {
|
|
|
60
60
|
template.innerHTML = templateHtml;
|
|
61
61
|
return Array.from(template.content.childNodes).map((node) => node.cloneNode(true));
|
|
62
62
|
};
|
|
63
|
+
const nodeAtState = (state, path) => {
|
|
64
|
+
if (state.nodes.length <= 1)
|
|
65
|
+
return nodeAt(state.nodes[0], path);
|
|
66
|
+
const [firstIndex, ...rest] = path;
|
|
67
|
+
return nodeAt(state.nodes[firstIndex ?? 0], rest);
|
|
68
|
+
};
|
|
63
69
|
const bindNodes = (anchor, state, scope, options) => {
|
|
64
70
|
state.scope = scope;
|
|
65
71
|
const firstElement = state.nodes.find((node) => node instanceof Element);
|
|
@@ -68,43 +74,49 @@ const bindNodes = (anchor, state, scope, options) => {
|
|
|
68
74
|
}
|
|
69
75
|
for (const binding of options.bindings) {
|
|
70
76
|
if (binding.kind === "text") {
|
|
71
|
-
setText(
|
|
77
|
+
setText(nodeAtState(state, binding.path), readBinding(scope, binding));
|
|
72
78
|
}
|
|
73
79
|
else if (binding.kind === "class") {
|
|
74
|
-
setClassPresence(
|
|
80
|
+
setClassPresence(nodeAtState(state, binding.path), binding.className, readBinding(scope, binding));
|
|
75
81
|
}
|
|
76
82
|
else if (binding.kind === "attr") {
|
|
77
|
-
setAttributeValue(
|
|
83
|
+
setAttributeValue(nodeAtState(state, binding.path), binding.name, readBinding(scope, binding));
|
|
78
84
|
}
|
|
79
85
|
else if (binding.kind === "style") {
|
|
80
|
-
setStyleValue(
|
|
86
|
+
setStyleValue(nodeAtState(state, binding.path), binding.name, readBinding(scope, binding));
|
|
81
87
|
}
|
|
82
88
|
else if (binding.kind === "ref") {
|
|
83
|
-
setRef(scope, binding.expression,
|
|
89
|
+
setRef(scope, binding.expression, nodeAtState(state, binding.path));
|
|
84
90
|
}
|
|
85
91
|
else if (binding.kind === "model") {
|
|
86
|
-
setControlValue(
|
|
92
|
+
setControlValue(nodeAtState(state, binding.path), binding.property, readBinding(scope, binding));
|
|
87
93
|
}
|
|
88
94
|
else if (binding.kind === "list") {
|
|
89
|
-
|
|
95
|
+
const container = nodeAtState(state, binding.path);
|
|
96
|
+
if (!(container instanceof Element))
|
|
97
|
+
continue;
|
|
98
|
+
mountKeyedList(container, [], readExpression(scope, binding.each, binding.read), { ...binding, scope });
|
|
90
99
|
}
|
|
91
100
|
else if (binding.kind === "if") {
|
|
92
|
-
mountConditional(
|
|
101
|
+
mountConditional(nodeAtState(state, binding.path), [], readExpression(scope, binding.test, binding.read), scope, binding);
|
|
93
102
|
}
|
|
94
103
|
}
|
|
95
104
|
if (state.cleanups.length === 0) {
|
|
96
105
|
for (const binding of options.bindings) {
|
|
97
106
|
if (binding.kind === "event") {
|
|
107
|
+
const target = nodeAtState(state, binding.path);
|
|
108
|
+
if (!(target instanceof Element))
|
|
109
|
+
continue;
|
|
98
110
|
const listener = (event) => {
|
|
99
111
|
const handler = readEvent(state.scope, binding);
|
|
100
112
|
if (typeof handler === "function") {
|
|
101
113
|
handler(event);
|
|
102
114
|
}
|
|
103
115
|
};
|
|
104
|
-
state.cleanups.push(delegate(
|
|
116
|
+
state.cleanups.push(delegate(target, binding.eventName, [], listener));
|
|
105
117
|
}
|
|
106
118
|
else if (binding.kind === "model") {
|
|
107
|
-
const element =
|
|
119
|
+
const element = nodeAtState(state, binding.path);
|
|
108
120
|
state.cleanups.push(bindControl(element, binding.property, () => readBinding(scope, binding), (value) => writeBinding(scope, binding, value)));
|
|
109
121
|
}
|
|
110
122
|
}
|
package/dist/runtime/list.js
CHANGED
|
@@ -171,12 +171,15 @@ const applyRowBinding = (record, scope, options, binding, index) => {
|
|
|
171
171
|
else if (binding.kind === "list") {
|
|
172
172
|
const eachBinding = binding.read ? { expression: binding.each, read: binding.read } : { expression: binding.each };
|
|
173
173
|
const value = readBinding(scope, eachBinding);
|
|
174
|
-
|
|
174
|
+
const container = nodeAtRecord(record, binding.path);
|
|
175
|
+
if (container instanceof Element) {
|
|
176
|
+
mountKeyedList(container, [], value, { ...binding, scope });
|
|
177
|
+
}
|
|
175
178
|
}
|
|
176
179
|
else if (binding.kind === "if") {
|
|
177
180
|
const testBinding = binding.read ? { expression: binding.test, read: binding.read } : { expression: binding.test };
|
|
178
181
|
const value = readBinding(scope, testBinding);
|
|
179
|
-
mountConditional(record
|
|
182
|
+
mountConditional(nodeAtRecord(record, binding.path), [], value, scope, binding);
|
|
180
183
|
}
|
|
181
184
|
};
|
|
182
185
|
const bindRowBindings = (record, options) => {
|
package/dist/runtime/signal.d.ts
CHANGED
package/dist/runtime/signal.js
CHANGED
|
@@ -1,9 +1,52 @@
|
|
|
1
1
|
const signalBrand = Symbol("tachyon.signal");
|
|
2
2
|
let activeEffect;
|
|
3
|
+
let currentOwner;
|
|
3
4
|
let batchDepth = 0;
|
|
4
5
|
let flushing = false;
|
|
5
6
|
const pendingComputedEffects = new Set();
|
|
6
7
|
const pendingEffects = new Set();
|
|
8
|
+
export const onCleanup = (cleanup) => {
|
|
9
|
+
if (currentOwner && !currentOwner.disposed) {
|
|
10
|
+
currentOwner.cleanups.push(cleanup);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export const createRoot = (fn) => {
|
|
14
|
+
const parent = currentOwner;
|
|
15
|
+
const owner = { disposed: false, cleanups: [] };
|
|
16
|
+
const dispose = () => {
|
|
17
|
+
if (owner.disposed)
|
|
18
|
+
return;
|
|
19
|
+
owner.disposed = true;
|
|
20
|
+
let firstError;
|
|
21
|
+
let failed = false;
|
|
22
|
+
for (let index = owner.cleanups.length - 1; index >= 0; index--) {
|
|
23
|
+
try {
|
|
24
|
+
owner.cleanups[index]?.();
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (!failed)
|
|
28
|
+
firstError = error;
|
|
29
|
+
failed = true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
owner.cleanups.length = 0;
|
|
33
|
+
if (failed)
|
|
34
|
+
throw firstError;
|
|
35
|
+
};
|
|
36
|
+
if (parent && !parent.disposed)
|
|
37
|
+
parent.cleanups.push(dispose);
|
|
38
|
+
currentOwner = owner;
|
|
39
|
+
try {
|
|
40
|
+
return fn(dispose);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
dispose();
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
currentOwner = parent;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
7
50
|
const cleanup = (runner) => {
|
|
8
51
|
for (const child of Array.from(runner.children)) {
|
|
9
52
|
disposeRunner(child);
|
|
@@ -182,7 +225,9 @@ const createEffect = (fn, computed) => {
|
|
|
182
225
|
};
|
|
183
226
|
parent?.children.add(runner);
|
|
184
227
|
runner.run();
|
|
185
|
-
|
|
228
|
+
const dispose = () => disposeRunner(runner);
|
|
229
|
+
onCleanup(dispose);
|
|
230
|
+
return dispose;
|
|
186
231
|
};
|
|
187
232
|
export const effect = (fn) => createEffect(fn, false);
|
|
188
233
|
export const catchError = (fn, onError) => effect(() => {
|
|
@@ -255,7 +300,7 @@ export const createResource = (source, fetcher) => {
|
|
|
255
300
|
else {
|
|
256
301
|
void run();
|
|
257
302
|
}
|
|
258
|
-
|
|
303
|
+
const resource = {
|
|
259
304
|
data,
|
|
260
305
|
error,
|
|
261
306
|
loading,
|
|
@@ -272,4 +317,6 @@ export const createResource = (source, fetcher) => {
|
|
|
272
317
|
loading.set(false);
|
|
273
318
|
},
|
|
274
319
|
};
|
|
320
|
+
onCleanup(resource.dispose);
|
|
321
|
+
return resource;
|
|
275
322
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tachyon-dom",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A small TypeScript UI runtime and HTML-first compiler.",
|
|
6
6
|
"keywords": [
|
|
@@ -205,6 +205,7 @@
|
|
|
205
205
|
"bench:html-minification": "tsx benchmark/html-minification.ts",
|
|
206
206
|
"bench:local:stable": "tsx benchmark/local-compare/run-local-compare.ts --iterations 30 --warmup 5",
|
|
207
207
|
"bench:local:trace": "tsx benchmark/local-compare/run-local-compare.ts --serve-mode dev --iterations 1 --warmup 1 --trace-implementation tachyon-dom --trace-scenario partialUpdate",
|
|
208
|
+
"bench:summary": "tsx benchmark/summary/cli.ts",
|
|
208
209
|
"bench:web-framework": "tsx benchmark/web-framework/run-web-framework-benchmark.ts",
|
|
209
210
|
"bench:web-framework:smoke": "tsx benchmark/web-framework/run-web-framework-benchmark.ts --smoke",
|
|
210
211
|
"example:router": "tsx examples/router.ts",
|
|
@@ -219,6 +220,8 @@
|
|
|
219
220
|
"build:create-package": "pnpm --dir packages/create-tachyon-dom build",
|
|
220
221
|
"check:exports": "publint --strict && attw --pack --no-emoji --profile esm-only",
|
|
221
222
|
"check:size": "size-limit",
|
|
223
|
+
"check:browser-entry": "node scripts/verify-browser-entry.mjs",
|
|
224
|
+
"check:quick-example-size": "node scripts/verify-quick-example-bundle.mjs",
|
|
222
225
|
"verify:package": "node scripts/verify-package-artifacts.mjs",
|
|
223
226
|
"verify:release": "node scripts/release-contract.mjs --verify-artifacts release-artifacts",
|
|
224
227
|
"prepare:release": "node scripts/release-contract.mjs",
|