toiljs 0.0.59 → 0.0.61
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/.github/workflows/ci.yml +31 -0
- package/CHANGELOG.md +15 -0
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +311 -118
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/index.d.ts +1 -1
- package/build/client/index.js +1 -1
- package/build/client/routing/mount.js +12 -1
- package/build/client/ssr/markers.d.ts +1 -0
- package/build/client/ssr/markers.js +3 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/config.d.ts +21 -0
- package/build/compiler/config.js +35 -0
- package/build/compiler/docs.d.ts +2 -1
- package/build/compiler/docs.js +33 -304
- package/build/compiler/index.d.ts +13 -0
- package/build/compiler/index.js +113 -21
- package/build/compiler/template-build.d.ts +21 -1
- package/build/compiler/template-build.js +110 -26
- package/build/compiler/toil-docs.generated.d.ts +1 -0
- package/build/compiler/toil-docs.generated.js +20 -0
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/daemon/catalog.d.ts +26 -0
- package/build/devserver/daemon/catalog.js +48 -0
- package/build/devserver/daemon/cron.d.ts +4 -0
- package/build/devserver/daemon/cron.js +50 -0
- package/build/devserver/daemon/host.d.ts +37 -0
- package/build/devserver/daemon/host.js +94 -0
- package/build/devserver/daemon/index.d.ts +34 -0
- package/build/devserver/daemon/index.js +241 -0
- package/build/devserver/db/catalog.d.ts +2 -0
- package/build/devserver/db/catalog.js +80 -0
- package/build/devserver/db/database.d.ts +80 -0
- package/build/devserver/db/database.js +1032 -0
- package/build/devserver/db/index.d.ts +3 -0
- package/build/devserver/db/index.js +3 -0
- package/build/devserver/db/routeKinds.d.ts +8 -0
- package/build/devserver/db/routeKinds.js +139 -0
- package/build/devserver/db/types.d.ts +121 -0
- package/build/devserver/db/types.js +52 -0
- package/build/devserver/email/index.js +1 -1
- package/build/devserver/index.d.ts +19 -24
- package/build/devserver/index.js +11 -165
- package/build/devserver/mstore/store.d.ts +18 -0
- package/build/devserver/mstore/store.js +82 -0
- package/build/devserver/{host.d.ts → runtime/host.d.ts} +7 -1
- package/build/devserver/{host.js → runtime/host.js} +51 -7
- package/build/devserver/{module.d.ts → runtime/module.d.ts} +2 -1
- package/build/devserver/{module.js → runtime/module.js} +34 -1
- package/build/devserver/server.d.ts +23 -0
- package/build/devserver/server.js +223 -0
- package/build/devserver/ssr.d.ts +25 -0
- package/build/devserver/ssr.js +114 -0
- package/build/devserver/wasm/sections.d.ts +2 -0
- package/build/devserver/wasm/sections.js +42 -0
- package/build/devserver/wasm/surface.d.ts +18 -0
- package/build/devserver/wasm/surface.js +41 -0
- package/docs/README.md +4 -4
- package/docs/auth-todo.md +6 -6
- package/docs/caching.md +5 -5
- package/docs/cli.md +15 -0
- package/docs/client.md +40 -0
- package/docs/crypto.md +4 -4
- package/docs/data.md +6 -6
- package/docs/email.md +28 -28
- package/docs/environment.md +10 -10
- package/docs/index.md +26 -0
- package/docs/ratelimit.md +10 -10
- package/docs/routing.md +2 -2
- package/docs/server.md +61 -0
- package/docs/ssr.md +561 -113
- package/docs/styling.md +22 -0
- package/docs/time.md +3 -3
- package/eslint.config.js +10 -1
- package/examples/basic/client/components/Header.tsx +3 -0
- package/examples/basic/client/routes/features/actions.tsx +0 -2
- package/examples/basic/client/routes/hello.tsx +89 -19
- package/examples/basic/client/styles/main.css +48 -0
- package/examples/basic/server/SsrHelloRender.ts +97 -0
- package/examples/basic/server/main.ts +5 -0
- package/examples/basic/server/migrations/GuestEntry.migration.ts +39 -0
- package/examples/basic/server/streams/Echo.ts +49 -0
- package/package.json +12 -10
- package/scripts/gen-toil-docs.mjs +96 -0
- package/server/runtime/time.ts +3 -3
- package/src/cli/create.ts +40 -3
- package/src/cli/db.ts +158 -0
- package/src/cli/diagnostics.ts +19 -0
- package/src/cli/doctor.ts +20 -0
- package/src/cli/index.ts +10 -0
- package/src/cli/update.ts +58 -0
- package/src/client/index.ts +1 -1
- package/src/client/routing/mount.tsx +18 -2
- package/src/client/ssr/markers.tsx +22 -0
- package/src/compiler/config.ts +88 -2
- package/src/compiler/docs.ts +47 -308
- package/src/compiler/index.ts +236 -32
- package/src/compiler/ssr-codegen.ts +1 -1
- package/src/compiler/template-build.ts +247 -46
- package/src/compiler/toil-docs.generated.ts +26 -0
- package/src/devserver/daemon/catalog.ts +120 -0
- package/src/devserver/daemon/cron.ts +87 -0
- package/src/devserver/daemon/host.ts +224 -0
- package/src/devserver/daemon/index.ts +349 -0
- package/src/devserver/db/catalog.ts +108 -0
- package/src/devserver/db/database.ts +1633 -0
- package/src/devserver/db/index.ts +18 -0
- package/src/devserver/db/routeKinds.ts +147 -0
- package/src/devserver/db/types.ts +139 -0
- package/src/devserver/email/index.ts +1 -1
- package/src/devserver/index.ts +31 -287
- package/src/devserver/mstore/store.ts +121 -0
- package/src/devserver/{host.ts → runtime/host.ts} +98 -7
- package/src/devserver/{module.ts → runtime/module.ts} +47 -1
- package/src/devserver/server.ts +393 -0
- package/src/devserver/ssr.ts +166 -0
- package/src/devserver/wasm/sections.ts +59 -0
- package/src/devserver/wasm/surface.ts +88 -0
- package/test/daemon-build.test.ts +198 -0
- package/test/daemon-catalog.test.ts +265 -0
- package/test/daemon-emulation.test.ts +216 -0
- package/test/db.test.ts +0 -0
- package/test/devserver-database.test.ts +510 -14
- package/test/devserver-pqauth.test.ts +1 -1
- package/test/devserver-secrets.test.ts +5 -1
- package/test/doctor.test.ts +13 -0
- package/test/email-preview.test.ts +6 -1
- package/test/example-guestbook.test.ts +43 -1
- package/test/fixtures/daemon-app.ts +56 -0
- package/test/global-setup.ts +17 -0
- package/test/pqauth-e2e.test.ts +1 -1
- package/test/ssr-render.test.ts +94 -27
- package/test/ssr-template.test.tsx +44 -1
- package/vitest.config.ts +3 -0
- package/build/devserver/database.d.ts +0 -8
- package/build/devserver/database.js +0 -418
- package/src/devserver/database.ts +0 -618
- /package/build/devserver/{dotenv.d.ts → config/dotenv.d.ts} +0 -0
- /package/build/devserver/{dotenv.js → config/dotenv.js} +0 -0
- /package/build/devserver/{env.d.ts → config/env.d.ts} +0 -0
- /package/build/devserver/{env.js → config/env.js} +0 -0
- /package/build/devserver/{ratelimit.d.ts → config/ratelimit.d.ts} +0 -0
- /package/build/devserver/{ratelimit.js → config/ratelimit.js} +0 -0
- /package/build/devserver/{cache.d.ts → http/cache.d.ts} +0 -0
- /package/build/devserver/{cache.js → http/cache.js} +0 -0
- /package/build/devserver/{envelope.d.ts → http/envelope.d.ts} +0 -0
- /package/build/devserver/{envelope.js → http/envelope.js} +0 -0
- /package/build/devserver/{proxy.d.ts → http/proxy.d.ts} +0 -0
- /package/build/devserver/{proxy.js → http/proxy.js} +0 -0
- /package/build/devserver/{crypto.d.ts → runtime/crypto.d.ts} +0 -0
- /package/build/devserver/{crypto.js → runtime/crypto.js} +0 -0
- /package/src/devserver/{dotenv.ts → config/dotenv.ts} +0 -0
- /package/src/devserver/{env.ts → config/env.ts} +0 -0
- /package/src/devserver/{ratelimit.ts → config/ratelimit.ts} +0 -0
- /package/src/devserver/{cache.ts → http/cache.ts} +0 -0
- /package/src/devserver/{envelope.ts → http/envelope.ts} +0 -0
- /package/src/devserver/{proxy.ts → http/proxy.ts} +0 -0
- /package/src/devserver/{crypto.ts → runtime/crypto.ts} +0 -0
package/test/doctor.test.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
checkDevScripts,
|
|
7
7
|
checkDuplicatePatterns,
|
|
8
8
|
type CheckGroup,
|
|
9
|
+
checkMigrationsDir,
|
|
9
10
|
checkMountSlots,
|
|
10
11
|
checkNode,
|
|
11
12
|
checkPeer,
|
|
@@ -219,6 +220,18 @@ describe('checkAuthSecrets', () => {
|
|
|
219
220
|
});
|
|
220
221
|
});
|
|
221
222
|
|
|
223
|
+
describe('checkMigrationsDir', () => {
|
|
224
|
+
it('passes when the server/migrations/ folder exists', () => {
|
|
225
|
+
expect(checkMigrationsDir(true).status).toBe('pass');
|
|
226
|
+
});
|
|
227
|
+
it('warns (not fails) when missing, naming the convention and the update fix', () => {
|
|
228
|
+
const c = checkMigrationsDir(false);
|
|
229
|
+
expect(c.status).toBe('warn');
|
|
230
|
+
expect(c.detail).toContain('migration.ts');
|
|
231
|
+
expect(c.fix).toContain('toiljs update');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
222
235
|
describe('summarize', () => {
|
|
223
236
|
it('tallies pass/warn/fail across groups', () => {
|
|
224
237
|
const groups: CheckGroup[] = [
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
|
|
3
4
|
import { createServer } from 'vite';
|
|
@@ -14,7 +15,11 @@ import { createViteConfig } from '../src/compiler/vite';
|
|
|
14
15
|
|
|
15
16
|
const EXAMPLE = path.resolve(__dirname, '../examples/basic');
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
// Drives a Vite server over examples/basic; skip where the example's deps are
|
|
19
|
+
// not installed (e.g. CI without the example set up).
|
|
20
|
+
describe.skipIf(!existsSync(path.join(EXAMPLE, 'node_modules')))(
|
|
21
|
+
'email preview end-to-end (examples/basic)',
|
|
22
|
+
() => {
|
|
18
23
|
it('lists Welcome and inlines its emails/styles/email.css; client/* alias resolves', async () => {
|
|
19
24
|
const cfg = await loadConfig({ root: EXAMPLE });
|
|
20
25
|
const items = listEmails(cfg);
|
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
* request". Skips until the example server wasm is built (`npm run build:server`).
|
|
8
8
|
*/
|
|
9
9
|
import fs from 'node:fs';
|
|
10
|
+
import os from 'node:os';
|
|
10
11
|
import path from 'node:path';
|
|
11
12
|
import { fileURLToPath } from 'node:url';
|
|
12
13
|
|
|
13
14
|
import { describe, expect, it, beforeEach } from 'vitest';
|
|
14
15
|
|
|
15
16
|
import { WasmServerModule } from '../src/devserver/index.js';
|
|
16
|
-
import { __resetDbForTests } from '../src/devserver/
|
|
17
|
+
import { __resetDbForTests, configureDbPersistence } from '../src/devserver/db/index.js';
|
|
17
18
|
|
|
18
19
|
const EXAMPLE_WASM = path.resolve(
|
|
19
20
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
@@ -75,4 +76,45 @@ describe.skipIf(!haveWasm)('guestbook demo: ToilDB events + counter persist acro
|
|
|
75
76
|
// A read-only GET on yet another instance sees the same persisted state.
|
|
76
77
|
expect(json(list(load())).total).toBe('2');
|
|
77
78
|
});
|
|
79
|
+
|
|
80
|
+
// End-to-end proof that the `server/migrations/GuestEntry.migration.ts` demo
|
|
81
|
+
// actually RUNS: write an entry under the current shape, downgrade it on disk to
|
|
82
|
+
// the original pre-`at` `GuestEntryV1` layout (drop the trailing u64 + re-stamp
|
|
83
|
+
// with v1's schema_version), then `list()` and confirm the woven decoder ran the
|
|
84
|
+
// `@migrate` - the entry comes back with the new `at` field defaulted to 0.
|
|
85
|
+
it('migrates an on-disk pre-`at` entry on read (the GuestEntry.migration demo fires)', () => {
|
|
86
|
+
const GUEST_ENTRY_V1_VERSION = 631968986; // layoutHash({author:string, message:string})
|
|
87
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gb-mig-'));
|
|
88
|
+
const file = path.join(dir, 'devdata.json');
|
|
89
|
+
try {
|
|
90
|
+
// 1. sign under the CURRENT shape (author, message, at); persistence flushes it.
|
|
91
|
+
configureDbPersistence(file);
|
|
92
|
+
expect(sign(load(), 'Ada', 'from the old days').status).toBe(200);
|
|
93
|
+
|
|
94
|
+
// 2. downgrade that event on disk to the v1 shape: GuestEntry encodes
|
|
95
|
+
// author + message + at(u64), so dropping the trailing 8 bytes yields a
|
|
96
|
+
// valid GuestEntryV1; re-stamp it with v1's schema_version.
|
|
97
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
98
|
+
const snap: any = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
99
|
+
const evKey = Object.keys(snap.events)[0];
|
|
100
|
+
const buf = Buffer.from(snap.events[evKey][0].v, 'base64');
|
|
101
|
+
snap.events[evKey][0] = {
|
|
102
|
+
v: buf.subarray(0, buf.length - 8).toString('base64'),
|
|
103
|
+
sv: GUEST_ENTRY_V1_VERSION,
|
|
104
|
+
};
|
|
105
|
+
fs.writeFileSync(file, JSON.stringify(snap));
|
|
106
|
+
|
|
107
|
+
// 3. reload + list: the read surfaces v1's version, so the guest's woven
|
|
108
|
+
// decoder runs the @migrate, copying author/message and defaulting at=0.
|
|
109
|
+
__resetDbForTests();
|
|
110
|
+
configureDbPersistence(file);
|
|
111
|
+
const v = json(list(load()));
|
|
112
|
+
expect(v.entries.length).toBe(1);
|
|
113
|
+
expect(v.entries[0].author).toBe('Ada');
|
|
114
|
+
expect(v.entries[0].message).toBe('from the old days');
|
|
115
|
+
expect(String(v.entries[0].at)).toBe('0'); // the migrated-in field
|
|
116
|
+
} finally {
|
|
117
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
78
120
|
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// A minimal @daemon fixture for the dev daemon-emulation test. It declares the
|
|
2
|
+
// `daemon.*` / `mstore.*` host imports directly (so it needs no toiljs globals
|
|
3
|
+
// lib) and records its activity into the dev MemoryStore, which the test reads
|
|
4
|
+
// back through `devMemoryStore`. Compiled with `--targetMode cold` by the test.
|
|
5
|
+
//
|
|
6
|
+
// onStart() -> mstore.incr("started", 1) and stamps the lease epoch
|
|
7
|
+
// tick() @scheduled -> mstore.incr("tick:fast", 1) (1s interval)
|
|
8
|
+
// sixHourly() cron -> mstore.incr("tick:cron", 1) (0 */6 * * *)
|
|
9
|
+
|
|
10
|
+
// @ts-nocheck — this is AssemblyScript source compiled by toilscript, not TS.
|
|
11
|
+
|
|
12
|
+
@external("env", "mstore.incr")
|
|
13
|
+
declare function mstoreIncr(keyPtr: i32, keyLen: i32, delta: i64, ttlSecs: i32): i64;
|
|
14
|
+
|
|
15
|
+
@external("env", "daemon.is_leader")
|
|
16
|
+
declare function daemonIsLeader(): i32;
|
|
17
|
+
|
|
18
|
+
@external("env", "daemon.current_epoch")
|
|
19
|
+
declare function daemonCurrentEpoch(): i64;
|
|
20
|
+
|
|
21
|
+
@external("env", "daemon.task_count")
|
|
22
|
+
declare function daemonTaskCount(): i32;
|
|
23
|
+
|
|
24
|
+
// Bump the i64 counter stored at the (utf8) `key` by 1. The host reads the key
|
|
25
|
+
// bytes straight out of linear memory (handleless mstore, ttl in seconds).
|
|
26
|
+
function bump(key: string): void {
|
|
27
|
+
let bytes = String.UTF8.encode(key);
|
|
28
|
+
mstoreIncr(changetype<i32>(bytes), bytes.byteLength, 1, 0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@daemon
|
|
32
|
+
class Jobs {
|
|
33
|
+
onStart(): void {
|
|
34
|
+
// Prove leader=true and that the epoch import is callable; record both so
|
|
35
|
+
// the test can assert the stubs from outside.
|
|
36
|
+
bump("started");
|
|
37
|
+
if (daemonIsLeader() == 1) bump("leader");
|
|
38
|
+
let epoch = daemonCurrentEpoch();
|
|
39
|
+
if (epoch >= 0) bump("epoch:nonneg");
|
|
40
|
+
if (daemonTaskCount() == 2) bump("taskcount:2");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@scheduled("1s")
|
|
44
|
+
tick(): void {
|
|
45
|
+
bump("tick:fast");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@scheduled("0 */6 * * *")
|
|
49
|
+
sixHourly(): void {
|
|
50
|
+
bump("tick:cron");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function probe(): i32 {
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Vitest global setup: generate `src/compiler/toil-docs.generated.ts` before the
|
|
7
|
+
* suite runs. `src/compiler/docs.ts` imports that module, but it is gitignored
|
|
8
|
+
* and only (re)created by the build's `gen:docs` step, so a fresh checkout that
|
|
9
|
+
* runs the tests without a prior build would otherwise fail to resolve it
|
|
10
|
+
* ("Cannot find module './toil-docs.generated.js'"). Running the same generator
|
|
11
|
+
* the npm scripts use keeps the tests self-contained. Runs once per vitest
|
|
12
|
+
* invocation, so it also covers `npx vitest` / watch mode.
|
|
13
|
+
*/
|
|
14
|
+
export default function setup(): void {
|
|
15
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
|
+
execFileSync('node', ['scripts/gen-toil-docs.mjs'], { cwd: root, stdio: 'ignore' });
|
|
17
|
+
}
|
package/test/pqauth-e2e.test.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { describe, expect, it, beforeEach, afterEach } from 'vitest';
|
|
|
20
20
|
import { ristretto255_oprf } from '@noble/curves/ed25519.js';
|
|
21
21
|
|
|
22
22
|
import { WasmServerModule } from '../src/devserver/index.js';
|
|
23
|
-
import { __resetDbForTests } from '../src/devserver/
|
|
23
|
+
import { __resetDbForTests } from '../src/devserver/db/index.js';
|
|
24
24
|
import { Auth } from '../src/client/auth.js';
|
|
25
25
|
import { DataReader, DataWriter } from '../src/io/codec.js';
|
|
26
26
|
|
package/test/ssr-render.test.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* End-to-end guest render: drive the real ToilScript-compiled `render` export
|
|
3
|
-
* of the
|
|
4
|
-
* produces exactly the bytes the Rust host's `decode_values`/`assemble`
|
|
5
|
-
* (the same wire format proven on the host side in
|
|
3
|
+
* of the basic example's server wasm, decode its values envelope, and confirm
|
|
4
|
+
* the guest produces exactly the bytes the Rust host's `decode_values`/`assemble`
|
|
5
|
+
* expects (the same wire format proven on the host side in
|
|
6
6
|
* `toil-backend/src/host/template/assemble.rs`). Closing the loop with the
|
|
7
7
|
* golden byte-identity test (`ssr-template.test.tsx`), this proves the full
|
|
8
8
|
* chain: guest values -> splice -> React-identical HTML.
|
|
9
|
+
*
|
|
10
|
+
* SSR is part of the normal (single-wasm) build now: the example's `/hello`
|
|
11
|
+
* route opts in with `export const ssr = true`, the build extracts its template
|
|
12
|
+
* into `build/client/_ssr/hello.{tmpl,slots,slots.ts}`, and the server
|
|
13
|
+
* `render` (`examples/basic/server/SsrHelloRender.ts`) fills the holes. We drive
|
|
14
|
+
* the same `build/server/release.wasm` the dev server and edge run, and pin the
|
|
15
|
+
* expected coherence hash to the generated `templates.json` so the guest and the
|
|
16
|
+
* deployed template are proven to agree.
|
|
9
17
|
*/
|
|
10
18
|
import fs from 'node:fs';
|
|
11
19
|
import path from 'node:path';
|
|
@@ -16,10 +24,14 @@ import { describe, expect, it } from 'vitest';
|
|
|
16
24
|
import { WasmServerModule } from '../src/devserver/index.js';
|
|
17
25
|
import { spliceTemplate } from '../src/compiler/template.js';
|
|
18
26
|
|
|
19
|
-
const
|
|
27
|
+
const EXAMPLE = path.resolve(
|
|
20
28
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
21
|
-
'../examples/basic
|
|
29
|
+
'../examples/basic',
|
|
22
30
|
);
|
|
31
|
+
/** Single-wasm SSR: the server `render` lives in the normal server wasm. */
|
|
32
|
+
const SERVER_WASM = path.join(EXAMPLE, 'build/server/release.wasm');
|
|
33
|
+
const SSR_DIR = path.join(EXAMPLE, 'build/client/_ssr');
|
|
34
|
+
const TEMPLATES = path.join(SSR_DIR, 'templates.json');
|
|
23
35
|
|
|
24
36
|
interface DecodedSlot {
|
|
25
37
|
slotId: number;
|
|
@@ -72,9 +84,25 @@ function decodeValues(buf: Uint8Array): DecodedValues {
|
|
|
72
84
|
return { status, hash, headers, slots };
|
|
73
85
|
}
|
|
74
86
|
|
|
75
|
-
|
|
87
|
+
/** The hash the build pinned for the `/hello` template (deploy-skew guard). */
|
|
88
|
+
function helloTemplateHash(): Buffer {
|
|
89
|
+
const index = JSON.parse(fs.readFileSync(TEMPLATES, 'utf8')) as {
|
|
90
|
+
route: string;
|
|
91
|
+
name: string;
|
|
92
|
+
hash: string;
|
|
93
|
+
}[];
|
|
94
|
+
const hello = index.find((t) => t.route === '/hello');
|
|
95
|
+
if (!hello) throw new Error('no /hello template in templates.json');
|
|
96
|
+
return Buffer.from(hello.hash, 'hex');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Skip cleanly when the example has not been built (no toolchain in CI): the
|
|
100
|
+
// build is what produces both the wasm and the template artifacts.
|
|
101
|
+
const built = fs.existsSync(SERVER_WASM) && fs.existsSync(TEMPLATES);
|
|
102
|
+
|
|
103
|
+
describe.skipIf(!built)('edge SSR guest render (real single-wasm build)', () => {
|
|
76
104
|
const load = (): WasmServerModule => {
|
|
77
|
-
const m = new WasmServerModule(
|
|
105
|
+
const m = new WasmServerModule(SERVER_WASM);
|
|
78
106
|
m.refresh();
|
|
79
107
|
return m;
|
|
80
108
|
};
|
|
@@ -86,36 +114,75 @@ describe.skipIf(!fs.existsSync(SSR_WASM))('edge SSR guest render (real wasm)', (
|
|
|
86
114
|
body: new Uint8Array(0),
|
|
87
115
|
});
|
|
88
116
|
|
|
89
|
-
it('produces a values envelope with the
|
|
117
|
+
it('produces a values envelope with the deployed template hash and escaped holes', () => {
|
|
90
118
|
const d = decodeValues(render(load(), '/hello'));
|
|
91
119
|
expect(d.status).toBe(200);
|
|
92
|
-
// The HASH
|
|
93
|
-
|
|
120
|
+
// The guest's compiled-in HASH must equal the deployed template's hash,
|
|
121
|
+
// or the host rejects the response as a deploy skew.
|
|
122
|
+
expect(d.hash.equals(helloTemplateHash())).toBe(true);
|
|
94
123
|
|
|
124
|
+
// Top-level slots in document order: name (text), blurb (raw), services (repeat).
|
|
95
125
|
expect(d.slots.map((s) => `${s.slotId}:${s.kind}`)).toEqual([
|
|
96
|
-
'0:0', //
|
|
97
|
-
'1:
|
|
126
|
+
'0:0', // name, text
|
|
127
|
+
'1:1', // blurb, raw
|
|
128
|
+
'2:3', // services, repeat
|
|
98
129
|
]);
|
|
99
|
-
// Text hole:
|
|
100
|
-
expect(d.slots[0].value.toString('utf8')).toBe('world
|
|
101
|
-
//
|
|
130
|
+
// Text hole: the default greeting target.
|
|
131
|
+
expect(d.slots[0].value.toString('utf8')).toBe('world');
|
|
132
|
+
// Raw hole: inserted verbatim (NOT escaped).
|
|
102
133
|
expect(d.slots[1].value.toString('utf8')).toBe(
|
|
103
|
-
'<
|
|
134
|
+
'Rendered at the <strong>edge</strong> from a tiny values envelope.',
|
|
104
135
|
);
|
|
136
|
+
// Repeat hole: three stamped rows, each nested hole React-escaped.
|
|
137
|
+
expect(d.slots[2].value.toString('utf8')).toBe(
|
|
138
|
+
'<li><strong>record</strong><span class="hello-region">us-east</span></li>' +
|
|
139
|
+
'<li><strong>unique</strong><span class="hello-region">eu-west</span></li>' +
|
|
140
|
+
'<li><strong>counter</strong><span class="hello-region">ap-south</span></li>',
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('escapes a text hole derived from the request (?name=)', () => {
|
|
145
|
+
const d = decodeValues(render(load(), '/hello?name=A<b>%26"x'));
|
|
146
|
+
expect(d.status).toBe(200);
|
|
147
|
+
// setText React-escapes: & -> &, <> -> entities, " -> ".
|
|
148
|
+
// (The query value arrives as the raw bytes after `name=`; only `%26`
|
|
149
|
+
// is a literal here, so the guest sees `A<b>&"x` after the `&` split is
|
|
150
|
+
// accounted for — assert the escaping shape rather than the exact bytes.)
|
|
151
|
+
const v = d.slots[0].value.toString('utf8');
|
|
152
|
+
expect(v).not.toContain('<b>');
|
|
153
|
+
expect(v).toContain('<');
|
|
105
154
|
});
|
|
106
155
|
|
|
107
|
-
it('splices into
|
|
156
|
+
it('splices into the real built template exactly (guest -> host -> HTML)', () => {
|
|
108
157
|
const d = decodeValues(render(load(), '/hello'));
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
]
|
|
117
|
-
|
|
118
|
-
|
|
158
|
+
const tmpl = fs.readFileSync(path.join(SSR_DIR, 'hello.tmpl'));
|
|
159
|
+
const slotsBin = fs.readFileSync(path.join(SSR_DIR, 'hello.slots'));
|
|
160
|
+
|
|
161
|
+
// Read the top-level slot offsets straight from the .slots manifest
|
|
162
|
+
// (header is 46 bytes; each entry is offset u32, id u16, kind u8, rsvd u8).
|
|
163
|
+
const nSlots = slotsBin.readUInt16LE(44);
|
|
164
|
+
const byId = new Map(d.slots.map((s) => [s.slotId, s.value]));
|
|
165
|
+
const inserts: { offset: number; value: Buffer }[] = [];
|
|
166
|
+
let o = 46;
|
|
167
|
+
for (let i = 0; i < nSlots; i++) {
|
|
168
|
+
const offset = slotsBin.readUInt32LE(o);
|
|
169
|
+
const id = slotsBin.readUInt16LE(o + 4);
|
|
170
|
+
inserts.push({ offset, value: Buffer.from(byId.get(id)!) });
|
|
171
|
+
o += 8;
|
|
172
|
+
}
|
|
173
|
+
const out = spliceTemplate(tmpl, inserts).toString('utf8');
|
|
174
|
+
|
|
175
|
+
// The spliced section is well-formed and carries every filled hole.
|
|
176
|
+
expect(out).toContain(
|
|
177
|
+
'<section class="hello"><h1>Hello, world!</h1>' +
|
|
178
|
+
'<p class="hello-blurb"><span>Rendered at the <strong>edge</strong> ' +
|
|
179
|
+
'from a tiny values envelope.</span></p>' +
|
|
180
|
+
'<h2>Service snapshot</h2>' +
|
|
181
|
+
'<ul class="hello-services">' +
|
|
182
|
+
'<li><strong>record</strong><span class="hello-region">us-east</span></li>' +
|
|
183
|
+
'<li><strong>unique</strong><span class="hello-region">eu-west</span></li>' +
|
|
184
|
+
'<li><strong>counter</strong><span class="hello-region">ap-south</span></li>' +
|
|
185
|
+
'</ul></section>',
|
|
119
186
|
);
|
|
120
187
|
});
|
|
121
188
|
|
|
@@ -20,7 +20,7 @@ import path from 'node:path';
|
|
|
20
20
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
21
21
|
import { describe, expect, it } from 'vitest';
|
|
22
22
|
|
|
23
|
-
import { Hole, Island, RawHtml, Repeat, __setSsrBuild } from '../src/client/ssr/markers';
|
|
23
|
+
import { Hole, Island, RawHtml, Repeat, attr, __setSsrBuild } from '../src/client/ssr/markers';
|
|
24
24
|
import { LoaderDataContext, useLoaderData } from '../src/client/routing/loader';
|
|
25
25
|
import {
|
|
26
26
|
extractRouteTemplate,
|
|
@@ -265,6 +265,49 @@ describe('ssr guest codegen (Slot enum + HASH)', () => {
|
|
|
265
265
|
});
|
|
266
266
|
});
|
|
267
267
|
|
|
268
|
+
describe('ssr attribute holes (attr())', () => {
|
|
269
|
+
it('is transparent in the browser and a sentinel under the build extractor', () => {
|
|
270
|
+
// Browser (default): passes the value through unchanged.
|
|
271
|
+
expect(attr('link', '/u/ada')).toBe('/u/ada');
|
|
272
|
+
// Build: emits a PUA sentinel token (start codepoint U+E000) carrying the id.
|
|
273
|
+
__setSsrBuild(true);
|
|
274
|
+
try {
|
|
275
|
+
const tok = attr('link', '/u/ada');
|
|
276
|
+
expect(tok).not.toBe('/u/ada');
|
|
277
|
+
expect(tok.charCodeAt(0)).toBe(0xe000);
|
|
278
|
+
expect(tok).toContain('link');
|
|
279
|
+
} finally {
|
|
280
|
+
__setSsrBuild(false);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('extracts an attr slot whose guest stamp reproduces React attribute output byte-for-byte', () => {
|
|
285
|
+
// Render with the attr() hole in an attribute position, in build mode.
|
|
286
|
+
__setSsrBuild(true);
|
|
287
|
+
let built: string;
|
|
288
|
+
try {
|
|
289
|
+
built = renderToStaticMarkup(<a href={attr('link', 'IGNORED_AT_BUILD')}>x</a>);
|
|
290
|
+
} finally {
|
|
291
|
+
__setSsrBuild(false);
|
|
292
|
+
}
|
|
293
|
+
const { tmpl, slots } = extractFromHtml(built);
|
|
294
|
+
expect(slots).toHaveLength(1);
|
|
295
|
+
expect(slots[0]).toMatchObject({ id: 'link', kind: 'attr' });
|
|
296
|
+
expect(kindByte(slots[0].kind)).toBe(2);
|
|
297
|
+
// The .tmpl carries the attribute with the hole stripped to an empty value.
|
|
298
|
+
expect(tmpl.toString('utf8')).toBe('<a href="">x</a>');
|
|
299
|
+
|
|
300
|
+
// Guest stamp: setAttr React-escapes (identical to text); splice at the offset.
|
|
301
|
+
const value = '/u/ada?q="a"&b<c>';
|
|
302
|
+
const stamped = spliceTemplate(tmpl, [
|
|
303
|
+
{ offset: slots[0].offset, value: Buffer.from(reactEscapeHtml(value), 'utf8') },
|
|
304
|
+
]).toString('utf8');
|
|
305
|
+
|
|
306
|
+
// Byte-identical to what React renders for the same attribute value (clean hydration).
|
|
307
|
+
expect(stamped).toBe(renderToStaticMarkup(<a href={value}>x</a>));
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
268
311
|
describe('ssr build orchestration', () => {
|
|
269
312
|
const SHELL =
|
|
270
313
|
'<!doctype html><html><head><title>t</title></head><body><div id="root"></div>' +
|
package/vitest.config.ts
CHANGED
|
@@ -6,6 +6,9 @@ export default defineConfig({
|
|
|
6
6
|
test: {
|
|
7
7
|
globals: true,
|
|
8
8
|
environment: 'node',
|
|
9
|
+
// Generate src/compiler/toil-docs.generated.ts (gitignored, imported by
|
|
10
|
+
// docs.ts) before the suite, so a fresh checkout can test without a build.
|
|
11
|
+
globalSetup: ['./test/global-setup.ts'],
|
|
9
12
|
include: ['test/**/*.test.ts', 'test/**/*.test.tsx', 'test/**/*.spec.ts'],
|
|
10
13
|
// test/assembly holds toilscript specs run by as-pect, not vitest.
|
|
11
14
|
exclude: [...configDefaults.exclude, 'test/assembly/**'],
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { MemoryRef } from './host.js';
|
|
2
|
-
export interface DbDevState {
|
|
3
|
-
handles: string[];
|
|
4
|
-
lastResult: Buffer | null;
|
|
5
|
-
}
|
|
6
|
-
export declare function freshDbState(): DbDevState;
|
|
7
|
-
export declare function buildDatabaseImports(ref: MemoryRef, db: DbDevState): Record<string, (...args: number[]) => number | bigint>;
|
|
8
|
-
export declare function __resetDbForTests(): void;
|