@astroscope/wormhole 1.1.0 → 2.0.0
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 +72 -116
- package/dist/define-BO-pYQc1.js +61 -0
- package/dist/define-BO-pYQc1.js.map +1 -0
- package/dist/index.browser.d.ts +9 -0
- package/dist/index.browser.d.ts.map +1 -0
- package/dist/index.browser.js +48 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.d.ts +35 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +166 -28
- package/dist/index.js.map +1 -1
- package/dist/key-BqTy1gM3.js +9 -0
- package/dist/key-BqTy1gM3.js.map +1 -0
- package/dist/react/index.d.ts +1 -2
- package/dist/react/index.d.ts.map +1 -1
- package/dist/scan-4GXufA9_.js +79 -0
- package/dist/scan-4GXufA9_.js.map +1 -0
- package/dist/server.d.ts +49 -15
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +154 -8
- package/dist/server.js.map +1 -1
- package/dist/types-okMd-V3s.d.ts +42 -0
- package/dist/types-okMd-V3s.d.ts.map +1 -0
- package/package.json +20 -16
- package/dist/types-Q2gaO_N_.d.ts +0 -31
- package/dist/types-Q2gaO_N_.d.ts.map +0 -1
- package/src/astro/WormholeScript.astro +0 -29
- package/src/astro/index.ts +0 -2
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# @astroscope/wormhole
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Share dynamic server data with React islands and client scripts — typed, reactive.
|
|
3
|
+
Share dynamic server data with React islands and client scripts — typed, streamed with the HTML, sliced per island.
|
|
6
4
|
|
|
7
5
|
## Why this library?
|
|
8
6
|
|
|
9
7
|
Astro recommends [nanostores](https://docs.astro.build/en/recipes/sharing-state/) for sharing state between islands, but nanostores are client-only — there's no built-in way to hydrate them with server data during SSR.
|
|
10
8
|
|
|
11
|
-
`@astroscope/wormhole` bridges this gap:
|
|
9
|
+
`@astroscope/wormhole` bridges this gap: define your wormholes in a registry, resolve their values per request in a single middleware, and read them anywhere — Astro frontmatter, React islands, `<script>` blocks — through one typed `wormholes` proxy. Client code imports nothing of yours: the data streams into the HTML exactly where it is needed.
|
|
10
|
+
|
|
11
|
+
**Delivery is sliced per island.** The build scans your client code to see which wormholes each component reads; at runtime every island receives exactly those, embedded in the HTML right before it. A wormhole no client code reads ships **zero bytes**.
|
|
12
12
|
|
|
13
13
|
**Typical use cases:**
|
|
14
14
|
|
|
@@ -20,9 +20,12 @@ Astro recommends [nanostores](https://docs.astro.build/en/recipes/sharing-state/
|
|
|
20
20
|
|
|
21
21
|
## Important notes
|
|
22
22
|
|
|
23
|
-
- **No secrets in wormholes.**
|
|
24
|
-
-
|
|
25
|
-
- **
|
|
23
|
+
- **No secrets in wormholes.** Wormhole data is serialized into inline `<script>` tags and sent to the browser. Never store tokens, API keys, credentials, or any sensitive data in a wormhole. (In dev, *all* open wormholes ship with every page; production slicing is an optimization, not a security boundary.)
|
|
24
|
+
- **Requires `@astroscope/node`** — per-island delivery uses its islands pipeline in production.
|
|
25
|
+
- **The registry is server-only.** Client code must never import `src/wormholes.ts` — it accesses values through the `wormholes` proxy instead. (Importing it in client code throws at once.)
|
|
26
|
+
- **`set()` is client-only.** Server values are request-scoped and come from the middleware.
|
|
27
|
+
- **Values are deeply readonly.** `get()`, `subscribe()` and `useWormhole()` expose the value as `DeepReadonly<T>` — in-place mutation would silently bypass subscribers. The only way to update is `set()` with a new value: `wormholes.counter.set({ ...wormholes.counter.get(), count: 1 })`.
|
|
28
|
+
- **Prerendered pages get no wormhole data** — values are per-request by definition.
|
|
26
29
|
|
|
27
30
|
## Examples
|
|
28
31
|
|
|
@@ -34,72 +37,68 @@ See the [demo/wormhole](../../demo/wormhole) directory for a working example.
|
|
|
34
37
|
npm install @astroscope/wormhole
|
|
35
38
|
```
|
|
36
39
|
|
|
40
|
+
```ts
|
|
41
|
+
// astro.config.ts
|
|
42
|
+
import node from '@astroscope/node';
|
|
43
|
+
import wormhole from '@astroscope/wormhole';
|
|
44
|
+
|
|
45
|
+
export default defineConfig({
|
|
46
|
+
output: 'server',
|
|
47
|
+
adapter: node(),
|
|
48
|
+
integrations: [wormhole()],
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The integration registers the build-time scanner and generates a type stub from your registry, so `wormholes.<name>` is fully typed everywhere.
|
|
53
|
+
|
|
37
54
|
## Usage
|
|
38
55
|
|
|
39
|
-
### 1. Define
|
|
56
|
+
### 1. Define the registry
|
|
40
57
|
|
|
41
|
-
|
|
58
|
+
One file, one export named `wormholes` — the keys are the wormhole names (same pattern as Astro Actions' `src/actions/index.ts`):
|
|
42
59
|
|
|
43
60
|
```ts
|
|
44
61
|
// src/wormholes.ts
|
|
45
62
|
import { defineWormhole } from '@astroscope/wormhole';
|
|
46
63
|
|
|
47
|
-
export type
|
|
64
|
+
export type Session = {
|
|
48
65
|
user: string;
|
|
49
66
|
role: string;
|
|
50
67
|
};
|
|
51
68
|
|
|
52
|
-
export const
|
|
69
|
+
export const wormholes = {
|
|
70
|
+
session: defineWormhole<Session>(),
|
|
71
|
+
cart: defineWormhole<{ items: string[] }>(),
|
|
72
|
+
};
|
|
53
73
|
```
|
|
54
74
|
|
|
55
|
-
|
|
75
|
+
`src/wormholes/index.ts` works too.
|
|
56
76
|
|
|
57
|
-
|
|
77
|
+
### 2. Resolve values in middleware
|
|
58
78
|
|
|
59
79
|
```ts
|
|
60
80
|
// src/middleware.ts
|
|
61
|
-
import {
|
|
62
|
-
import { defineMiddleware } from 'astro:middleware';
|
|
63
|
-
import { userState } from './wormholes';
|
|
64
|
-
|
|
65
|
-
export const onRequest = defineMiddleware((ctx, next) => {
|
|
66
|
-
const data = { user: 'Alice', role: 'admin' };
|
|
81
|
+
import { createWormholeMiddleware } from '@astroscope/wormhole/server';
|
|
67
82
|
|
|
68
|
-
|
|
83
|
+
export const onRequest = createWormholeMiddleware({
|
|
84
|
+
values: async (ctx) => ({
|
|
85
|
+
session: { user: 'Alice', role: 'admin' },
|
|
86
|
+
cart: await loadCart(ctx),
|
|
87
|
+
}),
|
|
69
88
|
});
|
|
70
89
|
```
|
|
71
90
|
|
|
72
|
-
`
|
|
91
|
+
The `values` return type is checked against your registry. Omit a key (or return `undefined` for it) to leave that wormhole closed for the request. Each request is isolated.
|
|
73
92
|
|
|
74
|
-
### 3.
|
|
75
|
-
|
|
76
|
-
Add `<WormholeScript>` to your layout to serialize the data into an inline script:
|
|
77
|
-
|
|
78
|
-
```astro
|
|
79
|
-
---
|
|
80
|
-
import { WormholeScript } from '@astroscope/wormhole/astro';
|
|
81
|
-
import { userState } from '../wormholes';
|
|
82
|
-
---
|
|
83
|
-
|
|
84
|
-
<html>
|
|
85
|
-
<head>
|
|
86
|
-
<WormholeScript wormhole={userState} />
|
|
87
|
-
</head>
|
|
88
|
-
<body>
|
|
89
|
-
<slot />
|
|
90
|
-
</body>
|
|
91
|
-
</html>
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
### 4. Read in components
|
|
93
|
+
### 3. Read anywhere
|
|
95
94
|
|
|
96
95
|
#### Astro frontmatter (SSR)
|
|
97
96
|
|
|
98
97
|
```astro
|
|
99
98
|
---
|
|
100
|
-
import {
|
|
99
|
+
import { wormholes } from '@astroscope/wormhole';
|
|
101
100
|
|
|
102
|
-
const { user } =
|
|
101
|
+
const { user } = wormholes.session.get();
|
|
103
102
|
---
|
|
104
103
|
|
|
105
104
|
<p>Hello, {user}</p>
|
|
@@ -108,11 +107,11 @@ const { user } = userState.get();
|
|
|
108
107
|
#### React islands
|
|
109
108
|
|
|
110
109
|
```tsx
|
|
110
|
+
import { wormholes } from '@astroscope/wormhole';
|
|
111
111
|
import { useWormhole } from '@astroscope/wormhole/react';
|
|
112
|
-
import { userState } from '../wormholes';
|
|
113
112
|
|
|
114
113
|
export function UserBadge() {
|
|
115
|
-
const { user, role } = useWormhole(
|
|
114
|
+
const { user, role } = useWormhole(wormholes.session);
|
|
116
115
|
|
|
117
116
|
return (
|
|
118
117
|
<span>
|
|
@@ -128,34 +127,33 @@ export function UserBadge() {
|
|
|
128
127
|
<p>User: <strong id="user">-</strong></p>
|
|
129
128
|
|
|
130
129
|
<script>
|
|
131
|
-
import {
|
|
130
|
+
import { wormholes } from '@astroscope/wormhole';
|
|
132
131
|
|
|
133
|
-
document.getElementById('user')!.textContent =
|
|
132
|
+
document.getElementById('user')!.textContent = wormholes.session.get().user;
|
|
134
133
|
|
|
135
|
-
|
|
134
|
+
wormholes.session.subscribe((data) => {
|
|
136
135
|
document.getElementById('user')!.textContent = data.user;
|
|
137
136
|
});
|
|
138
137
|
</script>
|
|
139
138
|
```
|
|
140
139
|
|
|
141
|
-
###
|
|
140
|
+
### 4. Update from the client
|
|
142
141
|
|
|
143
|
-
Call `set()` to update the wormhole —
|
|
142
|
+
Call `set()` to update the wormhole — every `useWormhole()` hook and `subscribe()` callback on the page reacts, across islands:
|
|
144
143
|
|
|
145
144
|
```tsx
|
|
145
|
+
import { wormholes } from '@astroscope/wormhole';
|
|
146
146
|
import { useWormhole } from '@astroscope/wormhole/react';
|
|
147
147
|
import { actions } from 'astro:actions';
|
|
148
|
-
import { userState } from '../wormholes';
|
|
149
148
|
|
|
150
149
|
export function RoleToggle() {
|
|
151
|
-
const { user, role } = useWormhole(
|
|
150
|
+
const { user, role } = useWormhole(wormholes.session);
|
|
152
151
|
|
|
153
152
|
async function toggle() {
|
|
154
|
-
const
|
|
155
|
-
const result = await actions.updateRole({ role: newRole });
|
|
153
|
+
const result = await actions.updateRole({ role: role === 'admin' ? 'viewer' : 'admin' });
|
|
156
154
|
|
|
157
155
|
if (!result.error) {
|
|
158
|
-
|
|
156
|
+
wormholes.session.set(result.data);
|
|
159
157
|
}
|
|
160
158
|
}
|
|
161
159
|
|
|
@@ -169,83 +167,41 @@ export function RoleToggle() {
|
|
|
169
167
|
|
|
170
168
|
## API
|
|
171
169
|
|
|
172
|
-
### `
|
|
173
|
-
|
|
174
|
-
Creates a typed wormhole channel. The returned object is universal — works on both server and client.
|
|
175
|
-
|
|
176
|
-
```ts
|
|
177
|
-
import { defineWormhole } from '@astroscope/wormhole';
|
|
178
|
-
|
|
179
|
-
const wh = defineWormhole<{ count: number }>('counter');
|
|
180
|
-
```
|
|
170
|
+
### `wormholes` <sub>proxy, server + client</sub>
|
|
181
171
|
|
|
182
|
-
|
|
183
|
-
| ------------------ | --------------------------------------------------------------------------------------------------- |
|
|
184
|
-
| `wh.get()` | Read current value as `DeepReadonly<T>` (from `AsyncLocalStorage` on server, from `globalThis` or local store on client) |
|
|
185
|
-
| `wh.set(data)` | Replace value with a new one and notify all subscribers (client-side) |
|
|
186
|
-
| `wh.subscribe(fn)` | Listen for changes, returns unsubscribe function |
|
|
187
|
-
| `wh.name` | The wormhole name |
|
|
188
|
-
| `wh.key` | The internal `globalThis` key |
|
|
172
|
+
The single access point, typed from your registry. `wormholes.cart` returns a `Wormhole<T>`:
|
|
189
173
|
|
|
190
|
-
|
|
174
|
+
| Method | Description |
|
|
175
|
+
| ------------------------- | -------------------------------------------------------------------------------- |
|
|
176
|
+
| `wormholes.x.get()` | Read the current value as `DeepReadonly<T>` (server: request scope; client: streamed data) |
|
|
177
|
+
| `wormholes.x.set(data)` | Replace the value and notify all subscribers (client only) |
|
|
178
|
+
| `wormholes.x.subscribe(fn)` | Listen for `set()` updates, returns an unsubscribe function |
|
|
191
179
|
|
|
192
|
-
|
|
180
|
+
Accesses must be static (`wormholes.cart`, `wormholes['cart']`). A dynamic access (`wormholes[name]`) still works, but the build can no longer tell which wormholes that chunk needs and delivers all open ones to its islands.
|
|
193
181
|
|
|
194
|
-
|
|
195
|
-
import type { DeepReadonly } from '@astroscope/wormhole';
|
|
182
|
+
### `defineWormhole<T>()` <sub>registry only</sub>
|
|
196
183
|
|
|
197
|
-
|
|
198
|
-
return cart.items.some((item) => item.productId === id);
|
|
199
|
-
}
|
|
200
|
-
```
|
|
184
|
+
Creates a typed wormhole for the registry; the name comes from the registry key. Registry entries are full `Wormhole<T>` objects, so server code can also import `wormholes` from your own registry file directly.
|
|
201
185
|
|
|
202
|
-
### `
|
|
186
|
+
### `createWormholeMiddleware({ values, exclude? })` <sub>server only</sub>
|
|
203
187
|
|
|
204
|
-
|
|
188
|
+
Creates the middleware: resolves `values(ctx)` per request, opens them for server-side `get()`, and delivers them to the client. `exclude` accepts the same patterns as other astroscope middlewares (defaults to `RECOMMENDED_EXCLUDES`).
|
|
205
189
|
|
|
206
|
-
|
|
207
|
-
import { open } from '@astroscope/wormhole/server';
|
|
190
|
+
### `openWormholes(wh, data, fn)` <sub>server only</sub>
|
|
208
191
|
|
|
209
|
-
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
To open several wormholes at once, pass an array of `[wormhole, data]` pairs — each pair is type-checked against its own wormhole:
|
|
213
|
-
|
|
214
|
-
```ts
|
|
215
|
-
return open([
|
|
216
|
-
[cartStore, cart],
|
|
217
|
-
[sessionStore, session],
|
|
218
|
-
], () => next());
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
Nested `open()` calls shadow only the wormholes they name; everything else stays visible.
|
|
192
|
+
Provides values to server code that runs outside the request pipeline — tests, out-of-request rendering. Never delivered to the client; inside the app, the middleware is the way. Accepts a single pair or an array of `[wormhole, data]` pairs.
|
|
222
193
|
|
|
223
194
|
### `useWormhole(wh)` <sub>React only</sub>
|
|
224
195
|
|
|
225
|
-
React hook that reads
|
|
226
|
-
|
|
227
|
-
```tsx
|
|
228
|
-
import { useWormhole } from '@astroscope/wormhole/react';
|
|
229
|
-
|
|
230
|
-
const data = useWormhole(myWormhole);
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
### `<WormholeScript wormhole={wh} />` <sub>Astro only</sub>
|
|
196
|
+
React hook that reads a wormhole and re-renders on `set()`. During SSR it reads the request scope, so server and client render identically.
|
|
234
197
|
|
|
235
|
-
|
|
198
|
+
### `DeepReadonly<T>` / `UnwrapWormholes<R>`
|
|
236
199
|
|
|
237
|
-
|
|
238
|
-
import {WormholeScript} from '@astroscope/wormhole/astro';
|
|
239
|
-
|
|
240
|
-
<WormholeScript wormhole={myWormhole} />
|
|
241
|
-
```
|
|
200
|
+
Exported helper types. `DeepReadonly<T>` is the recursively-readonly value type; `UnwrapWormholes` maps a registry shape to its value types.
|
|
242
201
|
|
|
243
202
|
## How it works
|
|
244
203
|
|
|
245
|
-
|
|
246
|
-
2. **`<WormholeScript>`** calls `wh.get()` during SSR, serializes the result into `<script is:inline>globalThis[key] = function(){return data;}</script>`
|
|
247
|
-
3. **Client** calls `wh.get()` — reads from `globalThis[key]()` (the serialized getter)
|
|
248
|
-
4. **`set()`** updates a local store and notifies all subscribers — `useWormhole()` hooks and `subscribe()` callbacks re-render/fire
|
|
204
|
+
At build time the integration scans your client code to see which wormholes each component reads, and generates the types for the `wormholes` proxy from your registry. At runtime the middleware resolves your values once per request, and each island's data is embedded in the HTML right before it — so it is always there before the island's code runs, even while the page is still streaming. On the client, `set()` updates the shared data and notifies subscribers, keeping islands and scripts in sync.
|
|
249
205
|
|
|
250
206
|
## License
|
|
251
207
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { t as wormholeKey } from "./key-BqTy1gM3.js";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
//#region src/als.ts
|
|
4
|
+
const als = globalThis[Symbol.for("@astroscope/wormhole/als")] ??= new AsyncLocalStorage();
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region src/define.ts
|
|
7
|
+
const ASSIGN = Symbol.for("@astroscope/wormhole.assign");
|
|
8
|
+
/**
|
|
9
|
+
* Define a wormhole for the `src/wormholes.ts` registry. The wormhole's name is the
|
|
10
|
+
* registry key — it is assigned when the registry is passed to
|
|
11
|
+
* `createWormholeMiddleware()` (or `assignWormholeNames()`).
|
|
12
|
+
*
|
|
13
|
+
* **Security:** wormhole data is serialized into the HTML and sent to the browser.
|
|
14
|
+
* Never store secrets (tokens, API keys, credentials) in a wormhole.
|
|
15
|
+
*/
|
|
16
|
+
function defineWormhole() {
|
|
17
|
+
let name;
|
|
18
|
+
const requireName = () => {
|
|
19
|
+
if (name === void 0) throw new Error("wormhole is not registered — pass its registry to createWormholeMiddleware()");
|
|
20
|
+
return name;
|
|
21
|
+
};
|
|
22
|
+
const key = () => wormholeKey(requireName());
|
|
23
|
+
return {
|
|
24
|
+
get name() {
|
|
25
|
+
return requireName();
|
|
26
|
+
},
|
|
27
|
+
get key() {
|
|
28
|
+
return key();
|
|
29
|
+
},
|
|
30
|
+
get() {
|
|
31
|
+
const value = als.getStore()?.get(key());
|
|
32
|
+
if (value === void 0) throw new Error(`wormhole "${requireName()}" is not open for this request — is it provided by the wormhole middleware?`);
|
|
33
|
+
return value;
|
|
34
|
+
},
|
|
35
|
+
set() {
|
|
36
|
+
throw new Error(`wormhole "${requireName()}" set() cannot be called on the server — values are request-scoped and provided by the middleware`);
|
|
37
|
+
},
|
|
38
|
+
subscribe() {
|
|
39
|
+
return () => {};
|
|
40
|
+
},
|
|
41
|
+
[ASSIGN](next) {
|
|
42
|
+
if (name !== void 0 && name !== next) throw new Error(`wormhole is registered under two names: "${name}" and "${next}"`);
|
|
43
|
+
name = next;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Assign each wormhole its registry key as name. Called by `createWormholeMiddleware()`;
|
|
49
|
+
* exposed for setups that read wormholes on the server without the middleware.
|
|
50
|
+
*/
|
|
51
|
+
function assignWormholeNames(registry) {
|
|
52
|
+
for (const [name, wormhole] of Object.entries(registry)) {
|
|
53
|
+
const assign = wormhole[ASSIGN];
|
|
54
|
+
if (typeof assign !== "function") throw new Error(`registry entry "${name}" is not a wormhole — the registry must contain defineWormhole() values only`);
|
|
55
|
+
assign(name);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
export { defineWormhole as n, als as r, assignWormholeNames as t };
|
|
60
|
+
|
|
61
|
+
//# sourceMappingURL=define-BO-pYQc1.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-BO-pYQc1.js","names":[],"sources":["../src/als.ts","../src/define.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\n\n// a single ALS holds an immutable map of all open wormholes (keyed by wormhole key);\n// keyed on globalThis so the vite-runner and native module instances share one context\nexport const als: AsyncLocalStorage<ReadonlyMap<string, unknown>> = ((globalThis as any)[\n Symbol.for('@astroscope/wormhole/als')\n] ??= new AsyncLocalStorage());\n","import { als } from './als.js';\nimport { wormholeKey } from './key.js';\nimport type { DeepReadonly, Wormhole } from './types.js';\n\n// name assignment must survive dual module instances (vite runner + native), so the\n// assigner rides the wormhole object itself under a Symbol.for key\nconst ASSIGN = Symbol.for('@astroscope/wormhole.assign');\n\n/**\n * Define a wormhole for the `src/wormholes.ts` registry. The wormhole's name is the\n * registry key — it is assigned when the registry is passed to\n * `createWormholeMiddleware()` (or `assignWormholeNames()`).\n *\n * **Security:** wormhole data is serialized into the HTML and sent to the browser.\n * Never store secrets (tokens, API keys, credentials) in a wormhole.\n */\nexport function defineWormhole<T>(): Wormhole<T> {\n let name: string | undefined;\n\n const requireName = (): string => {\n if (name === undefined) {\n throw new Error('wormhole is not registered — pass its registry to createWormholeMiddleware()');\n }\n\n return name;\n };\n\n const key = (): string => wormholeKey(requireName());\n\n // methods must not rely on `this` — consumers pass them around detached\n // (e.g. useSyncExternalStore(wormhole.subscribe, wormhole.get, wormhole.get))\n const wormhole: Wormhole<T> & { [ASSIGN](next: string): void } = {\n get name() {\n return requireName();\n },\n\n get key() {\n return key();\n },\n\n get(): DeepReadonly<T> {\n const value = als.getStore()?.get(key());\n\n if (value === undefined) {\n throw new Error(\n `wormhole \"${requireName()}\" is not open for this request — is it provided by the wormhole middleware?`,\n );\n }\n\n return value as DeepReadonly<T>;\n },\n\n set(): void {\n throw new Error(\n `wormhole \"${requireName()}\" set() cannot be called on the server — values are request-scoped and provided by the middleware`,\n );\n },\n\n subscribe(): () => void {\n // server values never change within a request — nothing to notify\n return () => {};\n },\n\n [ASSIGN](next: string): void {\n if (name !== undefined && name !== next) {\n throw new Error(`wormhole is registered under two names: \"${name}\" and \"${next}\"`);\n }\n\n name = next;\n },\n };\n\n return wormhole;\n}\n\n/**\n * Assign each wormhole its registry key as name. Called by `createWormholeMiddleware()`;\n * exposed for setups that read wormholes on the server without the middleware.\n */\nexport function assignWormholeNames(registry: Record<string, object>): void {\n for (const [name, wormhole] of Object.entries(registry)) {\n const assign = (wormhole as { [ASSIGN]?: (next: string) => void })[ASSIGN];\n\n if (typeof assign !== 'function') {\n throw new Error(\n `registry entry \"${name}\" is not a wormhole — the registry must contain defineWormhole() values only`,\n );\n }\n\n assign(name);\n }\n}\n"],"mappings":";;;AAIA,MAAa,MAAwD,WACnE,OAAO,IAAI,0BAA0B,OACjC,IAAI,kBAAkB;;;ACA5B,MAAM,SAAS,OAAO,IAAI,6BAA6B;;;;;;;;;AAUvD,SAAgB,iBAAiC;CAC/C,IAAI;CAEJ,MAAM,oBAA4B;EAChC,IAAI,SAAS,KAAA,GACX,MAAM,IAAI,MAAM,8EAA8E;EAGhG,OAAO;CACT;CAEA,MAAM,YAAoB,YAAY,YAAY,CAAC;CA6CnD,OAAO;EAxCL,IAAI,OAAO;GACT,OAAO,YAAY;EACrB;EAEA,IAAI,MAAM;GACR,OAAO,IAAI;EACb;EAEA,MAAuB;GACrB,MAAM,QAAQ,IAAI,SAAS,CAAC,EAAE,IAAI,IAAI,CAAC;GAEvC,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MACR,aAAa,YAAY,EAAE,4EAC7B;GAGF,OAAO;EACT;EAEA,MAAY;GACV,MAAM,IAAI,MACR,aAAa,YAAY,EAAE,kGAC7B;EACF;EAEA,YAAwB;GAEtB,aAAa,CAAC;EAChB;EAEA,CAAC,QAAQ,MAAoB;GAC3B,IAAI,SAAS,KAAA,KAAa,SAAS,MACjC,MAAM,IAAI,MAAM,4CAA4C,KAAK,SAAS,KAAK,EAAE;GAGnF,OAAO;EACT;CAGY;AAChB;;;;;AAMA,SAAgB,oBAAoB,UAAwC;CAC1E,KAAK,MAAM,CAAC,MAAM,aAAa,OAAO,QAAQ,QAAQ,GAAG;EACvD,MAAM,SAAU,SAAmD;EAEnE,IAAI,OAAO,WAAW,YACpB,MAAM,IAAI,MACR,mBAAmB,KAAK,6EAC1B;EAGF,OAAO,IAAI;CACb;AACF"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { a as WormholeRegistry, i as WormholeMap, n as UnwrapWormholes, r as Wormhole, t as DeepReadonly } from "./types-okMd-V3s.js";
|
|
2
|
+
//#region src/index.browser.d.ts
|
|
3
|
+
declare const wormholes: WormholeMap;
|
|
4
|
+
declare function defineWormhole(): never;
|
|
5
|
+
declare function assignWormholeNames(): never;
|
|
6
|
+
declare function wormholeIntegration(): never;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { type DeepReadonly, type UnwrapWormholes, type Wormhole, type WormholeMap, type WormholeRegistry, assignWormholeNames, wormholeIntegration as default, defineWormhole, wormholes };
|
|
9
|
+
//# sourceMappingURL=index.browser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.browser.d.ts","names":[],"sources":["../src/index.browser.ts"],"mappings":";;cAiEa,WAAW;iBAMR;iBAMA;iBAIQ"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { t as wormholeKey } from "./key-BqTy1gM3.js";
|
|
2
|
+
//#region src/index.browser.ts
|
|
3
|
+
const store = globalThis.__wormholes__ ??= {};
|
|
4
|
+
const stubs = /* @__PURE__ */ new Map();
|
|
5
|
+
function stub(name) {
|
|
6
|
+
let wormhole = stubs.get(name);
|
|
7
|
+
if (wormhole) return wormhole;
|
|
8
|
+
const entry = () => store[name] ??= {};
|
|
9
|
+
wormhole = {
|
|
10
|
+
name,
|
|
11
|
+
key: wormholeKey(name),
|
|
12
|
+
get() {
|
|
13
|
+
const value = entry().v;
|
|
14
|
+
if (value === void 0) throw new Error(`wormhole "${name}" has no data — is it provided by the wormhole middleware?`);
|
|
15
|
+
return value;
|
|
16
|
+
},
|
|
17
|
+
set(data) {
|
|
18
|
+
const current = entry();
|
|
19
|
+
current.v = data;
|
|
20
|
+
current.l?.forEach((fn) => fn(data));
|
|
21
|
+
},
|
|
22
|
+
subscribe(fn) {
|
|
23
|
+
const current = entry();
|
|
24
|
+
(current.l ??= []).push(fn);
|
|
25
|
+
return () => {
|
|
26
|
+
current.l = current.l?.filter((listener) => listener !== fn);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
stubs.set(name, wormhole);
|
|
31
|
+
return wormhole;
|
|
32
|
+
}
|
|
33
|
+
const wormholes = new Proxy({}, { get(_target, prop) {
|
|
34
|
+
return typeof prop === "string" ? stub(prop) : void 0;
|
|
35
|
+
} });
|
|
36
|
+
function defineWormhole() {
|
|
37
|
+
throw new Error("defineWormhole() is server-only — do not import the wormhole registry in client code; access wormholes via the `wormholes` proxy instead");
|
|
38
|
+
}
|
|
39
|
+
function assignWormholeNames() {
|
|
40
|
+
throw new Error("assignWormholeNames() is server-only");
|
|
41
|
+
}
|
|
42
|
+
function wormholeIntegration() {
|
|
43
|
+
throw new Error("the @astroscope/wormhole integration is server-only");
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { assignWormholeNames, wormholeIntegration as default, defineWormhole, wormholes };
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=index.browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.browser.js","names":[],"sources":["../src/index.browser.ts"],"sourcesContent":["import { wormholeKey } from './key.js';\nimport type { DeepReadonly, Wormhole, WormholeMap } from './types.js';\n\nexport type { DeepReadonly, UnwrapWormholes, Wormhole, WormholeMap, WormholeRegistry } from './types.js';\n\n/**\n * Browser side of the `wormholes` proxy. Values live on `self.__wormholes__` — written\n * by the inline merge scripts the server emits before each island (and at stream end\n * for script consumers), or by client-side `set()` calls. The entry protocol is\n * `{ v: value, l: listeners }`, shared with the emitted scripts, so writes land in\n * either order.\n */\n\ntype Entry = { v?: unknown; l?: ((value: unknown) => void)[] | undefined };\n\nconst store: Record<string, Entry> = ((globalThis as any).__wormholes__ ??= {});\n\nconst stubs = new Map<string, Wormhole<unknown>>();\n\nfunction stub(name: string): Wormhole<unknown> {\n let wormhole = stubs.get(name);\n\n if (wormhole) {\n return wormhole;\n }\n\n const entry = (): Entry => (store[name] ??= {});\n\n wormhole = {\n name,\n key: wormholeKey(name),\n\n get(): DeepReadonly<unknown> {\n const value = entry().v;\n\n if (value === undefined) {\n throw new Error(`wormhole \"${name}\" has no data — is it provided by the wormhole middleware?`);\n }\n\n return value;\n },\n\n set(data): void {\n const current = entry();\n\n current.v = data;\n current.l?.forEach((fn) => fn(data));\n },\n\n subscribe(fn): () => void {\n const current = entry();\n\n (current.l ??= []).push(fn);\n\n return () => {\n current.l = current.l?.filter((listener) => listener !== fn);\n };\n },\n };\n\n stubs.set(name, wormhole);\n\n return wormhole;\n}\n\nexport const wormholes: WormholeMap = new Proxy({} as WormholeMap, {\n get(_target, prop) {\n return typeof prop === 'string' ? stub(prop) : undefined;\n },\n});\n\nexport function defineWormhole(): never {\n throw new Error(\n 'defineWormhole() is server-only — do not import the wormhole registry in client code; access wormholes via the `wormholes` proxy instead',\n );\n}\n\nexport function assignWormholeNames(): never {\n throw new Error('assignWormholeNames() is server-only');\n}\n\nexport default function wormholeIntegration(): never {\n throw new Error('the @astroscope/wormhole integration is server-only');\n}\n"],"mappings":";;AAeA,MAAM,QAAgC,WAAoB,kBAAkB,CAAC;AAE7E,MAAM,wBAAQ,IAAI,IAA+B;AAEjD,SAAS,KAAK,MAAiC;CAC7C,IAAI,WAAW,MAAM,IAAI,IAAI;CAE7B,IAAI,UACF,OAAO;CAGT,MAAM,cAAsB,MAAM,UAAU,CAAC;CAE7C,WAAW;EACT;EACA,KAAK,YAAY,IAAI;EAErB,MAA6B;GAC3B,MAAM,QAAQ,MAAM,CAAC,CAAC;GAEtB,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MAAM,aAAa,KAAK,2DAA2D;GAG/F,OAAO;EACT;EAEA,IAAI,MAAY;GACd,MAAM,UAAU,MAAM;GAEtB,QAAQ,IAAI;GACZ,QAAQ,GAAG,SAAS,OAAO,GAAG,IAAI,CAAC;EACrC;EAEA,UAAU,IAAgB;GACxB,MAAM,UAAU,MAAM;GAEtB,CAAC,QAAQ,MAAM,CAAC,EAAA,CAAG,KAAK,EAAE;GAE1B,aAAa;IACX,QAAQ,IAAI,QAAQ,GAAG,QAAQ,aAAa,aAAa,EAAE;GAC7D;EACF;CACF;CAEA,MAAM,IAAI,MAAM,QAAQ;CAExB,OAAO;AACT;AAEA,MAAa,YAAyB,IAAI,MAAM,CAAC,GAAkB,EACjE,IAAI,SAAS,MAAM;CACjB,OAAO,OAAO,SAAS,WAAW,KAAK,IAAI,IAAI,KAAA;AACjD,EACF,CAAC;AAED,SAAgB,iBAAwB;CACtC,MAAM,IAAI,MACR,0IACF;AACF;AAEA,SAAgB,sBAA6B;CAC3C,MAAM,IAAI,MAAM,sCAAsC;AACxD;AAEA,SAAwB,sBAA6B;CACnD,MAAM,IAAI,MAAM,qDAAqD;AACvE"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,43 @@
|
|
|
1
|
-
import { n as Wormhole, t as DeepReadonly } from "./types-
|
|
2
|
-
|
|
1
|
+
import { a as WormholeRegistry, i as WormholeMap, n as UnwrapWormholes, r as Wormhole, t as DeepReadonly } from "./types-okMd-V3s.js";
|
|
2
|
+
import { AstroIntegration } from "astro";
|
|
3
|
+
//#region src/integration.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Astro integration for wormholes with per-island payload slicing.
|
|
6
|
+
*
|
|
7
|
+
* Resolves the `src/wormholes.ts` registry and exposes it to the middleware as a
|
|
8
|
+
* virtual module, registers the vite plugin that scans client code for
|
|
9
|
+
* `wormholes.<name>` accesses and maps them onto the emitted chunks, and generates
|
|
10
|
+
* the registry type stub that types the `wormholes` proxy. Delivery happens at
|
|
11
|
+
* runtime: the wormhole middleware opens per-request values, and the islands
|
|
12
|
+
* emitter (via `@astroscope/node`) writes each island's slice before its tag.
|
|
13
|
+
*/
|
|
14
|
+
declare function wormholeIntegration(): AstroIntegration;
|
|
15
|
+
//#endregion
|
|
3
16
|
//#region src/define.d.ts
|
|
4
17
|
/**
|
|
5
|
-
* Define a wormhole
|
|
18
|
+
* Define a wormhole for the `src/wormholes.ts` registry. The wormhole's name is the
|
|
19
|
+
* registry key — it is assigned when the registry is passed to
|
|
20
|
+
* `createWormholeMiddleware()` (or `assignWormholeNames()`).
|
|
6
21
|
*
|
|
7
22
|
* **Security:** wormhole data is serialized into the HTML and sent to the browser.
|
|
8
23
|
* Never store secrets (tokens, API keys, credentials) in a wormhole.
|
|
9
24
|
*/
|
|
10
|
-
declare function defineWormhole<T>(
|
|
25
|
+
declare function defineWormhole<T>(): Wormhole<T>;
|
|
26
|
+
/**
|
|
27
|
+
* Assign each wormhole its registry key as name. Called by `createWormholeMiddleware()`;
|
|
28
|
+
* exposed for setups that read wormholes on the server without the middleware.
|
|
29
|
+
*/
|
|
30
|
+
declare function assignWormholeNames(registry: Record<string, object>): void;
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/index.d.ts
|
|
33
|
+
/**
|
|
34
|
+
* Access wormholes by name — `wormholes.cart.get()`. This is the server side of the
|
|
35
|
+
* proxy (the browser build ships its own): values come from the request's
|
|
36
|
+
* wormhole middleware scope, so it works in frontmatter, endpoints, and
|
|
37
|
+
* server-rendered islands alike. Typed via the registry type stub the integration
|
|
38
|
+
* generates from `src/wormholes.ts`.
|
|
39
|
+
*/
|
|
40
|
+
declare const wormholes: WormholeMap;
|
|
11
41
|
//#endregion
|
|
12
|
-
export { type DeepReadonly, type Wormhole, defineWormhole };
|
|
42
|
+
export { type DeepReadonly, type UnwrapWormholes, type Wormhole, type WormholeMap, type WormholeRegistry, assignWormholeNames, wormholeIntegration as default, defineWormhole, wormholes };
|
|
13
43
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/define.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/integration.ts","../src/define.ts","../src/index.ts"],"mappings":";;;;;;;;;;;;;iBAgBwB,uBAAuB;;;;;;;;;;;iBCA/B,eAAe,MAAM,SAAS;;;;;iBA+D9B,oBAAoB,UAAU;;;;;;;;;;cClDjC,WAAW"}
|