@astroscope/wormhole 1.0.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 CHANGED
@@ -1,14 +1,14 @@
1
1
  # @astroscope/wormhole
2
2
 
3
- > **Note:** This package is in active development. APIs may change between versions.
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: populate data in middleware, read it in Astro frontmatter, React islands, or `<script>` blocks — same typed API on server and client. Multiple wormholes per page, reactive updates across all consumers, zero configuration.
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.** All wormhole data is serialized into an inline `<script>` tag and sent to the browser. Never store tokens, API keys, credentials, or any sensitive data in a wormhole.
24
- - **`set()` is client-only.** Calling `set()` does not work on the server. Use `open(wormhole, data, fn)` from `@astroscope/wormhole/server` to populate data in middleware.
25
- - **Values are deeply readonly.** `get()`, `subscribe()` and `useWormhole()` expose the value as `DeepReadonly<T>` — in-place mutation would silently bypass subscribers (and on the server would corrupt the request-scoped store). The only way to update is `set()` with a new value: `wh.set({ ...wh.get(), count: 1 })`.
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 a wormhole
56
+ ### 1. Define the registry
40
57
 
41
- Create a shared file imported by both server and client code:
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 UserState = {
64
+ export type Session = {
48
65
  user: string;
49
66
  role: string;
50
67
  };
51
68
 
52
- export const userState = defineWormhole<UserState>('user');
69
+ export const wormholes = {
70
+ session: defineWormhole<Session>(),
71
+ cart: defineWormhole<{ items: string[] }>(),
72
+ };
53
73
  ```
54
74
 
55
- ### 2. Populate in middleware
75
+ `src/wormholes/index.ts` works too.
56
76
 
57
- Use `open()` from the server entry point to provide data during request handling:
77
+ ### 2. Resolve values in middleware
58
78
 
59
79
  ```ts
60
80
  // src/middleware.ts
61
- import { open } from '@astroscope/wormhole/server';
62
- import { defineMiddleware } from 'astro:middleware';
63
- import { userState } from './wormholes';
81
+ import { createWormholeMiddleware } from '@astroscope/wormhole/server';
64
82
 
65
- export const onRequest = defineMiddleware((ctx, next) => {
66
- const data = { user: 'Alice', role: 'admin' };
67
-
68
- return open(userState, data, () => next());
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
- `open()` uses `AsyncLocalStorage` under the hood — each request gets its own isolated data.
73
-
74
- ### 3. Bridge to the client
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.
75
92
 
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 { userState } from '../wormholes';
99
+ import { wormholes } from '@astroscope/wormhole';
101
100
 
102
- const { user } = userState.get();
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(userState);
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 { userState } from '../wormholes';
130
+ import { wormholes } from '@astroscope/wormhole';
132
131
 
133
- document.getElementById('user')!.textContent = userState.get().user;
132
+ document.getElementById('user')!.textContent = wormholes.session.get().user;
134
133
 
135
- userState.subscribe((data) => {
134
+ wormholes.session.subscribe((data) => {
136
135
  document.getElementById('user')!.textContent = data.user;
137
136
  });
138
137
  </script>
139
138
  ```
140
139
 
141
- ### 5. Update from the client
140
+ ### 4. Update from the client
142
141
 
143
- Call `set()` to update the wormhole — all `useWormhole()` hooks and `subscribe()` callbacks react immediately:
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(userState);
150
+ const { user, role } = useWormhole(wormholes.session);
152
151
 
153
152
  async function toggle() {
154
- const newRole = role === 'admin' ? 'viewer' : 'admin';
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
- userState.set(result.data);
156
+ wormholes.session.set(result.data);
159
157
  }
160
158
  }
161
159
 
@@ -169,72 +167,41 @@ export function RoleToggle() {
169
167
 
170
168
  ## API
171
169
 
172
- ### `defineWormhole<T>(name)`
170
+ ### `wormholes` <sub>proxy, server + client</sub>
173
171
 
174
- Creates a typed wormhole channel. The returned object is universal — works on both server and client.
172
+ The single access point, typed from your registry. `wormholes.cart` returns a `Wormhole<T>`:
175
173
 
176
- ```ts
177
- import { defineWormhole } from '@astroscope/wormhole';
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 |
178
179
 
179
- const wh = defineWormhole<{ count: number }>('counter');
180
- ```
181
-
182
- | Method | Description |
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 |
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.
189
181
 
190
- ### `DeepReadonly<T>`
182
+ ### `defineWormhole<T>()` <sub>registry only</sub>
191
183
 
192
- Exported helper type — recursively `readonly` version of `T`. Wormhole values are exposed with this type; use it in your own function signatures when passing wormhole data around:
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.
193
185
 
194
- ```ts
195
- import type { DeepReadonly } from '@astroscope/wormhole';
186
+ ### `createWormholeMiddleware({ values, exclude? })` <sub>server only</sub>
196
187
 
197
- function isWithinCart(cart: DeepReadonly<Cart>, id: string) {
198
- return cart.items.some((item) => item.productId === id);
199
- }
200
- ```
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`).
201
189
 
202
- ### `open(wh, data, fn)` <sub>server only</sub>
190
+ ### `openWormholes(wh, data, fn)` <sub>server only</sub>
203
191
 
204
- Runs `fn` with `data` available via `wh.get()` for the duration of the call. Uses `AsyncLocalStorage` for request isolation.
205
-
206
- ```ts
207
- import { open } from '@astroscope/wormhole/server';
208
-
209
- return open(myWormhole, { count: 0 }, () => next());
210
- ```
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.
211
193
 
212
194
  ### `useWormhole(wh)` <sub>React only</sub>
213
195
 
214
- React hook that reads the wormhole and re-renders on changes. Uses `useSyncExternalStore` internally.
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.
215
197
 
216
- ```tsx
217
- import { useWormhole } from '@astroscope/wormhole/react';
218
-
219
- const data = useWormhole(myWormhole);
220
- ```
221
-
222
- ### `<WormholeScript wormhole={wh} />` <sub>Astro only</sub>
198
+ ### `DeepReadonly<T>` / `UnwrapWormholes<R>`
223
199
 
224
- Serializes the current wormhole value into an inline `<script>` tag for client hydration.
225
-
226
- ```astro
227
- import {WormholeScript} from '@astroscope/wormhole/astro';
228
-
229
- <WormholeScript wormhole={myWormhole} />
230
- ```
200
+ Exported helper types. `DeepReadonly<T>` is the recursively-readonly value type; `UnwrapWormholes` maps a registry shape to its value types.
231
201
 
232
202
  ## How it works
233
203
 
234
- 1. **Middleware** calls `open(wh, data, next)` — stores data in `AsyncLocalStorage` and sets `globalThis[key]` to read from it
235
- 2. **`<WormholeScript>`** calls `wh.get()` during SSR, serializes the result into `<script is:inline>globalThis[key] = function(){return data;}</script>`
236
- 3. **Client** calls `wh.get()` — reads from `globalThis[key]()` (the serialized getter)
237
- 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.
238
205
 
239
206
  ## License
240
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-0PQTD8gI.js";
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 that transfers state from server middleware to client components.
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>(name: string): Wormhole<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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/define.ts"],"mappings":";;;;;AAQA;;;;iBAAgB,cAAA,IAAkB,IAAA,WAAe,QAAQ,CAAC,CAAA"}
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"}