@rsc-kit/core 0.7.1 → 0.8.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 +4 -4
- package/dist/action.d.ts +0 -1
- package/dist/action.js.map +1 -1
- package/dist/build-rsc-vite.d.ts +0 -1
- package/dist/build-rsc-vite.js.map +1 -1
- package/dist/cache.d.ts +0 -1
- package/dist/cache.js.map +1 -1
- package/dist/devServer.d.ts +0 -1
- package/dist/devServer.js.map +1 -1
- package/dist/devUrls.d.ts +0 -1
- package/dist/devUrls.js.map +1 -1
- package/dist/embed.d.ts +0 -1
- package/dist/embed.js.map +1 -1
- package/dist/errors.d.ts +0 -1
- package/dist/errors.js.map +1 -1
- package/dist/export.d.ts +0 -1
- package/dist/export.js.map +1 -1
- package/dist/files.d.ts +0 -1
- package/dist/files.js.map +1 -1
- package/dist/headers.d.ts +20 -1
- package/dist/headers.js +20 -0
- package/dist/headers.js.map +1 -1
- package/dist/host.d.ts +11 -1
- package/dist/host.js +60 -0
- package/dist/host.js.map +1 -1
- package/dist/hostCalls.d.ts +0 -1
- package/dist/hostCalls.js.map +1 -1
- package/dist/js/ActivityRouter.d.ts +0 -1
- package/dist/js/ActivityRouter.js.map +1 -1
- package/dist/js/DocumentTitle.d.ts +0 -1
- package/dist/js/DocumentTitle.js.map +1 -1
- package/dist/js/Form.d.ts +0 -1
- package/dist/js/Form.js.map +1 -1
- package/dist/js/Link.d.ts +0 -1
- package/dist/js/Link.js.map +1 -1
- package/dist/js/PathnameProvider.d.ts +0 -1
- package/dist/js/PathnameProvider.js.map +1 -1
- package/dist/js/RedirectBoundary.d.ts +0 -1
- package/dist/js/RedirectBoundary.js.map +1 -1
- package/dist/js/SegmentBoundary.d.ts +0 -1
- package/dist/js/SegmentBoundary.js +14 -7
- package/dist/js/SegmentBoundary.js.map +1 -1
- package/dist/js/SlotBoundary.d.ts +0 -1
- package/dist/js/SlotBoundary.js.map +1 -1
- package/dist/js/createViteRscApp.d.ts +0 -1
- package/dist/js/createViteRscApp.js +21 -1
- package/dist/js/createViteRscApp.js.map +1 -1
- package/dist/js/errors.d.ts +0 -1
- package/dist/js/errors.js.map +1 -1
- package/dist/js/navigate.d.ts +2 -2
- package/dist/js/navigate.js +40 -7
- package/dist/js/navigate.js.map +1 -1
- package/dist/js/onlineStore.d.ts +0 -1
- package/dist/js/onlineStore.js.map +1 -1
- package/dist/js/pagePayload.d.ts +0 -1
- package/dist/js/pagePayload.js.map +1 -1
- package/dist/js/queryClient.d.ts +26 -0
- package/dist/js/queryClient.js +173 -0
- package/dist/js/queryClient.js.map +1 -0
- package/dist/js/router.d.ts +0 -1
- package/dist/js/router.js.map +1 -1
- package/dist/js/section.d.ts +0 -1
- package/dist/js/section.js.map +1 -1
- package/dist/js/segmentStore.d.ts +41 -2
- package/dist/js/segmentStore.js +49 -3
- package/dist/js/segmentStore.js.map +1 -1
- package/dist/js/slotStore.d.ts +0 -1
- package/dist/js/slotStore.js.map +1 -1
- package/dist/js/standardSchema.d.ts +0 -1
- package/dist/js/standardSchema.js.map +1 -1
- package/dist/js/useForm.d.ts +0 -1
- package/dist/js/useForm.js.map +1 -1
- package/dist/js/useLinkStatus.d.ts +0 -1
- package/dist/js/useLinkStatus.js.map +1 -1
- package/dist/js/useOnline.d.ts +0 -1
- package/dist/js/useOnline.js.map +1 -1
- package/dist/js/usePathname.d.ts +0 -1
- package/dist/js/usePathname.js.map +1 -1
- package/dist/js/useSearchParams.d.ts +0 -1
- package/dist/js/useSearchParams.js.map +1 -1
- package/dist/manifest.d.ts +0 -1
- package/dist/manifest.js.map +1 -1
- package/dist/nondeterminism.d.ts +0 -1
- package/dist/nondeterminism.js.map +1 -1
- package/dist/prerender.d.ts +0 -1
- package/dist/prerender.js.map +1 -1
- package/dist/query.d.ts +36 -0
- package/dist/query.js +74 -0
- package/dist/query.js.map +1 -0
- package/dist/redirect.d.ts +0 -1
- package/dist/redirect.js.map +1 -1
- package/dist/redirectDigest.d.ts +0 -1
- package/dist/redirectDigest.js.map +1 -1
- package/dist/request.d.ts +0 -1
- package/dist/request.js.map +1 -1
- package/dist/revalidate.d.ts +0 -1
- package/dist/revalidate.js.map +1 -1
- package/dist/routes.d.ts +0 -1
- package/dist/routes.js.map +1 -1
- package/dist/routing.d.ts +0 -1
- package/dist/routing.js.map +1 -1
- package/dist/runtime.d.ts +0 -1
- package/dist/runtime.js.map +1 -1
- package/dist/safeUrl.d.ts +0 -1
- package/dist/safeUrl.js.map +1 -1
- package/dist/vite.d.ts +45 -1
- package/dist/vite.js +352 -10
- package/dist/vite.js.map +1 -1
- package/package.json +14 -9
- package/dist/action.d.ts.map +0 -1
- package/dist/build-rsc-vite.d.ts.map +0 -1
- package/dist/cache.d.ts.map +0 -1
- package/dist/cli.d.ts +0 -2
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -84
- package/dist/cli.js.map +0 -1
- package/dist/devServer.d.ts.map +0 -1
- package/dist/devUrls.d.ts.map +0 -1
- package/dist/embed.d.ts.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/export.d.ts.map +0 -1
- package/dist/files.d.ts.map +0 -1
- package/dist/headers.d.ts.map +0 -1
- package/dist/host.d.ts.map +0 -1
- package/dist/hostCalls.d.ts.map +0 -1
- package/dist/js/ActivityRouter.d.ts.map +0 -1
- package/dist/js/DocumentTitle.d.ts.map +0 -1
- package/dist/js/Form.d.ts.map +0 -1
- package/dist/js/Link.d.ts.map +0 -1
- package/dist/js/PathnameProvider.d.ts.map +0 -1
- package/dist/js/RedirectBoundary.d.ts.map +0 -1
- package/dist/js/SegmentBoundary.d.ts.map +0 -1
- package/dist/js/SlotBoundary.d.ts.map +0 -1
- package/dist/js/createViteRscApp.d.ts.map +0 -1
- package/dist/js/errors.d.ts.map +0 -1
- package/dist/js/navigate.d.ts.map +0 -1
- package/dist/js/onlineStore.d.ts.map +0 -1
- package/dist/js/pagePayload.d.ts.map +0 -1
- package/dist/js/router.d.ts.map +0 -1
- package/dist/js/section.d.ts.map +0 -1
- package/dist/js/segmentStore.d.ts.map +0 -1
- package/dist/js/slotStore.d.ts.map +0 -1
- package/dist/js/standardSchema.d.ts.map +0 -1
- package/dist/js/useForm.d.ts.map +0 -1
- package/dist/js/useLinkStatus.d.ts.map +0 -1
- package/dist/js/useOnline.d.ts.map +0 -1
- package/dist/js/usePathname.d.ts.map +0 -1
- package/dist/js/useSearchParams.d.ts.map +0 -1
- package/dist/manifest.d.ts.map +0 -1
- package/dist/nondeterminism.d.ts.map +0 -1
- package/dist/prerender.d.ts.map +0 -1
- package/dist/redirect.d.ts.map +0 -1
- package/dist/redirectDigest.d.ts.map +0 -1
- package/dist/request.d.ts.map +0 -1
- package/dist/revalidate.d.ts.map +0 -1
- package/dist/routes.d.ts.map +0 -1
- package/dist/routing.d.ts.map +0 -1
- package/dist/runtime.d.ts.map +0 -1
- package/dist/safeUrl.d.ts.map +0 -1
- package/dist/vite.d.ts.map +0 -1
package/dist/safeUrl.d.ts
CHANGED
package/dist/safeUrl.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safeUrl.js","sourceRoot":"","sources":["../src/safeUrl.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,+EAA+E;AAC/E,2EAA2E;AAC3E,EAAE;AACF,4EAA4E;AAC5E,4EAA4E;AAC5E,6EAA6E;AAC7E,2EAA2E;AAE3E,kFAAkF;AAClF,MAAM,OAAO,GAAG,0BAA0B,CAAA;AAE1C,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,IAAI,CAAC;QACH,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;QAE5C,wEAAwE;QACxE,2EAA2E;QAC3E,oDAAoD;QACpD,OAAO,QAAQ,KAAK,OAAO,IAAI,QAAQ,KAAK,QAAQ,CAAA;IACtD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,IAAI,cAAc,CAAC,KAAK,CAAC;QAAE,OAAM;IAEjC,MAAM,IAAI,KAAK,CACb,kCAAkC,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI;QAC/D,yEAAyE;QACzE,wEAAwE,CAC3E,CAAA;AACH,CAAC"}
|
|
1
|
+
{"version":3,"file":"safeUrl.js","sourceRoot":"","sources":["../src/safeUrl.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,+EAA+E;AAC/E,2EAA2E;AAC3E,EAAE;AACF,4EAA4E;AAC5E,4EAA4E;AAC5E,6EAA6E;AAC7E,2EAA2E;AAE3E,kFAAkF;AAClF,MAAM,OAAO,GAAG,0BAA0B,CAAA;AAE1C,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,IAAI,CAAC;QACH,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;QAE5C,wEAAwE;QACxE,2EAA2E;QAC3E,oDAAoD;QACpD,OAAO,QAAQ,KAAK,OAAO,IAAI,QAAQ,KAAK,QAAQ,CAAA;IACtD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,IAAI,cAAc,CAAC,KAAK,CAAC;QAAE,OAAM;IAEjC,MAAM,IAAI,KAAK,CACb,kCAAkC,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI;QAC/D,yEAAyE;QACzE,wEAAwE,CAC3E,CAAA;AACH,CAAC","sourcesContent":["// Whether a redirect destination is a url a browser should be sent to.\n//\n// A destination is routinely computed rather than written — redirect()'s own\n// documentation describes reading one from a cookie as the usual case — and it\n// reaches location.href, location.replace and an inline script. A scheme other\n// than http(s) in any of those is script execution rather than navigation.\n//\n// Parsed rather than pattern-matched. javascript: can be written with mixed\n// case, with a newline inside it, or behind leading control characters, and\n// each of those is a different regex to get wrong. The URL parser normalises\n// all of it already, so asking it for the protocol is shorter and correct.\n\n/** A base that cannot collide with a real origin, for resolving relative urls. */\nconst NOWHERE = 'https://rsc-kit.invalid/'\n\nexport function isSafeRedirect(value: string): boolean {\n try {\n const { protocol } = new URL(value, NOWHERE)\n\n // http and https only. Not data:, not blob:, not javascript:, and not a\n // custom app scheme — a browser handing one of those to another program is\n // not something a render should be able to trigger.\n return protocol === 'http:' || protocol === 'https:'\n } catch {\n return false\n }\n}\n\n/**\n * The same check, as a refusal.\n *\n * Says the value back, deliberately. A destination that fails this is far more\n * often a bug in what the app computed than an attack, and a message that does\n * not name the value leaves nothing to go on.\n */\nexport function assertSafeRedirect(value: string): void {\n if (isSafeRedirect(value)) return\n\n throw new Error(\n 'Not a url a redirect may go to: ' + JSON.stringify(value) + '. ' +\n 'A destination has to be a path or an http(s) url — any other scheme is ' +\n 'handed to the browser as something to run rather than somewhere to go.',\n )\n}\n"]}
|
package/dist/vite.d.ts
CHANGED
|
@@ -4,6 +4,30 @@ export interface RscKitOptions {
|
|
|
4
4
|
projectRoot?: string;
|
|
5
5
|
/** Directory holding the app/ route tree. Defaults to `src`. */
|
|
6
6
|
sourceDir?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Animate navigations with React's `<ViewTransition>`.
|
|
9
|
+
*
|
|
10
|
+
* Off by default and a build-time constant rather than a runtime setting, so
|
|
11
|
+
* an app that does not ask for it does not carry the boundary at all.
|
|
12
|
+
*
|
|
13
|
+
* Needs react and react-dom at 19.3 or newer, where ViewTransition is
|
|
14
|
+
* stable. What it animates is the segment a navigation replaces; what a page
|
|
15
|
+
* does inside itself is the app's own business and needs no flag.
|
|
16
|
+
*/
|
|
17
|
+
viewTransitions?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Serve the app from a service worker, so it survives a reload with no
|
|
20
|
+
* network at all.
|
|
21
|
+
*
|
|
22
|
+
* Everything else this engine keeps — held pages, the prefetch cache — lives
|
|
23
|
+
* in one page's memory. Reload with no network and the browser shows its own
|
|
24
|
+
* error page: no script runs, so nothing cached in JavaScript is reachable.
|
|
25
|
+
* A worker is the only thing on the other side of that line.
|
|
26
|
+
*
|
|
27
|
+
* Off by default. It changes what a visitor sees when your deploy is broken,
|
|
28
|
+
* which is not a decision to make for someone.
|
|
29
|
+
*/
|
|
30
|
+
offline?: boolean;
|
|
7
31
|
/** Where the server bundles and generated entries go. Defaults to `.rsc`. */
|
|
8
32
|
outDir?: string;
|
|
9
33
|
/**
|
|
@@ -144,5 +168,25 @@ export interface RscKitOptions {
|
|
|
144
168
|
*/
|
|
145
169
|
prerender?: boolean;
|
|
146
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* The service worker, written into the client output at build time.
|
|
173
|
+
*
|
|
174
|
+
* Scoped to the site root because it is served from there, so it sees every
|
|
175
|
+
* navigation. Three kinds of request, three answers:
|
|
176
|
+
*
|
|
177
|
+
* Hashed assets are immutable by construction — the name changes when the
|
|
178
|
+
* bytes do — so they are answered from the cache and only fetched once ever.
|
|
179
|
+
*
|
|
180
|
+
* Documents and payloads go to the network first and fall back to the cache,
|
|
181
|
+
* because a page whose data moved on should say so while there is a network to
|
|
182
|
+
* ask. Only the fallback is what makes a reload work with none.
|
|
183
|
+
*
|
|
184
|
+
* A document and its payload share a url and differ by header. They do not
|
|
185
|
+
* collide, because the server sends `Vary: X-RSC, ...` and the Cache API
|
|
186
|
+
* honours it when a Response is stored whole and matched with its Request.
|
|
187
|
+
*
|
|
188
|
+
* Nothing else is touched. An action is a POST and must never be answered from
|
|
189
|
+
* a cache; anything cross-origin is somebody else's to cache.
|
|
190
|
+
*/
|
|
191
|
+
export declare const SERVICE_WORKER: (version: string, precache: string[]) => string;
|
|
147
192
|
export declare function rscKit(options?: RscKitOptions): PluginOption[];
|
|
148
|
-
//# sourceMappingURL=vite.d.ts.map
|
package/dist/vite.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// the structural config (entries, output dirs, base). @vitejs/plugin-rsc is
|
|
14
14
|
// included here so it always runs before any react() layer the app adds.
|
|
15
15
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
16
17
|
import { createRequire } from 'node:module';
|
|
17
18
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
18
19
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
@@ -27,6 +28,8 @@ let outDir;
|
|
|
27
28
|
let appDir;
|
|
28
29
|
let genDir;
|
|
29
30
|
let publicAssetsDir;
|
|
31
|
+
/** Where generated ambient declarations go — `.rsc-kit` at the project root. */
|
|
32
|
+
let typesDir;
|
|
30
33
|
let hotFile;
|
|
31
34
|
let hostCallOptions;
|
|
32
35
|
let packageDir;
|
|
@@ -50,6 +53,10 @@ let routeConfig;
|
|
|
50
53
|
let prerenderAfterBuild;
|
|
51
54
|
/** True during `vite build --watch`, where re-rendering every route is noise. */
|
|
52
55
|
let isWatch = false;
|
|
56
|
+
/** Whether navigations are wrapped in React's ViewTransition — see options. */
|
|
57
|
+
let viewTransitions = false;
|
|
58
|
+
/** Whether a service worker is generated and registered — see options. */
|
|
59
|
+
let offline = false;
|
|
53
60
|
/** Host functions to generate stubs for — see RscKitOptions.hostActions. */
|
|
54
61
|
let hostActions;
|
|
55
62
|
/**
|
|
@@ -168,6 +175,7 @@ function resolvePaths(options) {
|
|
|
168
175
|
// Vite's own default. Nitro overrides it with .output/public, which is why
|
|
169
176
|
// there is nothing here to configure.
|
|
170
177
|
publicAssetsDir = resolve(join(projectRoot, 'dist/client'));
|
|
178
|
+
typesDir = join(projectRoot, '.rsc-kit');
|
|
171
179
|
hotFile = options.hotFile || process.env.RSC_HOT_FILE || '';
|
|
172
180
|
hostCallOptions = options.hostCall;
|
|
173
181
|
packageDir = resolve(options.packageDir || process.env.RSC_PACKAGE_DIR || thisDir());
|
|
@@ -190,6 +198,8 @@ function resolvePaths(options) {
|
|
|
190
198
|
// A host driving the build out of process cannot pass an option, and may
|
|
191
199
|
// prerender itself afterwards with paths only it knows.
|
|
192
200
|
prerenderAfterBuild = options.prerender ?? process.env.RSC_PRERENDER !== '0';
|
|
201
|
+
viewTransitions = options.viewTransitions === true;
|
|
202
|
+
offline = options.offline === true;
|
|
193
203
|
hostActions = options.hostActions ?? fileHostActions(projectRoot);
|
|
194
204
|
}
|
|
195
205
|
function log(...args) {
|
|
@@ -378,14 +388,57 @@ function routeManifest() {
|
|
|
378
388
|
intercepts,
|
|
379
389
|
};
|
|
380
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* Say so when the typechecker cannot see the declarations just written.
|
|
393
|
+
*
|
|
394
|
+
* Moving them out of the source directory buys a tidy `src/` and costs this:
|
|
395
|
+
* `include` used to cover them by accident, and now it has to name `.rsc-kit`.
|
|
396
|
+
* A project that does not is not broken in any way it can notice — every file
|
|
397
|
+
* is written, every build passes, and `Link` quietly takes `string` again
|
|
398
|
+
* instead of the route union, so a link to a page that does not exist compiles
|
|
399
|
+
* and 404s in the browser.
|
|
400
|
+
*
|
|
401
|
+
* A guess, deliberately, and only ever a warning: `include` is one of several
|
|
402
|
+
* ways a tsconfig can reach a file, `extends` can supply it, and a project
|
|
403
|
+
* with no tsconfig at all is not doing this checking anyway. Wrong here costs
|
|
404
|
+
* a line of output; silent costs the types.
|
|
405
|
+
*/
|
|
406
|
+
function warnIfTypesUnreachable() {
|
|
407
|
+
const config = join(projectRoot, 'tsconfig.json');
|
|
408
|
+
if (!existsSync(config))
|
|
409
|
+
return;
|
|
410
|
+
try {
|
|
411
|
+
// Comments are legal in a tsconfig and JSON.parse does not take them.
|
|
412
|
+
const text = readFileSync(config, 'utf-8').replace(/\/\*[\s\S]*?\*\/|(^|\s)\/\/.*$/gm, '$1');
|
|
413
|
+
const include = JSON.parse(text).include;
|
|
414
|
+
if (!Array.isArray(include))
|
|
415
|
+
return;
|
|
416
|
+
if (include.some((entry) => typeof entry === 'string' && entry.includes('.rsc-kit')))
|
|
417
|
+
return;
|
|
418
|
+
log(`tsconfig.json does not include .rsc-kit, where the generated types are written.\n` +
|
|
419
|
+
` Add ".rsc-kit/**/*" to "include", or typed routes and rpc() fall back to string.`);
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
// An unparseable tsconfig is the project's own problem, not this one's.
|
|
423
|
+
}
|
|
424
|
+
}
|
|
381
425
|
// ── What the app imports ─────────────────────────────────────────────────────
|
|
382
426
|
/**
|
|
383
427
|
* Write the modules the app's source imports but nobody writes by hand.
|
|
384
428
|
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
* relative path,
|
|
388
|
-
*
|
|
429
|
+
* Two destinations, and the difference is whether anything imports the file.
|
|
430
|
+
*
|
|
431
|
+
* The stubs are imported by relative path, so they have to sit in the source
|
|
432
|
+
* directory — that path is the app's, and only the build knows it.
|
|
433
|
+
*
|
|
434
|
+
* The declarations are not imported by anything. They are ambient, which needs
|
|
435
|
+
* them inside the project and nothing more, so they go in `.rsc-kit/` at the
|
|
436
|
+
* root: four generated files in `src/` sat among the app's own and had to be
|
|
437
|
+
* gitignored one by one, and every one of them was noise beside the pages.
|
|
438
|
+
*
|
|
439
|
+
* Ambient still means the typechecker has to be told the directory exists —
|
|
440
|
+
* see the warning below, because a tsconfig that does not include it turns
|
|
441
|
+
* every one of these into a file nobody reads.
|
|
389
442
|
*
|
|
390
443
|
* Rewritten on every run. The failure they prevent is invisible at build
|
|
391
444
|
* time — a stale stub calls a global that has since been renamed, and only
|
|
@@ -393,26 +446,28 @@ function routeManifest() {
|
|
|
393
446
|
*/
|
|
394
447
|
function writeHostBindings(manifest) {
|
|
395
448
|
mkdirSync(sourceDir, { recursive: true });
|
|
449
|
+
mkdirSync(typesDir, { recursive: true });
|
|
396
450
|
// The global is installed at runtime, so nothing in app source declares it
|
|
397
451
|
// and a typecheck cannot see it. Written whether or not there are actions:
|
|
398
452
|
// server components call it directly too.
|
|
399
|
-
writeFileSync(join(
|
|
453
|
+
writeFileSync(join(typesDir, 'rsc-env.d.ts'), renderHostGlobalTypes());
|
|
400
454
|
// The engine's own ambient types, copied where the app's typechecker will
|
|
401
455
|
// see them. Deliberately a separate file from the one above: this one is
|
|
402
456
|
// the engine's and identical everywhere, that one is generated from how
|
|
403
457
|
// this host is configured.
|
|
404
458
|
// The urls this build found, so a link to a page that does not exist fails
|
|
405
459
|
// the typecheck instead of the browser.
|
|
406
|
-
writeFileSync(join(
|
|
460
|
+
writeFileSync(join(typesDir, 'rsc-routes.d.ts'), renderRouteTypes(manifest));
|
|
407
461
|
// The bundle the host imports is generated, so nothing declares it. Written
|
|
408
462
|
// here rather than left to the app: every app needs the identical file, and
|
|
409
463
|
// an app-authored one goes stale — the first version named only RscEngine,
|
|
410
464
|
// which typechecks a server and fails a prerender script.
|
|
411
|
-
writeFileSync(join(
|
|
465
|
+
writeFileSync(join(typesDir, 'rsc-engine.d.ts'), ENGINE_TYPES);
|
|
412
466
|
const engineTypes = join(packageDir, 'types.d.ts');
|
|
413
467
|
if (existsSync(engineTypes)) {
|
|
414
|
-
writeFileSync(join(
|
|
468
|
+
writeFileSync(join(typesDir, 'rsc-types.d.ts'), readFileSync(engineTypes, 'utf-8'));
|
|
415
469
|
}
|
|
470
|
+
warnIfTypesUnreachable();
|
|
416
471
|
const target = join(sourceDir, 'server-actions.generated.ts');
|
|
417
472
|
// A host with no functions of its own leaves no file behind: kept, its
|
|
418
473
|
// stubs would go on naming targets the host has stopped answering for.
|
|
@@ -471,6 +526,200 @@ function reportAllDynamic() {
|
|
|
471
526
|
|
|
472
527
|
${routes.length} dynamic — prerendering is off`);
|
|
473
528
|
}
|
|
529
|
+
/**
|
|
530
|
+
* The service worker, written into the client output at build time.
|
|
531
|
+
*
|
|
532
|
+
* Scoped to the site root because it is served from there, so it sees every
|
|
533
|
+
* navigation. Three kinds of request, three answers:
|
|
534
|
+
*
|
|
535
|
+
* Hashed assets are immutable by construction — the name changes when the
|
|
536
|
+
* bytes do — so they are answered from the cache and only fetched once ever.
|
|
537
|
+
*
|
|
538
|
+
* Documents and payloads go to the network first and fall back to the cache,
|
|
539
|
+
* because a page whose data moved on should say so while there is a network to
|
|
540
|
+
* ask. Only the fallback is what makes a reload work with none.
|
|
541
|
+
*
|
|
542
|
+
* A document and its payload share a url and differ by header. They do not
|
|
543
|
+
* collide, because the server sends `Vary: X-RSC, ...` and the Cache API
|
|
544
|
+
* honours it when a Response is stored whole and matched with its Request.
|
|
545
|
+
*
|
|
546
|
+
* Nothing else is touched. An action is a POST and must never be answered from
|
|
547
|
+
* a cache; anything cross-origin is somebody else's to cache.
|
|
548
|
+
*/
|
|
549
|
+
export const SERVICE_WORKER = (version, precache) => `// GENERATED by rscKit() — do not edit.
|
|
550
|
+
const VERSION = ${JSON.stringify(version)}
|
|
551
|
+
const CACHE = 'rsc-kit-' + VERSION
|
|
552
|
+
const PRECACHE = ${JSON.stringify(precache, null, 2)}
|
|
553
|
+
|
|
554
|
+
self.addEventListener('install', (event) => {
|
|
555
|
+
// The new worker takes over rather than waiting for every tab to close.
|
|
556
|
+
// Safe here because assets are content-hashed: a page already open keeps
|
|
557
|
+
// asking for the names it was built with, and those are still cached under
|
|
558
|
+
// their own version until this activates and sweeps.
|
|
559
|
+
event.waitUntil(caches.open(CACHE).then((cache) => cache.addAll(PRECACHE)).then(() => self.skipWaiting()))
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
self.addEventListener('activate', (event) => {
|
|
563
|
+
event.waitUntil(
|
|
564
|
+
caches
|
|
565
|
+
.keys()
|
|
566
|
+
.then((keys) => Promise.all(keys.filter((k) => k.startsWith('rsc-kit-') && k !== CACHE).map((k) => caches.delete(k))))
|
|
567
|
+
.then(() => self.clients.claim()),
|
|
568
|
+
)
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
const immutable = (url) => url.pathname.startsWith('/assets/') || /-[A-Za-z0-9_-]{8,}\\.[a-z]+$/.test(url.pathname)
|
|
572
|
+
|
|
573
|
+
// Whether a response may be kept at all.
|
|
574
|
+
//
|
|
575
|
+
// \`no-store\` is not advice here, it is the answer. A query defaults to
|
|
576
|
+
// \`private, no-store\` precisely because it may read the session, and a worker
|
|
577
|
+
// that files it by url alone would serve one visitor's answer to whoever signs
|
|
578
|
+
// in next — the Cache API has no notion of who asked. A live response is worse
|
|
579
|
+
// again: the clone goes on streaming long after the page that opened it is
|
|
580
|
+
// gone.
|
|
581
|
+
//
|
|
582
|
+
// Checked before every put, rather than only for the paths that happen to reach
|
|
583
|
+
// a query today. A new cacheable route added later must not have to remember
|
|
584
|
+
// this.
|
|
585
|
+
const mayStore = (response) =>
|
|
586
|
+
response.ok && !(response.headers.get('Cache-Control') || '').includes('no-store')
|
|
587
|
+
|
|
588
|
+
// What a response is filed under. A payload request carries headers the url
|
|
589
|
+
// does not, so the url is where they have to go.
|
|
590
|
+
const keyFor = (request) => {
|
|
591
|
+
if (!request.headers.get('X-RSC')) return request
|
|
592
|
+
|
|
593
|
+
const url = new URL(request.url)
|
|
594
|
+
url.searchParams.set('__rsc', request.headers.get('X-RSC-Segments') || '')
|
|
595
|
+
|
|
596
|
+
return new Request(url, { headers: request.headers })
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
self.addEventListener('fetch', (event) => {
|
|
600
|
+
const request = event.request
|
|
601
|
+
const url = new URL(request.url)
|
|
602
|
+
|
|
603
|
+
if (request.method !== 'GET' || url.origin !== self.location.origin) return
|
|
604
|
+
|
|
605
|
+
if (immutable(url)) {
|
|
606
|
+
event.respondWith(
|
|
607
|
+
caches.match(request).then(
|
|
608
|
+
(hit) =>
|
|
609
|
+
hit ??
|
|
610
|
+
fetch(request).then((response) => {
|
|
611
|
+
if (mayStore(response)) {
|
|
612
|
+
const copy = response.clone()
|
|
613
|
+
caches.open(CACHE).then((cache) => cache.put(request, copy))
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return response
|
|
617
|
+
}),
|
|
618
|
+
),
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
return
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
event.respondWith(
|
|
625
|
+
fetch(request)
|
|
626
|
+
.then((response) => {
|
|
627
|
+
if (mayStore(response)) {
|
|
628
|
+
const copy = response.clone()
|
|
629
|
+
caches.open(CACHE).then((cache) => cache.put(keyFor(request), copy))
|
|
630
|
+
|
|
631
|
+
// A document is not enough to boot from. The client hydrates from a
|
|
632
|
+
// payload it fetches for itself, and on a first visit that request
|
|
633
|
+
// happens before this worker controls the page — so it is never
|
|
634
|
+
// cached, and a reload with no network has the markup and nothing to
|
|
635
|
+
// hydrate it with.
|
|
636
|
+
//
|
|
637
|
+
// \`X-RSC: true\` and no segments header is exactly what a fresh boot
|
|
638
|
+
// sends, which is what makes this entry the one it finds: the server
|
|
639
|
+
// varies on those, and the Cache API matches on the same.
|
|
640
|
+
if (request.mode === 'navigate') {
|
|
641
|
+
const warm = new Request(request.url, { headers: { 'X-RSC': 'true' } })
|
|
642
|
+
|
|
643
|
+
fetch(warm)
|
|
644
|
+
.then((payload) => {
|
|
645
|
+
if (mayStore(payload)) {
|
|
646
|
+
caches.open(CACHE).then((cache) => cache.put(keyFor(warm), payload))
|
|
647
|
+
}
|
|
648
|
+
})
|
|
649
|
+
.catch(() => {})
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// And the other way round. Moving between pages fetches payloads and
|
|
653
|
+
// never documents, so a page reached only by a link had nothing to
|
|
654
|
+
// serve when someone reloaded its url — it navigated fine and then
|
|
655
|
+
// died on refresh, which is the half of offline nobody would trust.
|
|
656
|
+
//
|
|
657
|
+
// Once per url: the document is fetched only when the cache has none,
|
|
658
|
+
// so this costs one extra request the first time a page is visited
|
|
659
|
+
// rather than one on every navigation to it.
|
|
660
|
+
if (request.headers.get('X-RSC')) {
|
|
661
|
+
const document = new Request(request.url)
|
|
662
|
+
|
|
663
|
+
caches.open(CACHE).then(async (cache) => {
|
|
664
|
+
if (await cache.match(document)) return
|
|
665
|
+
|
|
666
|
+
const fresh = await fetch(document).catch(() => null)
|
|
667
|
+
|
|
668
|
+
if (fresh && mayStore(fresh)) await cache.put(document, fresh)
|
|
669
|
+
})
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return response
|
|
674
|
+
})
|
|
675
|
+
.catch(async () => {
|
|
676
|
+
const hit = await caches.match(keyFor(request))
|
|
677
|
+
|
|
678
|
+
if (hit) return hit
|
|
679
|
+
|
|
680
|
+
// Nothing cached for this url and no network to ask. Falling back to
|
|
681
|
+
// the cached root was worse than failing: the document IS the page
|
|
682
|
+
// here, so the visitor got the home page's markup under the address
|
|
683
|
+
// they asked for, and it did not hydrate — a wrong page pretending to
|
|
684
|
+
// be the right one. Letting it fail says what is true, and a page
|
|
685
|
+
// already open is unaffected.
|
|
686
|
+
return Response.error()
|
|
687
|
+
}),
|
|
688
|
+
)
|
|
689
|
+
})
|
|
690
|
+
`;
|
|
691
|
+
/**
|
|
692
|
+
* Write the worker beside the assets it caches.
|
|
693
|
+
*
|
|
694
|
+
* The version is a hash of what is being precached rather than a build id from
|
|
695
|
+
* the environment. The names are content-hashed already, so a build that
|
|
696
|
+
* changed nothing produces the same list and leaves the visitor's cache alone,
|
|
697
|
+
* and a build that changed anything produces a different one — which is the
|
|
698
|
+
* whole of cache invalidation, without asking anyone to set a variable.
|
|
699
|
+
*/
|
|
700
|
+
function writeServiceWorker(clientDir) {
|
|
701
|
+
if (!existsSync(clientDir))
|
|
702
|
+
return;
|
|
703
|
+
const files = [];
|
|
704
|
+
const walk = (dir, prefix) => {
|
|
705
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
706
|
+
// Not itself, and not a map: a debugger asks for those, a visitor does
|
|
707
|
+
// not, and precaching them doubles what an install costs.
|
|
708
|
+
if (entry.name === 'sw.js' || entry.name.endsWith('.map'))
|
|
709
|
+
continue;
|
|
710
|
+
const path = join(dir, entry.name);
|
|
711
|
+
if (entry.isDirectory())
|
|
712
|
+
walk(path, `${prefix}${entry.name}/`);
|
|
713
|
+
else
|
|
714
|
+
files.push(`${prefix}${entry.name}`);
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
walk(clientDir, '');
|
|
718
|
+
const precache = ['/', ...files.map((file) => `/${file}`)].sort();
|
|
719
|
+
const version = createHash('sha256').update(precache.join('\n')).digest('hex').slice(0, 12);
|
|
720
|
+
writeFileSync(join(clientDir, 'sw.js'), SERVICE_WORKER(version, precache));
|
|
721
|
+
log(`offline: ${precache.length} files precached as rsc-kit-${version}`);
|
|
722
|
+
}
|
|
474
723
|
/**
|
|
475
724
|
* The rsc bundle the build just wrote, whatever it decided to call it.
|
|
476
725
|
*
|
|
@@ -942,6 +1191,7 @@ import { createRscHandler } from ${JSON.stringify(join(packageDir, "host"))}
|
|
|
942
1191
|
import { httpHostCalls } from ${JSON.stringify(join(packageDir, 'hostCalls'))}
|
|
943
1192
|
import { prerenderedBeside } from ${JSON.stringify(join(packageDir, 'files'))}
|
|
944
1193
|
import { renderToReadableStream, decodeReply, loadServerAction } from '@vitejs/plugin-rsc/rsc'
|
|
1194
|
+
import { isQuery, queryCacheControl } from ${JSON.stringify(join(packageDir, 'query'))}
|
|
945
1195
|
import { Suspense, createElement, Fragment } from 'react'
|
|
946
1196
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
947
1197
|
${imports.join('\n')}
|
|
@@ -1629,6 +1879,44 @@ async function renderRevalidated(target: string, page: PageContext): Promise<unk
|
|
|
1629
1879
|
})
|
|
1630
1880
|
}
|
|
1631
1881
|
|
|
1882
|
+
/**
|
|
1883
|
+
* Answer one read.
|
|
1884
|
+
*
|
|
1885
|
+
* Only functions declared with query() are reachable. The id comes off the url,
|
|
1886
|
+
* and every registered server action has one — without the mark, this address
|
|
1887
|
+
* would invoke mutations over GET, for anyone who can fetch it.
|
|
1888
|
+
*/
|
|
1889
|
+
export async function handleQuery(
|
|
1890
|
+
id: string,
|
|
1891
|
+
args: string,
|
|
1892
|
+
report?: (error: unknown) => string,
|
|
1893
|
+
): Promise<{ stream: ReadableStream; cacheControl: string } | null> {
|
|
1894
|
+
applyHost()
|
|
1895
|
+
|
|
1896
|
+
let fn: unknown
|
|
1897
|
+
|
|
1898
|
+
try {
|
|
1899
|
+
fn = await loadServerAction(id)
|
|
1900
|
+
} catch {
|
|
1901
|
+
return null
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
// Null for both, deliberately. Saying "that exists but is not a query" tells
|
|
1905
|
+
// whoever is probing this endpoint which ids are real actions, and the ids
|
|
1906
|
+
// are stable across a build.
|
|
1907
|
+
if (!isQuery(fn)) return null
|
|
1908
|
+
|
|
1909
|
+
const decoded = (await decodeReply(args)) as unknown[]
|
|
1910
|
+
const result = await (fn as (...a: unknown[]) => unknown)(...decoded)
|
|
1911
|
+
|
|
1912
|
+
return {
|
|
1913
|
+
stream: renderToReadableStream(result, {
|
|
1914
|
+
onError: (error: unknown) => (report ? report(error) : 'Query failed.'),
|
|
1915
|
+
}),
|
|
1916
|
+
cacheControl: queryCacheControl(fn),
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1632
1920
|
export async function handleAction(
|
|
1633
1921
|
actionId: string,
|
|
1634
1922
|
body: string | FormData | Uint8Array,
|
|
@@ -1675,6 +1963,19 @@ export async function handleAction(
|
|
|
1675
1963
|
|
|
1676
1964
|
const args = (await decodeReply(decodable)) as unknown[]
|
|
1677
1965
|
const action = await loadServerAction(actionId)
|
|
1966
|
+
|
|
1967
|
+
// A query reached the ACTION endpoint, which means it was called directly
|
|
1968
|
+
// rather than through fetchQuery — so it went out as a POST and none of the
|
|
1969
|
+
// reasons it was marked a read apply to it. It still works, which is the
|
|
1970
|
+
// problem: nothing else would ever mention it. Only in development, and only
|
|
1971
|
+
// a warning, because the call is not wrong, just not what was asked for.
|
|
1972
|
+
if (isQuery(action) && import.meta.env?.DEV) {
|
|
1973
|
+
console.warn(
|
|
1974
|
+
'[rsc-kit] ' + actionId + ' is a query but was called directly, so it was sent as a POST. ' +
|
|
1975
|
+
'Call it through fetchQuery() to send a GET.',
|
|
1976
|
+
)
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1678
1979
|
const result = await (action as (...a: unknown[]) => unknown)(...args)
|
|
1679
1980
|
|
|
1680
1981
|
// Read after the action has run: what it invalidated is only known once its
|
|
@@ -2080,6 +2381,7 @@ export default async function handler(request: Request): Promise<Response> {
|
|
|
2080
2381
|
handleRscPprShell,
|
|
2081
2382
|
handleRscResume,
|
|
2082
2383
|
handleAction,
|
|
2384
|
+
handleQuery,
|
|
2083
2385
|
resolveMetadata,
|
|
2084
2386
|
runRouteMiddleware,
|
|
2085
2387
|
} as never,
|
|
@@ -2254,7 +2556,25 @@ if (import.meta.hot) {
|
|
|
2254
2556
|
void refresh('all')
|
|
2255
2557
|
})
|
|
2256
2558
|
}
|
|
2257
|
-
|
|
2559
|
+
${offline
|
|
2560
|
+
? `
|
|
2561
|
+
// Registered after load rather than during it, so fetching and installing the
|
|
2562
|
+
// worker competes with nothing the first visit actually needs. The second
|
|
2563
|
+
// visit is the one it is for.
|
|
2564
|
+
//
|
|
2565
|
+
// Not in development: the dev server is the thing you are editing, and a
|
|
2566
|
+
// worker answering from a cache in front of it turns every edit into a
|
|
2567
|
+
// question about which copy you are looking at.
|
|
2568
|
+
if ('serviceWorker' in navigator && import.meta.env.PROD) {
|
|
2569
|
+
window.addEventListener('load', () => {
|
|
2570
|
+
void navigator.serviceWorker.register('/sw.js').catch(() => {
|
|
2571
|
+
// A worker that will not register is not a reason for the page to fail.
|
|
2572
|
+
// The app works; it just will not survive being reloaded offline.
|
|
2573
|
+
})
|
|
2574
|
+
})
|
|
2575
|
+
}
|
|
2576
|
+
`
|
|
2577
|
+
: ''}`;
|
|
2258
2578
|
}
|
|
2259
2579
|
/**
|
|
2260
2580
|
* Intercepted URL patterns, published by the host before the build.
|
|
@@ -2384,7 +2704,23 @@ export function rscKit(options = {}) {
|
|
|
2384
2704
|
}
|
|
2385
2705
|
components.clear();
|
|
2386
2706
|
discover(appDir);
|
|
2387
|
-
|
|
2707
|
+
// Silent when it worked. The names were printed on every dev start and
|
|
2708
|
+
// every build — thirty of them for a middling app, above the output that
|
|
2709
|
+
// actually says something, and the build's classification table lists
|
|
2710
|
+
// every route anyway.
|
|
2711
|
+
//
|
|
2712
|
+
// Nothing found is the case worth a word, because the app still builds:
|
|
2713
|
+
// a server with no routes answers 404 to everything, which reads as a
|
|
2714
|
+
// routing bug rather than as an empty directory. Fatal for a build,
|
|
2715
|
+
// said out loud for a dev server — where deleting the last page while
|
|
2716
|
+
// editing is a state to pass through, not to be thrown out of.
|
|
2717
|
+
if (components.size === 0) {
|
|
2718
|
+
const message = `No routes under ${appDir}. A directory with a page.tsx in it is a route; ` +
|
|
2719
|
+
'without one there is nothing to serve.';
|
|
2720
|
+
if (env.command === 'build')
|
|
2721
|
+
throw new Error(`[rsc-kit] ${message}`);
|
|
2722
|
+
log(message);
|
|
2723
|
+
}
|
|
2388
2724
|
const loadingErrors = validateLoadingBoundaries();
|
|
2389
2725
|
if (loadingErrors.length) {
|
|
2390
2726
|
throw new Error('[rsc-kit] A page that blocks before it can paint needs a loading.tsx boundary.\n\n' +
|
|
@@ -2446,6 +2782,10 @@ export function rscKit(options = {}) {
|
|
|
2446
2782
|
*/
|
|
2447
2783
|
define: {
|
|
2448
2784
|
'process.env.NODE_ENV': JSON.stringify(env.mode === 'development' ? 'development' : 'production'),
|
|
2785
|
+
// A constant, so the boundary and its import fall out of the bundle
|
|
2786
|
+
// entirely when this is off rather than shipping a branch nobody takes.
|
|
2787
|
+
__RSC_VIEW_TRANSITIONS__: JSON.stringify(viewTransitions),
|
|
2788
|
+
__RSC_OFFLINE__: JSON.stringify(offline),
|
|
2449
2789
|
},
|
|
2450
2790
|
/*
|
|
2451
2791
|
* This package's client modules are served as source, never
|
|
@@ -2666,6 +3006,8 @@ export function rscKit(options = {}) {
|
|
|
2666
3006
|
? join(dirname(clientOut), 'server', NITRO_STATIC_DIR)
|
|
2667
3007
|
: join(outDir, NITRO_STATIC_DIR);
|
|
2668
3008
|
await prerenderAfterBundles(bundle, staticDir, clientOut ?? publicAssetsDir);
|
|
3009
|
+
if (offline)
|
|
3010
|
+
writeServiceWorker(clientOut ?? publicAssetsDir);
|
|
2669
3011
|
},
|
|
2670
3012
|
configResolved(config) {
|
|
2671
3013
|
isWatch = config.build?.watch != null;
|