@rshono/core 1.0.0-rc.1 → 1.0.0-rc.10
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 +202 -159
- package/dist/builder/page-files.d.ts.map +1 -1
- package/dist/builder/page-files.js +7 -3
- package/dist/builder/page-files.js.map +1 -1
- package/dist/builder/public-env.d.ts +6 -0
- package/dist/builder/public-env.d.ts.map +1 -1
- package/dist/builder/public-env.js +6 -0
- package/dist/builder/public-env.js.map +1 -1
- package/dist/builder/rspack-config.d.ts +3 -3
- package/dist/builder/rspack-config.d.ts.map +1 -1
- package/dist/builder/rspack-config.js +18 -21
- package/dist/builder/rspack-config.js.map +1 -1
- package/dist/cli/build.d.ts +2 -2
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/dev.d.ts +2 -2
- package/dist/cli/dev.d.ts.map +1 -1
- package/dist/cli/dev.js +63 -35
- package/dist/cli/dev.js.map +1 -1
- package/dist/cli/index.js +10 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/config.d.ts +58 -70
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +17 -1
- package/dist/config.js.map +1 -1
- package/dist/deploy/cloudflare/build.js +1 -1
- package/dist/deploy/cloudflare/build.js.map +1 -1
- package/dist/deploy/cloudflare/runtime.d.ts.map +1 -1
- package/dist/deploy/cloudflare/runtime.js +7 -37
- package/dist/deploy/cloudflare/runtime.js.map +1 -1
- package/dist/deploy/contract.d.ts +22 -16
- package/dist/deploy/contract.d.ts.map +1 -1
- package/dist/deploy/contract.js.map +1 -1
- package/dist/deploy/filesystem.d.ts +1 -1
- package/dist/deploy/filesystem.d.ts.map +1 -1
- package/dist/deploy/filesystem.js +7 -10
- package/dist/deploy/filesystem.js.map +1 -1
- package/dist/deploy/node/runtime.d.ts +4 -0
- package/dist/deploy/node/runtime.d.ts.map +1 -1
- package/dist/deploy/node/runtime.js +24 -3
- package/dist/deploy/node/runtime.js.map +1 -1
- package/dist/deploy/presets.d.ts +3 -3
- package/dist/deploy/presets.d.ts.map +1 -1
- package/dist/deploy/presets.js +13 -30
- package/dist/deploy/presets.js.map +1 -1
- package/dist/deploy/vercel/runtime.d.ts.map +1 -1
- package/dist/deploy/vercel/runtime.js +0 -3
- package/dist/deploy/vercel/runtime.js.map +1 -1
- package/dist/index.d.ts +6 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +85 -48
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +4 -6
- package/dist/router.js.map +1 -1
- package/dist/runtime/boundaries.d.ts +39 -25
- package/dist/runtime/boundaries.d.ts.map +1 -1
- package/dist/runtime/boundaries.js +22 -17
- package/dist/runtime/boundaries.js.map +1 -1
- package/dist/runtime/client.d.ts +9 -8
- package/dist/runtime/client.d.ts.map +1 -1
- package/dist/runtime/client.js +9 -8
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/context.d.ts +216 -70
- package/dist/runtime/context.d.ts.map +1 -1
- package/dist/runtime/context.js +299 -93
- package/dist/runtime/context.js.map +1 -1
- package/dist/runtime/control.d.ts.map +1 -1
- package/dist/runtime/control.js +7 -0
- package/dist/runtime/control.js.map +1 -1
- package/dist/runtime/dev-protocol.d.ts.map +1 -1
- package/dist/runtime/dev-protocol.js.map +1 -1
- package/dist/runtime/entry.client.d.ts +4 -0
- package/dist/runtime/entry.client.d.ts.map +1 -1
- package/dist/runtime/entry.client.js +190 -154
- package/dist/runtime/entry.client.js.map +1 -1
- package/dist/runtime/entry.rsc.d.ts.map +1 -1
- package/dist/runtime/entry.rsc.js +145 -159
- package/dist/runtime/entry.rsc.js.map +1 -1
- package/dist/runtime/entry.ssr.d.ts +9 -1
- package/dist/runtime/entry.ssr.d.ts.map +1 -1
- package/dist/runtime/entry.ssr.js +36 -18
- package/dist/runtime/entry.ssr.js.map +1 -1
- package/dist/runtime/flight-inject.d.ts +31 -0
- package/dist/runtime/flight-inject.d.ts.map +1 -0
- package/dist/runtime/flight-inject.js +221 -0
- package/dist/runtime/flight-inject.js.map +1 -0
- package/dist/runtime/navigation.d.ts +20 -38
- package/dist/runtime/navigation.d.ts.map +1 -1
- package/dist/runtime/navigation.js +10 -53
- package/dist/runtime/navigation.js.map +1 -1
- package/dist/runtime/request.d.ts +6 -0
- package/dist/runtime/request.d.ts.map +1 -1
- package/dist/runtime/request.js +8 -0
- package/dist/runtime/request.js.map +1 -1
- package/dist/runtime/server.d.ts +7 -12
- package/dist/runtime/server.d.ts.map +1 -1
- package/dist/runtime/server.js +15 -12
- package/dist/runtime/server.js.map +1 -1
- package/dist/server/headers.d.ts +9 -9
- package/dist/server/headers.js +9 -9
- package/dist/server/headers.js.map +1 -1
- package/dist/server/load-config.d.ts +2 -2
- package/dist/server/load-config.d.ts.map +1 -1
- package/dist/server/load-config.js +22 -11
- package/dist/server/load-config.js.map +1 -1
- package/dist/server/prerendered.d.ts +43 -15
- package/dist/server/prerendered.d.ts.map +1 -1
- package/dist/server/prerendered.js +47 -0
- package/dist/server/prerendered.js.map +1 -1
- package/dist/server/server-config.d.ts +13 -39
- package/dist/server/server-config.d.ts.map +1 -1
- package/dist/server/server-config.js +5 -74
- package/dist/server/server-config.js.map +1 -1
- package/dist/server/ssg.d.ts +2 -2
- package/dist/server/ssg.d.ts.map +1 -1
- package/dist/server/ssg.js +21 -34
- package/dist/server/ssg.js.map +1 -1
- package/package.json +13 -16
- package/dist/deploy/bun/runtime.d.ts +0 -11
- package/dist/deploy/bun/runtime.d.ts.map +0 -1
- package/dist/deploy/bun/runtime.js +0 -22
- package/dist/deploy/bun/runtime.js.map +0 -1
- package/dist/deploy/deno/runtime.d.ts +0 -11
- package/dist/deploy/deno/runtime.d.ts.map +0 -1
- package/dist/deploy/deno/runtime.js +0 -16
- package/dist/deploy/deno/runtime.js.map +0 -1
- package/dist/deploy/listen.d.ts +0 -20
- package/dist/deploy/listen.d.ts.map +0 -1
- package/dist/deploy/listen.js +0 -24
- package/dist/deploy/listen.js.map +0 -1
- package/dist/deploy/netlify/build.d.ts +0 -8
- package/dist/deploy/netlify/build.d.ts.map +0 -1
- package/dist/deploy/netlify/build.js +0 -52
- package/dist/deploy/netlify/build.js.map +0 -1
- package/dist/deploy/netlify/runtime.d.ts +0 -13
- package/dist/deploy/netlify/runtime.d.ts.map +0 -1
- package/dist/deploy/netlify/runtime.js +0 -24
- package/dist/deploy/netlify/runtime.js.map +0 -1
- package/dist/server/compress.d.ts +0 -15
- package/dist/server/compress.d.ts.map +0 -1
- package/dist/server/compress.js +0 -76
- package/dist/server/compress.js.map +0 -1
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carrying the flight payload inside the HTML document, so the browser hydrates from bytes it
|
|
3
|
+
* already has rather than fetching the page a second time.
|
|
4
|
+
*
|
|
5
|
+
* The wire format is `rsc-html-stream`'s — a run of
|
|
6
|
+
* `<script>(self.__FLIGHT_DATA||=[]).push("…")</script>` tags — but the implementation is
|
|
7
|
+
* first-party, because that package's `injectRSCPayload` tests each HTML chunk for the document
|
|
8
|
+
* trailer with `endsWith`. React's byte writer packs its output into 2 kB views and splits any write
|
|
9
|
+
* that straddles one, so `</body></html>` can arrive as two chunks; upstream then fails to hold it
|
|
10
|
+
* back, and the document ends up with two trailers and the payload script after the first of them.
|
|
11
|
+
* A page's byte layout is deterministic, so that is not an intermittent fault — an unlucky page is
|
|
12
|
+
* malformed on every request. `test/unit.test.mjs` pins the split-trailer cases.
|
|
13
|
+
*
|
|
14
|
+
* The reader for this format is `readFlightPayload` in `entry.client.tsx`.
|
|
15
|
+
*/
|
|
16
|
+
const encoder = new TextEncoder();
|
|
17
|
+
/** What React closes an `<html>` document with, and what this module re-emits after the last payload script. */
|
|
18
|
+
const TRAILER = '</body></html>';
|
|
19
|
+
const TRAILER_BYTES = encoder.encode(TRAILER);
|
|
20
|
+
const hasSetImmediate = typeof setImmediate === 'function';
|
|
21
|
+
const schedule = hasSetImmediate ? setImmediate : (fn) => setTimeout(fn, 0);
|
|
22
|
+
const unschedule = (handle) => {
|
|
23
|
+
if (hasSetImmediate)
|
|
24
|
+
clearImmediate(handle);
|
|
25
|
+
else
|
|
26
|
+
clearTimeout(handle);
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Escapes the two sequences that would end a `<script>` element early.
|
|
30
|
+
*
|
|
31
|
+
* Guarded by an `includes('<')` because a flight payload usually has no `<` in it at all, and the
|
|
32
|
+
* scan is far cheaper than two regex passes over 30 kB. `</script` becomes `</\script` rather than
|
|
33
|
+
* `<\/script`, which would break the valid JS `0</script/` (a regexp literal).
|
|
34
|
+
*/
|
|
35
|
+
function escapeScript(script) {
|
|
36
|
+
return script.includes('<') ? script.replace(/<!--/g, '<\\!--').replace(/<\/(script)/gi, '</\\$1') : script;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The bytes of `chunk` as a latin1 string, which is the form `btoa` takes.
|
|
40
|
+
*
|
|
41
|
+
* Sliced rather than `String.fromCharCode(...chunk)`, which passes one argument per byte and
|
|
42
|
+
* overflows the call stack on a chunk of any size. The slice width is a stack-safety choice, not a
|
|
43
|
+
* tuned one — this is only reached for a chunk that split a multi-byte character.
|
|
44
|
+
*/
|
|
45
|
+
function latin1(chunk) {
|
|
46
|
+
let out = '';
|
|
47
|
+
for (let at = 0; at < chunk.length; at += 8192)
|
|
48
|
+
out += String.fromCharCode(...chunk.subarray(at, at + 8192));
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
/** Whether `buffer`'s first `length` bytes end with the document trailer. */
|
|
52
|
+
function endsWithTrailer(buffer, length) {
|
|
53
|
+
if (length < TRAILER_BYTES.length)
|
|
54
|
+
return false;
|
|
55
|
+
const from = length - TRAILER_BYTES.length;
|
|
56
|
+
for (let i = 0; i < TRAILER_BYTES.length; i++) {
|
|
57
|
+
if (buffer[from + i] !== TRAILER_BYTES[i])
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
export function injectFlightPayload(rscStream, options = {}) {
|
|
63
|
+
const { nonce, onDone } = options;
|
|
64
|
+
const scriptOpen = `<script${nonce ? ` nonce="${nonce}"` : ''}>(self.__FLIGHT_DATA||=[]).push(`;
|
|
65
|
+
const scriptClose = ')</script>';
|
|
66
|
+
const { promise: flightWritten, resolve: flightDone } = Promise.withResolvers();
|
|
67
|
+
let startedFlight = false;
|
|
68
|
+
const batch = [];
|
|
69
|
+
let boundary = null;
|
|
70
|
+
/**
|
|
71
|
+
* Set once the consumer has gone away, so nothing downstream tries to enqueue into a readable that
|
|
72
|
+
* can no longer take it.
|
|
73
|
+
*
|
|
74
|
+
* It cannot simply be the `cancel` hook that sets this. Per the Streams standard, cancelling the
|
|
75
|
+
* readable *after* the close algorithm has started returns the pending finish promise without
|
|
76
|
+
* running the transformer's `cancel` at all — and `flush` awaiting the whole flight payload is
|
|
77
|
+
* precisely that window. So a failed enqueue is also treated as the signal, wherever one happens.
|
|
78
|
+
*/
|
|
79
|
+
let cancelled = false;
|
|
80
|
+
/** Held so {@link cancelled} can release the teed RSC branch rather than leaving it to be pumped. */
|
|
81
|
+
let flightReader = null;
|
|
82
|
+
/**
|
|
83
|
+
* Emits the HTML buffered since the last boundary, holding back the document trailer for
|
|
84
|
+
* {@link TransformStream.flush} to re-emit after the payload scripts.
|
|
85
|
+
*
|
|
86
|
+
* The batch is joined before the trailer is looked for, so a trailer React split across two views
|
|
87
|
+
* is still found — that is the whole reason this module is not `rsc-html-stream/server`. React
|
|
88
|
+
* writes its final flush in one synchronous run, so every chunk of the trailer lands in the same
|
|
89
|
+
* batch; a trailer split *across* batches is not a shape React produces.
|
|
90
|
+
*/
|
|
91
|
+
function emitBatch(controller) {
|
|
92
|
+
boundary = null;
|
|
93
|
+
let total = 0;
|
|
94
|
+
for (const chunk of batch)
|
|
95
|
+
total += chunk.byteLength;
|
|
96
|
+
if (total === 0) {
|
|
97
|
+
batch.length = 0;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const joined = new Uint8Array(total);
|
|
101
|
+
let at = 0;
|
|
102
|
+
for (const chunk of batch) {
|
|
103
|
+
joined.set(chunk, at);
|
|
104
|
+
at += chunk.byteLength;
|
|
105
|
+
}
|
|
106
|
+
batch.length = 0;
|
|
107
|
+
const end = endsWithTrailer(joined, total) ? total - TRAILER_BYTES.length : total;
|
|
108
|
+
if (end > 0)
|
|
109
|
+
controller.enqueue(joined.subarray(0, end));
|
|
110
|
+
}
|
|
111
|
+
async function writeFlight(controller) {
|
|
112
|
+
const reader = (flightReader = rscStream.getReader());
|
|
113
|
+
// `fatal`, so a chunk that split a multi-byte character throws rather than emitting U+FFFD and
|
|
114
|
+
// corrupting the payload — the catch below falls back to a byte-exact encoding for it.
|
|
115
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
116
|
+
const push = (literal) => controller.enqueue(encoder.encode(scriptOpen + literal + scriptClose));
|
|
117
|
+
for (;;) {
|
|
118
|
+
if (cancelled)
|
|
119
|
+
return;
|
|
120
|
+
const { done, value } = await reader.read();
|
|
121
|
+
if (done)
|
|
122
|
+
break;
|
|
123
|
+
// Only the *decode* is guarded. Wrapping the `push` in the same `try` conflated a split
|
|
124
|
+
// multi-byte character with a controller nobody is reading any more, and answered the second by
|
|
125
|
+
// re-encoding the chunk and enqueueing it again — which throws in turn, out of the catch.
|
|
126
|
+
let literal;
|
|
127
|
+
try {
|
|
128
|
+
literal = escapeScript(JSON.stringify(decoder.decode(value, { stream: true })));
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
literal = `Uint8Array.from(atob(${JSON.stringify(btoa(latin1(value)))}), m => m.codePointAt(0))`;
|
|
132
|
+
}
|
|
133
|
+
if (cancelled)
|
|
134
|
+
return;
|
|
135
|
+
// A failed enqueue means the consumer is gone. Stop pumping and release the RSC branch, so
|
|
136
|
+
// `flush` unparks now rather than whenever the flight payload would have ended on its own.
|
|
137
|
+
try {
|
|
138
|
+
push(literal);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
cancelled = true;
|
|
142
|
+
reader.cancel().catch(() => { });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (cancelled)
|
|
147
|
+
return;
|
|
148
|
+
const remaining = decoder.decode();
|
|
149
|
+
if (remaining.length)
|
|
150
|
+
push(escapeScript(JSON.stringify(remaining)));
|
|
151
|
+
}
|
|
152
|
+
const transformer = {
|
|
153
|
+
transform(chunk, controller) {
|
|
154
|
+
batch.push(chunk);
|
|
155
|
+
if (boundary)
|
|
156
|
+
return;
|
|
157
|
+
// A macrotask, not a microtask: React writes a whole flush into its stream in one synchronous
|
|
158
|
+
// run, but `pipeThrough` delivers those chunks to us one microtask at a time. Only a task
|
|
159
|
+
// boundary guarantees the flush has arrived in full — a script injected between two of its
|
|
160
|
+
// chunks would land inside a tag.
|
|
161
|
+
boundary = schedule(() => {
|
|
162
|
+
try {
|
|
163
|
+
emitBatch(controller);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
controller.error(error);
|
|
167
|
+
flightDone();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (!startedFlight) {
|
|
171
|
+
startedFlight = true;
|
|
172
|
+
writeFlight(controller)
|
|
173
|
+
.catch((error) => controller.error(error))
|
|
174
|
+
.then(flightDone);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
async flush(controller) {
|
|
179
|
+
await flightWritten;
|
|
180
|
+
// That await spans the entire flight payload, and the consumer can go away inside it — a
|
|
181
|
+
// browser's stop button, a navigation away, a proxy timeout. `cancel` below is *not* what tells
|
|
182
|
+
// us so (see `cancelled`), which leaves the enqueue throwing `ERR_INVALID_STATE` as the only
|
|
183
|
+
// signal. Unguarded it rejects `flush`, and nothing owns that rejection: it surfaces as an
|
|
184
|
+
// unhandled one and, where the host does not swallow it, takes the process down.
|
|
185
|
+
try {
|
|
186
|
+
if (boundary) {
|
|
187
|
+
unschedule(boundary);
|
|
188
|
+
emitBatch(controller);
|
|
189
|
+
}
|
|
190
|
+
if (!cancelled)
|
|
191
|
+
controller.enqueue(encoder.encode(TRAILER));
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// Nowhere left to put the trailer. A response the client abandoned is not a fault.
|
|
195
|
+
cancelled = true;
|
|
196
|
+
flightReader?.cancel().catch(() => { });
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
// Unconditional, and the reason this is a `finally`: `onDone` is what releases the abort
|
|
200
|
+
// forwarder in `renderComponent`, so it has to run however the response ended.
|
|
201
|
+
onDone?.();
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
cancel(reason) {
|
|
205
|
+
cancelled = true;
|
|
206
|
+
if (boundary) {
|
|
207
|
+
unschedule(boundary);
|
|
208
|
+
boundary = null;
|
|
209
|
+
}
|
|
210
|
+
batch.length = 0;
|
|
211
|
+
// Without this the teed RSC branch keeps being pumped for a response nobody will read, and the
|
|
212
|
+
// tee's other half buffers every chunk waiting for this one to catch up.
|
|
213
|
+
flightReader?.cancel(reason).catch(() => { });
|
|
214
|
+
// Unparks `flush` if it is waiting on a payload that will now never arrive.
|
|
215
|
+
flightDone();
|
|
216
|
+
onDone?.();
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
return new TransformStream(transformer);
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=flight-inject.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flight-inject.js","sourceRoot":"","sources":["../../src/runtime/flight-inject.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Carrying the flight payload inside the HTML document, so the browser hydrates from bytes it\n * already has rather than fetching the page a second time.\n *\n * The wire format is `rsc-html-stream`'s — a run of\n * `<script>(self.__FLIGHT_DATA||=[]).push(\"…\")</script>` tags — but the implementation is\n * first-party, because that package's `injectRSCPayload` tests each HTML chunk for the document\n * trailer with `endsWith`. React's byte writer packs its output into 2 kB views and splits any write\n * that straddles one, so `</body></html>` can arrive as two chunks; upstream then fails to hold it\n * back, and the document ends up with two trailers and the payload script after the first of them.\n * A page's byte layout is deterministic, so that is not an intermittent fault — an unlucky page is\n * malformed on every request. `test/unit.test.mjs` pins the split-trailer cases.\n *\n * The reader for this format is `readFlightPayload` in `entry.client.tsx`.\n */\n\n/**\n * {@link Transformer} plus the `cancel` hook the Streams standard added for a cancelled readable side.\n *\n * Node calls it (verified on 22.x) but the bundled lib types have not caught up, so it is declared\n * here rather than reached for with an `any`. It matters because `cancel` is the only notification\n * that a response ended *without* finishing, and both users of it release a listener that would\n * otherwise outlive the request.\n */\nexport type CancellableTransformer<I, O> = Transformer<I, O> & { cancel?: (reason?: unknown) => void };\n\nconst encoder = new TextEncoder();\n\n/** What React closes an `<html>` document with, and what this module re-emits after the last payload script. */\nconst TRAILER = '</body></html>';\nconst TRAILER_BYTES = encoder.encode(TRAILER);\n\n/**\n * A macrotask boundary. `setImmediate` where there is one (Node, Bun, Deno's node compat), which is\n * the current turn's check phase rather than a timer; `setTimeout` is the portable fallback that\n * every other runtime has. A Node timer has a 1ms floor and every HTML flush would pay it — worth\n * the two lines: uncontended time-to-last-byte on a 30 kB payload, 4.7ms → 3.0ms.\n */\ntype TaskHandle = ReturnType<typeof setTimeout> | ReturnType<typeof setImmediate>;\nconst hasSetImmediate = typeof setImmediate === 'function';\nconst schedule: (fn: () => void) => TaskHandle = hasSetImmediate ? setImmediate : (fn) => setTimeout(fn, 0);\nconst unschedule = (handle: TaskHandle): void => {\n if (hasSetImmediate) clearImmediate(handle as ReturnType<typeof setImmediate>);\n else clearTimeout(handle as ReturnType<typeof setTimeout>);\n};\n\n/**\n * Escapes the two sequences that would end a `<script>` element early.\n *\n * Guarded by an `includes('<')` because a flight payload usually has no `<` in it at all, and the\n * scan is far cheaper than two regex passes over 30 kB. `</script` becomes `</\\script` rather than\n * `<\\/script`, which would break the valid JS `0</script/` (a regexp literal).\n */\nfunction escapeScript(script: string): string {\n return script.includes('<') ? script.replace(/<!--/g, '<\\\\!--').replace(/<\\/(script)/gi, '</\\\\$1') : script;\n}\n\n/**\n * The bytes of `chunk` as a latin1 string, which is the form `btoa` takes.\n *\n * Sliced rather than `String.fromCharCode(...chunk)`, which passes one argument per byte and\n * overflows the call stack on a chunk of any size. The slice width is a stack-safety choice, not a\n * tuned one — this is only reached for a chunk that split a multi-byte character.\n */\nfunction latin1(chunk: Uint8Array): string {\n let out = '';\n for (let at = 0; at < chunk.length; at += 8192) out += String.fromCharCode(...chunk.subarray(at, at + 8192));\n return out;\n}\n\n/** Whether `buffer`'s first `length` bytes end with the document trailer. */\nfunction endsWithTrailer(buffer: Uint8Array, length: number): boolean {\n if (length < TRAILER_BYTES.length) return false;\n const from = length - TRAILER_BYTES.length;\n for (let i = 0; i < TRAILER_BYTES.length; i++) {\n if (buffer[from + i] !== TRAILER_BYTES[i]) return false;\n }\n return true;\n}\n\nexport function injectFlightPayload(\n rscStream: ReadableStream<Uint8Array>,\n options: { nonce?: string; onDone?: () => void } = {},\n): TransformStream<Uint8Array, Uint8Array> {\n const { nonce, onDone } = options;\n const scriptOpen = `<script${nonce ? ` nonce=\"${nonce}\"` : ''}>(self.__FLIGHT_DATA||=[]).push(`;\n const scriptClose = ')</script>';\n\n const { promise: flightWritten, resolve: flightDone } = Promise.withResolvers<void>();\n let startedFlight = false;\n\n const batch: Uint8Array[] = [];\n let boundary: TaskHandle | null = null;\n\n /**\n * Set once the consumer has gone away, so nothing downstream tries to enqueue into a readable that\n * can no longer take it.\n *\n * It cannot simply be the `cancel` hook that sets this. Per the Streams standard, cancelling the\n * readable *after* the close algorithm has started returns the pending finish promise without\n * running the transformer's `cancel` at all — and `flush` awaiting the whole flight payload is\n * precisely that window. So a failed enqueue is also treated as the signal, wherever one happens.\n */\n let cancelled = false;\n /** Held so {@link cancelled} can release the teed RSC branch rather than leaving it to be pumped. */\n let flightReader: ReadableStreamDefaultReader<Uint8Array> | null = null;\n\n /**\n * Emits the HTML buffered since the last boundary, holding back the document trailer for\n * {@link TransformStream.flush} to re-emit after the payload scripts.\n *\n * The batch is joined before the trailer is looked for, so a trailer React split across two views\n * is still found — that is the whole reason this module is not `rsc-html-stream/server`. React\n * writes its final flush in one synchronous run, so every chunk of the trailer lands in the same\n * batch; a trailer split *across* batches is not a shape React produces.\n */\n function emitBatch(controller: TransformStreamDefaultController<Uint8Array>): void {\n boundary = null;\n let total = 0;\n for (const chunk of batch) total += chunk.byteLength;\n if (total === 0) {\n batch.length = 0;\n return;\n }\n\n const joined = new Uint8Array(total);\n let at = 0;\n for (const chunk of batch) {\n joined.set(chunk, at);\n at += chunk.byteLength;\n }\n batch.length = 0;\n\n const end = endsWithTrailer(joined, total) ? total - TRAILER_BYTES.length : total;\n if (end > 0) controller.enqueue(joined.subarray(0, end));\n }\n\n async function writeFlight(controller: TransformStreamDefaultController<Uint8Array>): Promise<void> {\n const reader = (flightReader = rscStream.getReader());\n // `fatal`, so a chunk that split a multi-byte character throws rather than emitting U+FFFD and\n // corrupting the payload — the catch below falls back to a byte-exact encoding for it.\n const decoder = new TextDecoder('utf-8', { fatal: true });\n const push = (literal: string) => controller.enqueue(encoder.encode(scriptOpen + literal + scriptClose));\n for (;;) {\n if (cancelled) return;\n const { done, value } = await reader.read();\n if (done) break;\n // Only the *decode* is guarded. Wrapping the `push` in the same `try` conflated a split\n // multi-byte character with a controller nobody is reading any more, and answered the second by\n // re-encoding the chunk and enqueueing it again — which throws in turn, out of the catch.\n let literal: string;\n try {\n literal = escapeScript(JSON.stringify(decoder.decode(value, { stream: true })));\n } catch {\n literal = `Uint8Array.from(atob(${JSON.stringify(btoa(latin1(value)))}), m => m.codePointAt(0))`;\n }\n if (cancelled) return;\n // A failed enqueue means the consumer is gone. Stop pumping and release the RSC branch, so\n // `flush` unparks now rather than whenever the flight payload would have ended on its own.\n try {\n push(literal);\n } catch {\n cancelled = true;\n reader.cancel().catch(() => {});\n return;\n }\n }\n if (cancelled) return;\n const remaining = decoder.decode();\n if (remaining.length) push(escapeScript(JSON.stringify(remaining)));\n }\n\n const transformer: CancellableTransformer<Uint8Array, Uint8Array> = {\n transform(chunk, controller) {\n batch.push(chunk);\n if (boundary) return;\n // A macrotask, not a microtask: React writes a whole flush into its stream in one synchronous\n // run, but `pipeThrough` delivers those chunks to us one microtask at a time. Only a task\n // boundary guarantees the flush has arrived in full — a script injected between two of its\n // chunks would land inside a tag.\n boundary = schedule(() => {\n try {\n emitBatch(controller);\n } catch (error) {\n controller.error(error);\n flightDone();\n return;\n }\n if (!startedFlight) {\n startedFlight = true;\n writeFlight(controller)\n .catch((error) => controller.error(error))\n .then(flightDone);\n }\n });\n },\n async flush(controller) {\n await flightWritten;\n // That await spans the entire flight payload, and the consumer can go away inside it — a\n // browser's stop button, a navigation away, a proxy timeout. `cancel` below is *not* what tells\n // us so (see `cancelled`), which leaves the enqueue throwing `ERR_INVALID_STATE` as the only\n // signal. Unguarded it rejects `flush`, and nothing owns that rejection: it surfaces as an\n // unhandled one and, where the host does not swallow it, takes the process down.\n try {\n if (boundary) {\n unschedule(boundary);\n emitBatch(controller);\n }\n if (!cancelled) controller.enqueue(encoder.encode(TRAILER));\n } catch {\n // Nowhere left to put the trailer. A response the client abandoned is not a fault.\n cancelled = true;\n flightReader?.cancel().catch(() => {});\n } finally {\n // Unconditional, and the reason this is a `finally`: `onDone` is what releases the abort\n // forwarder in `renderComponent`, so it has to run however the response ended.\n onDone?.();\n }\n },\n cancel(reason) {\n cancelled = true;\n if (boundary) {\n unschedule(boundary);\n boundary = null;\n }\n batch.length = 0;\n // Without this the teed RSC branch keeps being pumped for a response nobody will read, and the\n // tee's other half buffers every chunk waiting for this one to catch up.\n flightReader?.cancel(reason).catch(() => {});\n // Unparks `flush` if it is waiting on a payload that will now never arrive.\n flightDone();\n onDone?.();\n },\n };\n return new TransformStream<Uint8Array, Uint8Array>(transformer);\n}\n"]}
|
|
@@ -5,23 +5,27 @@ import { type ReactNode } from 'react';
|
|
|
5
5
|
* `push` / `replace` / `refresh` are **soft** navigations: the new page's flight
|
|
6
6
|
* payload is fetched and applied in place, so client component state outside the
|
|
7
7
|
* changed subtree survives. Off-site or non-HTTP hrefs fall back to a full load.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* const { router } = useNavigation();
|
|
12
|
+
* router.push('/dashboard'); // navigate, new history entry
|
|
13
|
+
* router.replace('/login'); // navigate, no new entry
|
|
14
|
+
* router.refresh(); // re-run this route's server components
|
|
15
|
+
* ```
|
|
8
16
|
*/
|
|
9
|
-
export interface
|
|
17
|
+
export interface NavigationRouter {
|
|
10
18
|
/** Navigates to `href` and pushes a new history entry. */
|
|
11
19
|
push(href: string): void;
|
|
12
20
|
/** Navigates to `href`, replacing the current history entry instead of adding one. */
|
|
13
21
|
replace(href: string): void;
|
|
14
|
-
/** Goes back one history entry — `history.back()`. */
|
|
15
|
-
back(): void;
|
|
16
|
-
/** Goes forward one history entry — `history.forward()`. */
|
|
17
|
-
forward(): void;
|
|
18
22
|
/** Re-fetches the current route from the server, re-running its server components. */
|
|
19
23
|
refresh(): void;
|
|
20
24
|
/** `true` while a soft navigation is in flight — use it to disable controls or show a spinner. */
|
|
21
25
|
pending: boolean;
|
|
22
26
|
}
|
|
23
|
-
/** The current location plus the {@link
|
|
24
|
-
export interface
|
|
27
|
+
/** The current location plus the {@link NavigationRouter}, as returned by {@link useNavigation}. */
|
|
28
|
+
export interface NavigationState {
|
|
25
29
|
/**
|
|
26
30
|
* The full current {@link URL} — read `url.pathname`, `url.searchParams` and the
|
|
27
31
|
* rest off it. A fresh instance per navigation, so mutating it affects nothing
|
|
@@ -31,16 +35,16 @@ export interface Navigation {
|
|
|
31
35
|
/** Matched route params for the current page, e.g. `{ id: '42' }` for `/profile/:id`. */
|
|
32
36
|
params: Record<string, string>;
|
|
33
37
|
/** Imperative navigation actions and the `pending` flag. */
|
|
34
|
-
router:
|
|
38
|
+
router: NavigationRouter;
|
|
35
39
|
}
|
|
36
40
|
/**
|
|
37
|
-
* Carries the live {@link
|
|
41
|
+
* Carries the live {@link NavigationRouter} implementation from the hydration runtime down
|
|
38
42
|
* to {@link RouterProvider}. Framework internal — read the router through
|
|
39
43
|
* {@link useNavigation} instead.
|
|
40
44
|
*
|
|
41
45
|
* @internal
|
|
42
46
|
*/
|
|
43
|
-
export declare const RouterContext: import("react").Context<
|
|
47
|
+
export declare const RouterContext: import("react").Context<NavigationRouter>;
|
|
44
48
|
/**
|
|
45
49
|
* Publishes the per-render location and params so {@link useNavigation} can read
|
|
46
50
|
* them. Framework internal — the RSC entry wraps every page in one.
|
|
@@ -62,7 +66,7 @@ export declare function RouterProvider({ href, params, children }: {
|
|
|
62
66
|
* `pending` flag that is `true` while a client navigation is in flight.
|
|
63
67
|
*
|
|
64
68
|
* Hooks can't run in a server component; read the same URL data there from
|
|
65
|
-
* `
|
|
69
|
+
* `getRequestContext()` (`@rshono/core/server`) instead.
|
|
66
70
|
*
|
|
67
71
|
* @example
|
|
68
72
|
* ```tsx
|
|
@@ -80,35 +84,13 @@ export declare function RouterProvider({ href, params, children }: {
|
|
|
80
84
|
* }
|
|
81
85
|
* ```
|
|
82
86
|
*
|
|
83
|
-
* @returns The current {@link
|
|
84
|
-
* ({@link
|
|
85
|
-
* `pending`.
|
|
87
|
+
* @returns The current {@link NavigationState}: `url` and `params`, plus `router`
|
|
88
|
+
* ({@link NavigationRouter}) with `push` / `replace` / `refresh` / `pending`.
|
|
86
89
|
* @throws If called outside a page's React tree, where there is no navigation
|
|
87
90
|
* context to read.
|
|
88
|
-
*/
|
|
89
|
-
export declare function useNavigation(): Navigation;
|
|
90
|
-
export interface NavigationProgressProps {
|
|
91
|
-
/** Bar color. Defaults to a neutral blue. */
|
|
92
|
-
color?: string;
|
|
93
|
-
/** Bar height in pixels. Defaults to `3`. */
|
|
94
|
-
height?: number;
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* An opt-in top progress bar that appears while a client navigation is in
|
|
98
|
-
* flight (driven by {@link Router.pending}). Drop one instance in your root
|
|
99
|
-
* layout; it renders nothing on the server and stays invisible until the first
|
|
100
|
-
* soft navigation, so there's no hydration flicker.
|
|
101
91
|
*
|
|
102
|
-
* @
|
|
103
|
-
*
|
|
104
|
-
* import { NavigationProgress } from '@rshono/core/client';
|
|
105
|
-
*
|
|
106
|
-
* // in your layout, once:
|
|
107
|
-
* <body>
|
|
108
|
-
* <NavigationProgress />
|
|
109
|
-
* {children}
|
|
110
|
-
* </body>
|
|
111
|
-
* ```
|
|
92
|
+
* @see {@link https://www.rshono.com/docs/api#rshonocoreclient | Docs — `@rshono/core/client`}
|
|
93
|
+
* @see {@link https://www.rshono.com/docs/pages#client-components | Docs — client components}
|
|
112
94
|
*/
|
|
113
|
-
export declare function
|
|
95
|
+
export declare function useNavigation(): NavigationState;
|
|
114
96
|
//# sourceMappingURL=navigation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../src/runtime/navigation.tsx"],"names":[],"mappings":"AAEA,OAAO,
|
|
1
|
+
{"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../src/runtime/navigation.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAsC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE3E;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,gBAAgB;IAC/B,0DAA0D;IAC1D,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,sFAAsF;IACtF,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,sFAAsF;IACtF,OAAO,IAAI,IAAI,CAAC;IAChB,kGAAkG;IAClG,OAAO,EAAE,OAAO,CAAC;CAClB;AAKD,oGAAoG;AACpG,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,GAAG,EAAE,GAAG,CAAC;IACT,yFAAyF;IACzF,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,4DAA4D;IAC5D,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAMD;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,2CAAiD,CAAC;AAI5E;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,QAAQ,EAAE,SAAS,CAAA;CAAE,+BAK/H;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,aAAa,IAAI,eAAe,CAQ/C"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { createContext, useContext,
|
|
3
|
+
import { createContext, useContext, useMemo } from 'react';
|
|
4
4
|
const noop = () => { };
|
|
5
|
-
const defaultRouter = { push: noop, replace: noop,
|
|
5
|
+
const defaultRouter = { push: noop, replace: noop, refresh: noop, pending: false };
|
|
6
6
|
/**
|
|
7
|
-
* Carries the live {@link
|
|
7
|
+
* Carries the live {@link NavigationRouter} implementation from the hydration runtime down
|
|
8
8
|
* to {@link RouterProvider}. Framework internal — read the router through
|
|
9
9
|
* {@link useNavigation} instead.
|
|
10
10
|
*
|
|
@@ -33,7 +33,7 @@ export function RouterProvider({ href, params, children }) {
|
|
|
33
33
|
* `pending` flag that is `true` while a client navigation is in flight.
|
|
34
34
|
*
|
|
35
35
|
* Hooks can't run in a server component; read the same URL data there from
|
|
36
|
-
* `
|
|
36
|
+
* `getRequestContext()` (`@rshono/core/server`) instead.
|
|
37
37
|
*
|
|
38
38
|
* @example
|
|
39
39
|
* ```tsx
|
|
@@ -51,62 +51,19 @@ export function RouterProvider({ href, params, children }) {
|
|
|
51
51
|
* }
|
|
52
52
|
* ```
|
|
53
53
|
*
|
|
54
|
-
* @returns The current {@link
|
|
55
|
-
* ({@link
|
|
56
|
-
* `pending`.
|
|
54
|
+
* @returns The current {@link NavigationState}: `url` and `params`, plus `router`
|
|
55
|
+
* ({@link NavigationRouter}) with `push` / `replace` / `refresh` / `pending`.
|
|
57
56
|
* @throws If called outside a page's React tree, where there is no navigation
|
|
58
57
|
* context to read.
|
|
58
|
+
*
|
|
59
|
+
* @see {@link https://www.rshono.com/docs/api#rshonocoreclient | Docs — `@rshono/core/client`}
|
|
60
|
+
* @see {@link https://www.rshono.com/docs/pages#client-components | Docs — client components}
|
|
59
61
|
*/
|
|
60
62
|
export function useNavigation() {
|
|
61
63
|
const value = useContext(NavigationContext);
|
|
62
64
|
if (!value) {
|
|
63
|
-
throw new Error("[rshono] useNavigation() must be called inside a 'use client' component rendered by a page. In a server component, read the URL from
|
|
65
|
+
throw new Error("[rshono] useNavigation() must be called inside a 'use client' component rendered by a page. In a server component, read the URL from getRequestContext() instead.");
|
|
64
66
|
}
|
|
65
67
|
return value;
|
|
66
68
|
}
|
|
67
|
-
/**
|
|
68
|
-
* An opt-in top progress bar that appears while a client navigation is in
|
|
69
|
-
* flight (driven by {@link Router.pending}). Drop one instance in your root
|
|
70
|
-
* layout; it renders nothing on the server and stays invisible until the first
|
|
71
|
-
* soft navigation, so there's no hydration flicker.
|
|
72
|
-
*
|
|
73
|
-
* @example
|
|
74
|
-
* ```tsx
|
|
75
|
-
* import { NavigationProgress } from '@rshono/core/client';
|
|
76
|
-
*
|
|
77
|
-
* // in your layout, once:
|
|
78
|
-
* <body>
|
|
79
|
-
* <NavigationProgress />
|
|
80
|
-
* {children}
|
|
81
|
-
* </body>
|
|
82
|
-
* ```
|
|
83
|
-
*/
|
|
84
|
-
export function NavigationProgress({ color = '#3b82f6', height = 3 } = {}) {
|
|
85
|
-
const { router } = useNavigation();
|
|
86
|
-
const [bar, setBar] = useState({ width: 0, opacity: 0 });
|
|
87
|
-
useEffect(() => {
|
|
88
|
-
if (router.pending) {
|
|
89
|
-
// Jump in, then creep toward — but never reach — the end while we wait.
|
|
90
|
-
setBar({ width: 15, opacity: 1 });
|
|
91
|
-
const ramp = setTimeout(() => setBar({ width: 85, opacity: 1 }), 80);
|
|
92
|
-
return () => clearTimeout(ramp);
|
|
93
|
-
}
|
|
94
|
-
// Done: snap to full, then fade out. (No-op if it was never shown.)
|
|
95
|
-
setBar((b) => (b.opacity === 0 ? b : { width: 100, opacity: 1 }));
|
|
96
|
-
const hide = setTimeout(() => setBar({ width: 0, opacity: 0 }), 220);
|
|
97
|
-
return () => clearTimeout(hide);
|
|
98
|
-
}, [router.pending]);
|
|
99
|
-
return (_jsx("div", { "data-rshono-progress": "", "aria-hidden": "true", style: {
|
|
100
|
-
position: 'fixed',
|
|
101
|
-
top: 0,
|
|
102
|
-
left: 0,
|
|
103
|
-
height,
|
|
104
|
-
width: `${bar.width}%`,
|
|
105
|
-
opacity: bar.opacity,
|
|
106
|
-
background: color,
|
|
107
|
-
zIndex: 2147483647,
|
|
108
|
-
pointerEvents: 'none',
|
|
109
|
-
transition: 'width 200ms ease-out, opacity 200ms ease-out',
|
|
110
|
-
} }));
|
|
111
|
-
}
|
|
112
69
|
//# sourceMappingURL=navigation.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navigation.js","sourceRoot":"","sources":["../../src/runtime/navigation.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,
|
|
1
|
+
{"version":3,"file":"navigation.js","sourceRoot":"","sources":["../../src/runtime/navigation.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,OAAO,EAAkB,MAAM,OAAO,CAAC;AA6C3E,MAAM,IAAI,GAAG,GAAG,EAAE,GAAE,CAAC,CAAC;AAEtB,MAAM,aAAa,GAAqB,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAErG;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAmB,aAAa,CAAC,CAAC;AAE5E,MAAM,iBAAiB,GAAG,aAAa,CAAyB,IAAI,CAAC,CAAC;AAEtE;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAyE;IAC9H,MAAM,MAAM,GAAG,UAAU,CAAC,aAAa,CAAC,CAAC;IACzC,MAAM,KAAK,GAAG,OAAO,CAAkB,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAE/G,OAAO,KAAC,iBAAiB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAA8B,CAAC;AAC3F,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,UAAU,aAAa;IAC3B,MAAM,KAAK,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC;IAC5C,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,mKAAmK,CACpK,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["'use client';\n\nimport { createContext, useContext, useMemo, type ReactNode } from 'react';\n\n/**\n * Imperative navigation actions, reached as `useNavigation().router`.\n *\n * `push` / `replace` / `refresh` are **soft** navigations: the new page's flight\n * payload is fetched and applied in place, so client component state outside the\n * changed subtree survives. Off-site or non-HTTP hrefs fall back to a full load.\n *\n * @example\n * ```tsx\n * const { router } = useNavigation();\n * router.push('/dashboard'); // navigate, new history entry\n * router.replace('/login'); // navigate, no new entry\n * router.refresh(); // re-run this route's server components\n * ```\n */\nexport interface NavigationRouter {\n /** Navigates to `href` and pushes a new history entry. */\n push(href: string): void;\n /** Navigates to `href`, replacing the current history entry instead of adding one. */\n replace(href: string): void;\n /** Re-fetches the current route from the server, re-running its server components. */\n refresh(): void;\n /** `true` while a soft navigation is in flight — use it to disable controls or show a spinner. */\n pending: boolean;\n}\n\n// No `back()` / `forward()` — `history.back()` and `history.forward()` need no framework, and the\n// traversal is picked up by the runtime's `popstate` listener either way.\n\n/** The current location plus the {@link NavigationRouter}, as returned by {@link useNavigation}. */\nexport interface NavigationState {\n /**\n * The full current {@link URL} — read `url.pathname`, `url.searchParams` and the\n * rest off it. A fresh instance per navigation, so mutating it affects nothing\n * else; it is not written back to the address bar either.\n */\n url: URL;\n /** Matched route params for the current page, e.g. `{ id: '42' }` for `/profile/:id`. */\n params: Record<string, string>;\n /** Imperative navigation actions and the `pending` flag. */\n router: NavigationRouter;\n}\n\nconst noop = () => {};\n\nconst defaultRouter: NavigationRouter = { push: noop, replace: noop, refresh: noop, pending: false };\n\n/**\n * Carries the live {@link NavigationRouter} implementation from the hydration runtime down\n * to {@link RouterProvider}. Framework internal — read the router through\n * {@link useNavigation} instead.\n *\n * @internal\n */\nexport const RouterContext = createContext<NavigationRouter>(defaultRouter);\n\nconst NavigationContext = createContext<NavigationState | null>(null);\n\n/**\n * Publishes the per-render location and params so {@link useNavigation} can read\n * them. Framework internal — the RSC entry wraps every page in one.\n *\n * @internal\n */\nexport function RouterProvider({ href, params, children }: { href: string; params: Record<string, string>; children: ReactNode }) {\n const router = useContext(RouterContext);\n const value = useMemo<NavigationState>(() => ({ url: new URL(href), params, router }), [href, params, router]);\n\n return <NavigationContext.Provider value={value}>{children}</NavigationContext.Provider>;\n}\n\n/**\n * Reactive access to the current URL and programmatic navigation, in one hook.\n *\n * Call it from a `'use client'` component. The location fields (`url` and\n * `params`) are computed on the server and travel in the flight payload, so they\n * are correct during SSR — no hydration flicker — and update automatically on\n * every navigation. The `router` sub-object holds the imperative actions plus a\n * `pending` flag that is `true` while a client navigation is in flight.\n *\n * Hooks can't run in a server component; read the same URL data there from\n * `getRequestContext()` (`@rshono/core/server`) instead.\n *\n * @example\n * ```tsx\n * 'use client';\n * import { useNavigation } from '@rshono/core/client';\n *\n * export function NextPage() {\n * const { url, router } = useNavigation();\n * const page = Number(url.searchParams.get('page') ?? '1');\n * return (\n * <button disabled={router.pending} onClick={() => router.push(`${url.pathname}?page=${page + 1}`)}>\n * Next {router.pending ? '…' : ''}\n * </button>\n * );\n * }\n * ```\n *\n * @returns The current {@link NavigationState}: `url` and `params`, plus `router`\n * ({@link NavigationRouter}) with `push` / `replace` / `refresh` / `pending`.\n * @throws If called outside a page's React tree, where there is no navigation\n * context to read.\n *\n * @see {@link https://www.rshono.com/docs/api#rshonocoreclient | Docs — `@rshono/core/client`}\n * @see {@link https://www.rshono.com/docs/pages#client-components | Docs — client components}\n */\nexport function useNavigation(): NavigationState {\n const value = useContext(NavigationContext);\n if (!value) {\n throw new Error(\n \"[rshono] useNavigation() must be called inside a 'use client' component rendered by a page. In a server component, read the URL from getRequestContext() instead.\",\n );\n }\n return value;\n}\n"]}
|
|
@@ -28,6 +28,12 @@ export declare function parseRenderRequest(request: Request): RenderRequest;
|
|
|
28
28
|
export declare function acceptsRsc(request: Request): boolean;
|
|
29
29
|
/** True when the response should be a flight payload rather than an HTML document. */
|
|
30
30
|
export declare function wantsRsc(renderRequest: RenderRequest): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* {@link wantsRsc} straight off the request, for the error and control-signal paths that have no
|
|
33
|
+
* parsed {@link RenderRequest} to hand — they are reached from Hono's `onError` / `notFound` rather
|
|
34
|
+
* than from the render.
|
|
35
|
+
*/
|
|
36
|
+
export declare function requestWantsRsc(request: Request): boolean;
|
|
31
37
|
/** True when the request carries a server action to run before rendering. */
|
|
32
38
|
export declare function isActionRequest(renderRequest: RenderRequest): renderRequest is Extract<RenderRequest, {
|
|
33
39
|
kind: 'form-action' | 'rsc-action';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../../src/runtime/request.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH,MAAM,MAAM,aAAa,GAAG;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAExI,uHAAuH;AACvH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAAG,OAAO,CASpG;AAID,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,aAAa,CAQlE;AAED,gHAAgH;AAChH,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAEpD;AAED,sFAAsF;AACtF,wBAAgB,QAAQ,CAAC,aAAa,EAAE,aAAa,GAAG,OAAO,CAE9D;AAED,6EAA6E;AAC7E,wBAAgB,eAAe,CAAC,aAAa,EAAE,aAAa,GAAG,aAAa,IAAI,OAAO,CAAC,aAAa,EAAE;IAAE,IAAI,EAAE,aAAa,GAAG,YAAY,CAAA;CAAE,CAAC,CAE7I"}
|
|
1
|
+
{"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../../src/runtime/request.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH,MAAM,MAAM,aAAa,GAAG;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAExI,uHAAuH;AACvH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAAG,OAAO,CASpG;AAID,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,aAAa,CAQlE;AAED,gHAAgH;AAChH,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAEpD;AAED,sFAAsF;AACtF,wBAAgB,QAAQ,CAAC,aAAa,EAAE,aAAa,GAAG,OAAO,CAE9D;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAEzD;AAED,6EAA6E;AAC7E,wBAAgB,eAAe,CAAC,aAAa,EAAE,aAAa,GAAG,aAAa,IAAI,OAAO,CAAC,aAAa,EAAE;IAAE,IAAI,EAAE,aAAa,GAAG,YAAY,CAAA;CAAE,CAAC,CAE7I"}
|
package/dist/runtime/request.js
CHANGED
|
@@ -32,6 +32,14 @@ export function acceptsRsc(request) {
|
|
|
32
32
|
export function wantsRsc(renderRequest) {
|
|
33
33
|
return renderRequest.kind === 'rsc' || renderRequest.kind === 'rsc-action';
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* {@link wantsRsc} straight off the request, for the error and control-signal paths that have no
|
|
37
|
+
* parsed {@link RenderRequest} to hand — they are reached from Hono's `onError` / `notFound` rather
|
|
38
|
+
* than from the render.
|
|
39
|
+
*/
|
|
40
|
+
export function requestWantsRsc(request) {
|
|
41
|
+
return wantsRsc(parseRenderRequest(request));
|
|
42
|
+
}
|
|
35
43
|
/** True when the request carries a server action to run before rendering. */
|
|
36
44
|
export function isActionRequest(renderRequest) {
|
|
37
45
|
return renderRequest.kind === 'form-action' || renderRequest.kind === 'rsc-action';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request.js","sourceRoot":"","sources":["../../src/runtime/request.ts"],"names":[],"mappings":"AAAA,MAAM,gBAAgB,GAAG,cAAc,CAAC;AACxC,MAAM,gBAAgB,GAAG,kBAAkB,CAAC;AAc5C,uHAAuH;AACvH,MAAM,UAAU,gBAAgB,CAAC,SAAiB,EAAE,MAAuC;IACzF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAC1D,IAAI,MAAM;QAAE,OAAO,CAAC,GAAG,CAAC,gBAAgB,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IACrD,OAAO,IAAI,OAAO,CAAC,GAAG,EAAE;QACtB,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK;QAC/B,OAAO;QACP,IAAI,EAAE,MAAM,EAAE,IAAI;KACnB,CAAC,CAAC;AACL,CAAC;AAED,MAAM,kBAAkB,GAAG,+DAA+D,CAAC;AAE3F,MAAM,UAAU,kBAAkB,CAAC,OAAgB;IACjD,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QAC9B,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;QACvD,IAAI,QAAQ;YAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC;QACtD,IAAI,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;QACvG,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;IAC9B,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC;AAC5D,CAAC;AAED,gHAAgH;AAChH,MAAM,UAAU,UAAU,CAAC,OAAgB;IACzC,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,gBAAgB,CAAC,IAAI,KAAK,CAAC;AAC5E,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,QAAQ,CAAC,aAA4B;IACnD,OAAO,aAAa,CAAC,IAAI,KAAK,KAAK,IAAI,aAAa,CAAC,IAAI,KAAK,YAAY,CAAC;AAC7E,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,eAAe,CAAC,aAA4B;IAC1D,OAAO,aAAa,CAAC,IAAI,KAAK,aAAa,IAAI,aAAa,CAAC,IAAI,KAAK,YAAY,CAAC;AACrF,CAAC","sourcesContent":["const HEADER_ACTION_ID = 'x-rsc-action';\nconst RSC_CONTENT_TYPE = 'text/x-component';\n\n/**\n * The four distinct shapes an incoming render request can take, modeled as a\n * discriminated union so illegal combinations (e.g. an action id on a GET) are\n * unrepresentable and every consumer dispatches exhaustively on `kind`.\n *\n * - `document` — a normal navigation; respond with a full SSR HTML document.\n * - `rsc` — a soft-navigation flight fetch (`Accept: text/x-component`).\n * - `form-action` — a progressive-enhancement `<form>` POST (no JavaScript).\n * - `rsc-action` — a client-initiated server-action call carrying an action id.\n */\nexport type RenderRequest = { kind: 'document' } | { kind: 'rsc' } | { kind: 'form-action' } | { kind: 'rsc-action'; actionId: string };\n\n/** Builds the `Request` the client sends to ask a page for its flight payload, optionally carrying a server action. */\nexport function createRscRequest(urlString: string, action?: { id: string; body: BodyInit }): Request {\n const url = new URL(urlString, location.origin);\n const headers = new Headers({ Accept: RSC_CONTENT_TYPE });\n if (action) headers.set(HEADER_ACTION_ID, action.id);\n return new Request(url, {\n method: action ? 'POST' : 'GET',\n headers,\n body: action?.body,\n });\n}\n\nconst FORM_CONTENT_TYPES = /^(?:multipart\\/form-data|application\\/x-www-form-urlencoded)/i;\n\nexport function parseRenderRequest(request: Request): RenderRequest {\n if (request.method === 'POST') {\n const actionId = request.headers.get(HEADER_ACTION_ID);\n if (actionId) return { kind: 'rsc-action', actionId };\n if (FORM_CONTENT_TYPES.test(request.headers.get('content-type') ?? '')) return { kind: 'form-action' };\n return { kind: 'document' };\n }\n return { kind: acceptsRsc(request) ? 'rsc' : 'document' };\n}\n\n/** Whether the client asked for a flight payload. For GET paths that only need the boolean, without parsing. */\nexport function acceptsRsc(request: Request): boolean {\n return request.headers.get('accept')?.includes(RSC_CONTENT_TYPE) ?? false;\n}\n\n/** True when the response should be a flight payload rather than an HTML document. */\nexport function wantsRsc(renderRequest: RenderRequest): boolean {\n return renderRequest.kind === 'rsc' || renderRequest.kind === 'rsc-action';\n}\n\n/** True when the request carries a server action to run before rendering. */\nexport function isActionRequest(renderRequest: RenderRequest): renderRequest is Extract<RenderRequest, { kind: 'form-action' | 'rsc-action' }> {\n return renderRequest.kind === 'form-action' || renderRequest.kind === 'rsc-action';\n}\n"]}
|
|
1
|
+
{"version":3,"file":"request.js","sourceRoot":"","sources":["../../src/runtime/request.ts"],"names":[],"mappings":"AAAA,MAAM,gBAAgB,GAAG,cAAc,CAAC;AACxC,MAAM,gBAAgB,GAAG,kBAAkB,CAAC;AAc5C,uHAAuH;AACvH,MAAM,UAAU,gBAAgB,CAAC,SAAiB,EAAE,MAAuC;IACzF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAC1D,IAAI,MAAM;QAAE,OAAO,CAAC,GAAG,CAAC,gBAAgB,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IACrD,OAAO,IAAI,OAAO,CAAC,GAAG,EAAE;QACtB,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK;QAC/B,OAAO;QACP,IAAI,EAAE,MAAM,EAAE,IAAI;KACnB,CAAC,CAAC;AACL,CAAC;AAED,MAAM,kBAAkB,GAAG,+DAA+D,CAAC;AAE3F,MAAM,UAAU,kBAAkB,CAAC,OAAgB;IACjD,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QAC9B,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;QACvD,IAAI,QAAQ;YAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC;QACtD,IAAI,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;QACvG,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;IAC9B,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC;AAC5D,CAAC;AAED,gHAAgH;AAChH,MAAM,UAAU,UAAU,CAAC,OAAgB;IACzC,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,gBAAgB,CAAC,IAAI,KAAK,CAAC;AAC5E,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,QAAQ,CAAC,aAA4B;IACnD,OAAO,aAAa,CAAC,IAAI,KAAK,KAAK,IAAI,aAAa,CAAC,IAAI,KAAK,YAAY,CAAC;AAC7E,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,OAAgB;IAC9C,OAAO,QAAQ,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,eAAe,CAAC,aAA4B;IAC1D,OAAO,aAAa,CAAC,IAAI,KAAK,aAAa,IAAI,aAAa,CAAC,IAAI,KAAK,YAAY,CAAC;AACrF,CAAC","sourcesContent":["const HEADER_ACTION_ID = 'x-rsc-action';\nconst RSC_CONTENT_TYPE = 'text/x-component';\n\n/**\n * The four distinct shapes an incoming render request can take, modeled as a\n * discriminated union so illegal combinations (e.g. an action id on a GET) are\n * unrepresentable and every consumer dispatches exhaustively on `kind`.\n *\n * - `document` — a normal navigation; respond with a full SSR HTML document.\n * - `rsc` — a soft-navigation flight fetch (`Accept: text/x-component`).\n * - `form-action` — a progressive-enhancement `<form>` POST (no JavaScript).\n * - `rsc-action` — a client-initiated server-action call carrying an action id.\n */\nexport type RenderRequest = { kind: 'document' } | { kind: 'rsc' } | { kind: 'form-action' } | { kind: 'rsc-action'; actionId: string };\n\n/** Builds the `Request` the client sends to ask a page for its flight payload, optionally carrying a server action. */\nexport function createRscRequest(urlString: string, action?: { id: string; body: BodyInit }): Request {\n const url = new URL(urlString, location.origin);\n const headers = new Headers({ Accept: RSC_CONTENT_TYPE });\n if (action) headers.set(HEADER_ACTION_ID, action.id);\n return new Request(url, {\n method: action ? 'POST' : 'GET',\n headers,\n body: action?.body,\n });\n}\n\nconst FORM_CONTENT_TYPES = /^(?:multipart\\/form-data|application\\/x-www-form-urlencoded)/i;\n\nexport function parseRenderRequest(request: Request): RenderRequest {\n if (request.method === 'POST') {\n const actionId = request.headers.get(HEADER_ACTION_ID);\n if (actionId) return { kind: 'rsc-action', actionId };\n if (FORM_CONTENT_TYPES.test(request.headers.get('content-type') ?? '')) return { kind: 'form-action' };\n return { kind: 'document' };\n }\n return { kind: acceptsRsc(request) ? 'rsc' : 'document' };\n}\n\n/** Whether the client asked for a flight payload. For GET paths that only need the boolean, without parsing. */\nexport function acceptsRsc(request: Request): boolean {\n return request.headers.get('accept')?.includes(RSC_CONTENT_TYPE) ?? false;\n}\n\n/** True when the response should be a flight payload rather than an HTML document. */\nexport function wantsRsc(renderRequest: RenderRequest): boolean {\n return renderRequest.kind === 'rsc' || renderRequest.kind === 'rsc-action';\n}\n\n/**\n * {@link wantsRsc} straight off the request, for the error and control-signal paths that have no\n * parsed {@link RenderRequest} to hand — they are reached from Hono's `onError` / `notFound` rather\n * than from the render.\n */\nexport function requestWantsRsc(request: Request): boolean {\n return wantsRsc(parseRenderRequest(request));\n}\n\n/** True when the request carries a server action to run before rendering. */\nexport function isActionRequest(renderRequest: RenderRequest): renderRequest is Extract<RenderRequest, { kind: 'form-action' | 'rsc-action' }> {\n return renderRequest.kind === 'form-action' || renderRequest.kind === 'rsc-action';\n}\n"]}
|
package/dist/runtime/server.d.ts
CHANGED
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@rshono/core/server` — the request-scoped surface, for use inside server components
|
|
3
|
-
* and `'use server'` action modules:
|
|
4
|
-
* params, env and middleware variables, the
|
|
5
|
-
* control-flow helpers, and
|
|
6
|
-
* framework catches.
|
|
3
|
+
* and `'use server'` action modules: `getRequestContext()` for the URL, cookies,
|
|
4
|
+
* params, env and middleware variables, the `redirect()` and `notFound()`
|
|
5
|
+
* control-flow helpers, and `onServerError()` for reporting the errors the
|
|
6
|
+
* framework catches. Plus `publicUrl(c)`, for middleware in `src/server.ts`, which
|
|
7
|
+
* is handed Hono's `c` rather than a request context.
|
|
7
8
|
*
|
|
8
9
|
* Server-only. Importing this from a `'use client'` module is a mistake — those
|
|
9
10
|
* run in the browser (and are SSR'd without a bound context). Read what you need
|
|
10
11
|
* on the server and pass it down as props, or use `useNavigation()` from
|
|
11
12
|
* `@rshono/core/client` for URL data.
|
|
12
13
|
*
|
|
13
|
-
*
|
|
14
|
-
* implemented: that module also exports the plumbing the framework's own entry
|
|
15
|
-
* points need (`runWithContext`, `readParams`, `publicUrl`, `reportServerError`),
|
|
16
|
-
* and pointing the `./server` subpath straight at it published every one of them —
|
|
17
|
-
* `@internal` in the docs, but present in a consumer's autocomplete and pinned by
|
|
18
|
-
* semver all the same. They stay reachable by relative import, which is the only
|
|
19
|
-
* way the framework itself ever reaches them.
|
|
14
|
+
* @see {@link https://www.rshono.com/docs/api#rshonocoreserver | Docs — `@rshono/core/server`}
|
|
20
15
|
*
|
|
21
16
|
* @packageDocumentation
|
|
22
17
|
*/
|
|
23
|
-
export {
|
|
18
|
+
export { getRequestContext, notFound, onServerError, publicUrl, redirect, type EnvVars, type RedirectStatus, type RequestContext, type ServerErrorContext, type ServerErrorHandler, type ServerErrorSource, } from './context.js';
|
|
24
19
|
//# sourceMappingURL=server.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/runtime/server.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/runtime/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,EACL,iBAAiB,EACjB,QAAQ,EACR,aAAa,EAIb,SAAS,EACT,QAAQ,EACR,KAAK,OAAO,EACZ,KAAK,cAAc,EAGnB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,GACvB,MAAM,cAAc,CAAC"}
|