@astroscope/wormhole 1.1.0 → 3.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 +70 -126
- package/dist/als-CntRqWdO.js +7 -0
- package/dist/als-CntRqWdO.js.map +1 -0
- package/dist/define-1UkPnsXV.js +83 -0
- package/dist/define-1UkPnsXV.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 +53 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +230 -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/middleware-entrypoint.d.ts +4 -0
- package/dist/middleware-entrypoint.d.ts.map +1 -0
- package/dist/middleware-entrypoint.js +263 -0
- package/dist/middleware-entrypoint.js.map +1 -0
- package/dist/react/index.d.ts +3 -4
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js.map +1 -1
- package/dist/scan-CczEOWpD.js +79 -0
- package/dist/scan-CczEOWpD.js.map +1 -0
- package/dist/server.d.ts +11 -16
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +4 -8
- package/dist/server.js.map +1 -1
- package/dist/types-DiWa8aQg.d.ts +53 -0
- package/dist/types-DiWa8aQg.d.ts.map +1 -0
- package/package.json +25 -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,16 @@
|
|
|
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 — each with the handler that resolves its value per request — 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
|
+
**Loading is scoped per route.** The build scans server and client code to see which wormholes each route can reach; at runtime only those handlers run for a request. A wormhole nothing on the route reads costs **nothing** — no database call, no bytes.
|
|
12
|
+
|
|
13
|
+
**Delivery is sliced per island.** Every island receives exactly the wormholes its code reads, embedded in the HTML right before it. A wormhole no client code reads ships **zero bytes**.
|
|
12
14
|
|
|
13
15
|
**Typical use cases:**
|
|
14
16
|
|
|
@@ -20,9 +22,12 @@ Astro recommends [nanostores](https://docs.astro.build/en/recipes/sharing-state/
|
|
|
20
22
|
|
|
21
23
|
## Important notes
|
|
22
24
|
|
|
23
|
-
- **No secrets in wormholes.**
|
|
24
|
-
-
|
|
25
|
-
- **
|
|
25
|
+
- **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.)
|
|
26
|
+
- **Requires `@astroscope/node`** — per-island delivery uses its islands pipeline in production.
|
|
27
|
+
- **The registry is server-only.** It carries the handlers, and with them your server code. Client code must never import `src/wormholes.ts` — it accesses values through the `wormholes` proxy instead. (The client build fails on such an import, and the eslint plugin's `no-registry-import` rule flags it in island code.)
|
|
28
|
+
- **`set()` is client-only.** Server values are request-scoped and come from the handlers.
|
|
29
|
+
- **Values are deeply readonly.** `get()`, `subscribe()` and `useWormhole()` expose the value as `ReadonlyDeep<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 })`.
|
|
30
|
+
- **Prerendered pages get no wormhole data** — values are per-request by definition.
|
|
26
31
|
|
|
27
32
|
## Examples
|
|
28
33
|
|
|
@@ -34,72 +39,56 @@ See the [demo/wormhole](../../demo/wormhole) directory for a working example.
|
|
|
34
39
|
npm install @astroscope/wormhole
|
|
35
40
|
```
|
|
36
41
|
|
|
42
|
+
```ts
|
|
43
|
+
// astro.config.ts
|
|
44
|
+
import node from '@astroscope/node';
|
|
45
|
+
import wormhole from '@astroscope/wormhole';
|
|
46
|
+
|
|
47
|
+
export default defineConfig({
|
|
48
|
+
output: 'server',
|
|
49
|
+
adapter: node(),
|
|
50
|
+
integrations: [wormhole()],
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The integration registers the build-time scanner, injects the middleware after your own `src/middleware.ts` (so handlers see everything it put on `locals`), and generates a type stub from your registry, so `wormholes.<name>` is fully typed everywhere. `wormhole({ exclude })` takes the same patterns as other astroscope middlewares (defaults to `RECOMMENDED_EXCLUDES`).
|
|
55
|
+
|
|
37
56
|
## Usage
|
|
38
57
|
|
|
39
|
-
### 1. Define
|
|
58
|
+
### 1. Define the registry
|
|
40
59
|
|
|
41
|
-
|
|
60
|
+
One file, one export named `wormholes` — the keys are the wormhole names, each entry carries its handler (same pattern as Astro Actions' `src/actions/index.ts`):
|
|
42
61
|
|
|
43
62
|
```ts
|
|
44
63
|
// src/wormholes.ts
|
|
45
64
|
import { defineWormhole } from '@astroscope/wormhole';
|
|
46
65
|
|
|
47
|
-
export type
|
|
66
|
+
export type Session = {
|
|
48
67
|
user: string;
|
|
49
68
|
role: string;
|
|
50
69
|
};
|
|
51
70
|
|
|
52
|
-
export const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// src/middleware.ts
|
|
61
|
-
import { open } from '@astroscope/wormhole/server';
|
|
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' };
|
|
67
|
-
|
|
68
|
-
return open(userState, data, () => next());
|
|
69
|
-
});
|
|
71
|
+
export const wormholes = {
|
|
72
|
+
session: defineWormhole({
|
|
73
|
+
handler: (ctx): Session => ({ user: ctx.locals.user, role: ctx.locals.role }),
|
|
74
|
+
}),
|
|
75
|
+
cart: defineWormhole({
|
|
76
|
+
handler: async (ctx) => loadCart(ctx),
|
|
77
|
+
}),
|
|
78
|
+
};
|
|
70
79
|
```
|
|
71
80
|
|
|
72
|
-
`
|
|
73
|
-
|
|
74
|
-
### 3. Bridge to the client
|
|
81
|
+
`src/wormholes/index.ts` works too. A handler receives the request's `APIContext` and runs only for routes the build found a reader on; return `undefined` to leave the wormhole closed for the request. Each request is isolated.
|
|
75
82
|
|
|
76
|
-
|
|
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
|
|
83
|
+
### 2. Read anywhere
|
|
95
84
|
|
|
96
85
|
#### Astro frontmatter (SSR)
|
|
97
86
|
|
|
98
87
|
```astro
|
|
99
88
|
---
|
|
100
|
-
import {
|
|
89
|
+
import { wormholes } from '@astroscope/wormhole';
|
|
101
90
|
|
|
102
|
-
const { user } =
|
|
91
|
+
const { user } = wormholes.session.get();
|
|
103
92
|
---
|
|
104
93
|
|
|
105
94
|
<p>Hello, {user}</p>
|
|
@@ -108,11 +97,11 @@ const { user } = userState.get();
|
|
|
108
97
|
#### React islands
|
|
109
98
|
|
|
110
99
|
```tsx
|
|
100
|
+
import { wormholes } from '@astroscope/wormhole';
|
|
111
101
|
import { useWormhole } from '@astroscope/wormhole/react';
|
|
112
|
-
import { userState } from '../wormholes';
|
|
113
102
|
|
|
114
103
|
export function UserBadge() {
|
|
115
|
-
const { user, role } = useWormhole(
|
|
104
|
+
const { user, role } = useWormhole(wormholes.session);
|
|
116
105
|
|
|
117
106
|
return (
|
|
118
107
|
<span>
|
|
@@ -128,34 +117,33 @@ export function UserBadge() {
|
|
|
128
117
|
<p>User: <strong id="user">-</strong></p>
|
|
129
118
|
|
|
130
119
|
<script>
|
|
131
|
-
import {
|
|
120
|
+
import { wormholes } from '@astroscope/wormhole';
|
|
132
121
|
|
|
133
|
-
document.getElementById('user')!.textContent =
|
|
122
|
+
document.getElementById('user')!.textContent = wormholes.session.get().user;
|
|
134
123
|
|
|
135
|
-
|
|
124
|
+
wormholes.session.subscribe((data) => {
|
|
136
125
|
document.getElementById('user')!.textContent = data.user;
|
|
137
126
|
});
|
|
138
127
|
</script>
|
|
139
128
|
```
|
|
140
129
|
|
|
141
|
-
###
|
|
130
|
+
### 3. Update from the client
|
|
142
131
|
|
|
143
|
-
Call `set()` to update the wormhole —
|
|
132
|
+
Call `set()` to update the wormhole — every `useWormhole()` hook and `subscribe()` callback on the page reacts, across islands:
|
|
144
133
|
|
|
145
134
|
```tsx
|
|
135
|
+
import { wormholes } from '@astroscope/wormhole';
|
|
146
136
|
import { useWormhole } from '@astroscope/wormhole/react';
|
|
147
137
|
import { actions } from 'astro:actions';
|
|
148
|
-
import { userState } from '../wormholes';
|
|
149
138
|
|
|
150
139
|
export function RoleToggle() {
|
|
151
|
-
const { user, role } = useWormhole(
|
|
140
|
+
const { user, role } = useWormhole(wormholes.session);
|
|
152
141
|
|
|
153
142
|
async function toggle() {
|
|
154
|
-
const
|
|
155
|
-
const result = await actions.updateRole({ role: newRole });
|
|
143
|
+
const result = await actions.updateRole({ role: role === 'admin' ? 'viewer' : 'admin' });
|
|
156
144
|
|
|
157
145
|
if (!result.error) {
|
|
158
|
-
|
|
146
|
+
wormholes.session.set(result.data);
|
|
159
147
|
}
|
|
160
148
|
}
|
|
161
149
|
|
|
@@ -169,83 +157,39 @@ export function RoleToggle() {
|
|
|
169
157
|
|
|
170
158
|
## API
|
|
171
159
|
|
|
172
|
-
### `
|
|
160
|
+
### `wormholes` <sub>proxy, server + client</sub>
|
|
173
161
|
|
|
174
|
-
|
|
162
|
+
The single access point, typed from your registry. `wormholes.cart` returns a `Wormhole<T>`:
|
|
175
163
|
|
|
176
|
-
|
|
177
|
-
|
|
164
|
+
| Method | Description |
|
|
165
|
+
| --------------------------- | ------------------------------------------------------------------------------------------ |
|
|
166
|
+
| `wormholes.x.get()` | Read the current value as `ReadonlyDeep<T>` (server: request scope; client: streamed data) |
|
|
167
|
+
| `wormholes.x.set(data)` | Replace the value and notify all subscribers (client only) |
|
|
168
|
+
| `wormholes.x.subscribe(fn)` | Listen for `set()` updates, returns an unsubscribe function |
|
|
178
169
|
|
|
179
|
-
|
|
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 |
|
|
189
|
-
|
|
190
|
-
### `DeepReadonly<T>`
|
|
191
|
-
|
|
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:
|
|
193
|
-
|
|
194
|
-
```ts
|
|
195
|
-
import type { DeepReadonly } from '@astroscope/wormhole';
|
|
196
|
-
|
|
197
|
-
function isWithinCart(cart: DeepReadonly<Cart>, id: string) {
|
|
198
|
-
return cart.items.some((item) => item.productId === id);
|
|
199
|
-
}
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
### `open(wh, data, fn)` <sub>server only</sub>
|
|
203
|
-
|
|
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';
|
|
170
|
+
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.
|
|
208
171
|
|
|
209
|
-
|
|
210
|
-
```
|
|
172
|
+
### `defineWormhole({ handler, eager? })` <sub>registry only</sub>
|
|
211
173
|
|
|
212
|
-
|
|
174
|
+
Creates a typed wormhole for the registry; the value type is inferred from the handler (or given as `defineWormhole<T>(...)`), the name comes from the registry key. `eager: true` runs the handler on every request instead of only where the build found a reader — for wormholes read by islands the build cannot attribute to a route (components chosen dynamically at render time). Registry entries are full `Wormhole<T>` objects, so server code can also import `wormholes` from your own registry file directly.
|
|
213
175
|
|
|
214
|
-
|
|
215
|
-
return open([
|
|
216
|
-
[cartStore, cart],
|
|
217
|
-
[sessionStore, session],
|
|
218
|
-
], () => next());
|
|
219
|
-
```
|
|
176
|
+
### `openWormholes(wh, data, fn)` <sub>server only</sub>
|
|
220
177
|
|
|
221
|
-
|
|
178
|
+
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
179
|
|
|
223
180
|
### `useWormhole(wh)` <sub>React only</sub>
|
|
224
181
|
|
|
225
|
-
React hook that reads
|
|
182
|
+
React hook that reads a wormhole and re-renders on `set()`. During SSR it reads the request scope, so server and client render identically.
|
|
226
183
|
|
|
227
|
-
|
|
228
|
-
import { useWormhole } from '@astroscope/wormhole/react';
|
|
229
|
-
|
|
230
|
-
const data = useWormhole(myWormhole);
|
|
231
|
-
```
|
|
184
|
+
### `UnwrapWormholes<R>`
|
|
232
185
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
Serializes the current wormhole value into an inline `<script>` tag for client hydration.
|
|
236
|
-
|
|
237
|
-
```astro
|
|
238
|
-
import {WormholeScript} from '@astroscope/wormhole/astro';
|
|
239
|
-
|
|
240
|
-
<WormholeScript wormhole={myWormhole} />
|
|
241
|
-
```
|
|
186
|
+
Exported helper type mapping a registry shape to its value types. The recursively-readonly value type is `ReadonlyDeep<T>` from `@entwico/dash`.
|
|
242
187
|
|
|
243
188
|
## How it works
|
|
244
189
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
4. **`set()`** updates a local store and notifies all subscribers — `useWormhole()` hooks and `subscribe()` callbacks re-render/fire
|
|
190
|
+
At build time the integration scans the code for `wormholes.<name>` reads: server modules are attributed to the routes whose page or endpoint reaches them, client modules to the emitted chunks. It also generates the types for the `wormholes` proxy from your registry. At runtime the middleware runs, in parallel, the handlers of everything the request's route can reach — frontmatter and endpoint reads, the islands the page hydrates (via `@astroscope/node`'s route map), its `<script>` blocks — 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. Routes the build could not attribute (dev, astro's own routes) load everything. On the client, `set()` updates the shared data and notifies subscribers, keeping islands and scripts in sync.
|
|
191
|
+
|
|
192
|
+
Each handler runs under its own `wormhole <name>` span beneath the request span — they run in parallel, and the trace shows which one holds the page's first byte — and records `astro.wormhole.handler.duration` {`astro.wormhole.name`}. A throwing handler is counted on `astro.wormhole.handler.failures` {`astro.wormhole.name`, `error.type`}, logged with its name, and fails the request. All through the adapter's telemetry, so nothing runs without its SDK.
|
|
249
193
|
|
|
250
194
|
## License
|
|
251
195
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"als-CntRqWdO.js","names":[],"sources":["../src/als.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"],"mappings":";;AAIA,MAAa,MAAwD,WACnE,OAAO,IAAI,0BAA0B,OACjC,IAAI,kBAAkB"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { t as als } from "./als-CntRqWdO.js";
|
|
2
|
+
import { t as wormholeKey } from "./key-BqTy1gM3.js";
|
|
3
|
+
//#region src/define.ts
|
|
4
|
+
const ASSIGN = Symbol.for("@astroscope/wormhole.assign");
|
|
5
|
+
const SOURCE = Symbol.for("@astroscope/wormhole.source");
|
|
6
|
+
/**
|
|
7
|
+
* Define a wormhole for the `src/wormholes.ts` registry, actions-style. The
|
|
8
|
+
* handler resolves the value per request; the middleware calls it only for routes
|
|
9
|
+
* the build found a reader on (frontmatter, islands, scripts) — or on every
|
|
10
|
+
* request with `eager: true`. The wormhole's name is the registry key, assigned
|
|
11
|
+
* when the registry is passed to `createWormholeMiddleware()` (or
|
|
12
|
+
* `assignWormholeNames()`).
|
|
13
|
+
*
|
|
14
|
+
* **Security:** wormhole data is serialized into the HTML and sent to the browser.
|
|
15
|
+
* Never store secrets (tokens, API keys, credentials) in a wormhole.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* export const wormholes = {
|
|
20
|
+
* cart: defineWormhole({ handler: (ctx) => ctx.locals.cart }),
|
|
21
|
+
* config: defineWormhole({ handler: () => loadConfig(), eager: true }),
|
|
22
|
+
* };
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
function defineWormhole(definition) {
|
|
26
|
+
const wormhole = createWormhole();
|
|
27
|
+
wormhole[SOURCE] = {
|
|
28
|
+
handler: definition.handler,
|
|
29
|
+
eager: definition.eager ?? false
|
|
30
|
+
};
|
|
31
|
+
return wormhole;
|
|
32
|
+
}
|
|
33
|
+
/** the server-side wormhole object without a handler — the `wormholes` proxy stubs (internal) */
|
|
34
|
+
function createWormhole() {
|
|
35
|
+
let name;
|
|
36
|
+
const requireName = () => {
|
|
37
|
+
if (name === void 0) throw new Error("wormhole is not registered — pass its registry to createWormholeMiddleware()");
|
|
38
|
+
return name;
|
|
39
|
+
};
|
|
40
|
+
const key = () => wormholeKey(requireName());
|
|
41
|
+
return {
|
|
42
|
+
get name() {
|
|
43
|
+
return requireName();
|
|
44
|
+
},
|
|
45
|
+
get key() {
|
|
46
|
+
return key();
|
|
47
|
+
},
|
|
48
|
+
get() {
|
|
49
|
+
const value = als.getStore()?.get(key());
|
|
50
|
+
if (value === void 0) throw new Error(`wormhole "${requireName()}" is not open for this request — is it provided by the wormhole middleware?`);
|
|
51
|
+
return value;
|
|
52
|
+
},
|
|
53
|
+
set() {
|
|
54
|
+
throw new Error(`wormhole "${requireName()}" set() cannot be called on the server — values are request-scoped and provided by the middleware`);
|
|
55
|
+
},
|
|
56
|
+
subscribe() {
|
|
57
|
+
return () => {};
|
|
58
|
+
},
|
|
59
|
+
[ASSIGN](next) {
|
|
60
|
+
if (name !== void 0 && name !== next) throw new Error(`wormhole is registered under two names: "${name}" and "${next}"`);
|
|
61
|
+
name = next;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** the definition a registry entry was created with — undefined for proxy stubs */
|
|
66
|
+
function getWormholeSource(wormhole) {
|
|
67
|
+
return wormhole[SOURCE];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Assign each wormhole its registry key as name. Called by `createWormholeMiddleware()`;
|
|
71
|
+
* exposed for setups that read wormholes on the server without the middleware.
|
|
72
|
+
*/
|
|
73
|
+
function assignWormholeNames(registry) {
|
|
74
|
+
for (const [name, wormhole] of Object.entries(registry)) {
|
|
75
|
+
const assign = wormhole[ASSIGN];
|
|
76
|
+
if (typeof assign !== "function") throw new Error(`registry entry "${name}" is not a wormhole — the registry must contain defineWormhole() values only`);
|
|
77
|
+
assign(name);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
export { getWormholeSource as i, createWormhole as n, defineWormhole as r, assignWormholeNames as t };
|
|
82
|
+
|
|
83
|
+
//# sourceMappingURL=define-1UkPnsXV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-1UkPnsXV.js","names":[],"sources":["../src/define.ts"],"sourcesContent":["import type { ReadonlyDeep } from '@entwico/dash';\nimport { als } from './als.js';\nimport { wormholeKey } from './key.js';\nimport type { Wormhole, WormholeDefinition, WormholeHandler } from './types.js';\n\n// name assignment and the handler must survive dual module instances (vite runner +\n// native), so both ride the wormhole object itself under Symbol.for keys\nconst ASSIGN = Symbol.for('@astroscope/wormhole.assign');\nconst SOURCE = Symbol.for('@astroscope/wormhole.source');\n\n/** how the middleware opens a wormhole: its handler and whether every route loads it */\nexport type WormholeSource = { handler: WormholeHandler<unknown>; eager: boolean };\n\ntype Internal = Wormhole<unknown> & { [ASSIGN](next: string): void; [SOURCE]?: WormholeSource | undefined };\n\n/**\n * Define a wormhole for the `src/wormholes.ts` registry, actions-style. The\n * handler resolves the value per request; the middleware calls it only for routes\n * the build found a reader on (frontmatter, islands, scripts) — or on every\n * request with `eager: true`. The wormhole's name is the registry key, assigned\n * when the registry is passed to `createWormholeMiddleware()` (or\n * `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 *\n * @example\n * ```typescript\n * export const wormholes = {\n * cart: defineWormhole({ handler: (ctx) => ctx.locals.cart }),\n * config: defineWormhole({ handler: () => loadConfig(), eager: true }),\n * };\n * ```\n */\nexport function defineWormhole<T>(definition: WormholeDefinition<T>): Wormhole<T> {\n const wormhole = createWormhole() as Internal;\n\n wormhole[SOURCE] = { handler: definition.handler as WormholeHandler<unknown>, eager: definition.eager ?? false };\n\n return wormhole as Wormhole<T>;\n}\n\n/** the server-side wormhole object without a handler — the `wormholes` proxy stubs (internal) */\nexport function createWormhole<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(): ReadonlyDeep<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 ReadonlyDeep<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/** the definition a registry entry was created with — undefined for proxy stubs */\nexport function getWormholeSource<T>(wormhole: Wormhole<T>): WormholeSource | undefined {\n return (wormhole as unknown as Internal)[SOURCE];\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":";;;AAOA,MAAM,SAAS,OAAO,IAAI,6BAA6B;AACvD,MAAM,SAAS,OAAO,IAAI,6BAA6B;;;;;;;;;;;;;;;;;;;;AA0BvD,SAAgB,eAAkB,YAAgD;CAChF,MAAM,WAAW,eAAe;CAEhC,SAAS,UAAU;EAAE,SAAS,WAAW;EAAqC,OAAO,WAAW,SAAS;CAAM;CAE/G,OAAO;AACT;;AAGA,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;;AAGA,SAAgB,kBAAqB,UAAmD;CACtF,OAAQ,SAAiC;AAC3C;;;;;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 WormholeMap, i as WormholeHandler, n as Wormhole, o as WormholeRegistry, r as WormholeDefinition, t as UnwrapWormholes } from "./types-DiWa8aQg.js";
|
|
2
|
+
//#region src/index.browser.d.ts
|
|
3
|
+
export declare const wormholes: WormholeMap;
|
|
4
|
+
export declare function defineWormhole(): never;
|
|
5
|
+
export declare function assignWormholeNames(): never;
|
|
6
|
+
export default function wormholeIntegration(): never;
|
|
7
|
+
//#endregion
|
|
8
|
+
export type { UnwrapWormholes, Wormhole, WormholeDefinition, WormholeHandler, WormholeMap, WormholeRegistry };
|
|
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":";;qBAyEa,WAAW;wBAMR;wBAMA;wBAIQ"}
|
|
@@ -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 type { ReadonlyDeep } from '@entwico/dash';\nimport { wormholeKey } from './key.js';\nimport type { Wormhole, WormholeMap } from './types.js';\n\nexport type {\n UnwrapWormholes,\n Wormhole,\n WormholeDefinition,\n WormholeHandler,\n WormholeMap,\n WormholeRegistry,\n} 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(): ReadonlyDeep<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":";;AAuBA,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,61 @@
|
|
|
1
|
-
import { n as Wormhole, t as
|
|
2
|
-
|
|
1
|
+
import { a as WormholeMap, i as WormholeHandler, n as Wormhole, o as WormholeRegistry, r as WormholeDefinition, t as UnwrapWormholes } from "./types-DiWa8aQg.js";
|
|
2
|
+
import { ExcludePattern } from "@astroscope/node/excludes";
|
|
3
|
+
import { AstroIntegration } from "astro";
|
|
4
|
+
//#region src/integration.d.ts
|
|
5
|
+
type WormholeIntegrationOptions = {
|
|
6
|
+
/** patterns the middleware skips — no handler runs, nothing is delivered; defaults to RECOMMENDED_EXCLUDES */
|
|
7
|
+
exclude?: ExcludePattern[] | undefined;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Astro integration for wormholes with per-route loading and per-island delivery.
|
|
11
|
+
*
|
|
12
|
+
* Resolves the `src/wormholes.ts` registry and exposes it to the middleware as a
|
|
13
|
+
* virtual module, injects the middleware after the project's own (so handlers see
|
|
14
|
+
* everything earlier middleware put on `locals`), registers the vite plugin that
|
|
15
|
+
* scans server and client code for `wormholes.<name>` accesses and maps them onto
|
|
16
|
+
* routes and chunks, and generates the registry type stub that types the
|
|
17
|
+
* `wormholes` proxy. Delivery happens at runtime: the middleware runs the handlers
|
|
18
|
+
* the route needs, and the islands emitter (via `@astroscope/node`) writes each
|
|
19
|
+
* island's slice before its tag.
|
|
20
|
+
*/
|
|
21
|
+
export default function wormholeIntegration(options?: WormholeIntegrationOptions): AstroIntegration;
|
|
22
|
+
//#endregion
|
|
3
23
|
//#region src/define.d.ts
|
|
4
24
|
/**
|
|
5
|
-
* Define a wormhole
|
|
25
|
+
* Define a wormhole for the `src/wormholes.ts` registry, actions-style. The
|
|
26
|
+
* handler resolves the value per request; the middleware calls it only for routes
|
|
27
|
+
* the build found a reader on (frontmatter, islands, scripts) — or on every
|
|
28
|
+
* request with `eager: true`. The wormhole's name is the registry key, assigned
|
|
29
|
+
* when the registry is passed to `createWormholeMiddleware()` (or
|
|
30
|
+
* `assignWormholeNames()`).
|
|
6
31
|
*
|
|
7
32
|
* **Security:** wormhole data is serialized into the HTML and sent to the browser.
|
|
8
33
|
* Never store secrets (tokens, API keys, credentials) in a wormhole.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* export const wormholes = {
|
|
38
|
+
* cart: defineWormhole({ handler: (ctx) => ctx.locals.cart }),
|
|
39
|
+
* config: defineWormhole({ handler: () => loadConfig(), eager: true }),
|
|
40
|
+
* };
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare function defineWormhole<T>(definition: WormholeDefinition<T>): Wormhole<T>;
|
|
44
|
+
/**
|
|
45
|
+
* Assign each wormhole its registry key as name. Called by `createWormholeMiddleware()`;
|
|
46
|
+
* exposed for setups that read wormholes on the server without the middleware.
|
|
47
|
+
*/
|
|
48
|
+
export declare function assignWormholeNames(registry: Record<string, object>): void;
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/index.d.ts
|
|
51
|
+
/**
|
|
52
|
+
* Access wormholes by name — `wormholes.cart.get()`. This is the server side of the
|
|
53
|
+
* proxy (the browser build ships its own): values come from the request's
|
|
54
|
+
* wormhole middleware scope, so it works in frontmatter, endpoints, and
|
|
55
|
+
* server-rendered islands alike. Typed via the registry type stub the integration
|
|
56
|
+
* generates from `src/wormholes.ts`.
|
|
9
57
|
*/
|
|
10
|
-
declare
|
|
58
|
+
export declare const wormholes: WormholeMap;
|
|
11
59
|
//#endregion
|
|
12
|
-
export {
|
|
60
|
+
export type { UnwrapWormholes, Wormhole, WormholeDefinition, WormholeHandler, WormholeIntegrationOptions, WormholeMap, WormholeRegistry };
|
|
13
61
|
//# 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":";;;;KAQY;;EAEV,UAAU;;;;;;;;;;;;;;wBAeY,oBAAoB,UAAS,6BAAkC;;;;;;;;;;;;;;;;;;;;;;wBCSvE,eAAe,GAAG,YAAY,mBAAmB,KAAK,SAAS;;;;;wBA6E/D,oBAAoB,UAAU;;;;;;;;;;qBC1EjC,WAAW"}
|