@rsc-kit/core 0.5.0 → 0.7.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/dist/build-rsc-vite.d.ts +1 -1
- package/dist/build-rsc-vite.js +5 -5
- package/dist/build-rsc-vite.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/devServer.js +1 -1
- package/dist/devServer.js.map +1 -1
- package/dist/export.d.ts.map +1 -1
- package/dist/export.js +20 -0
- package/dist/export.js.map +1 -1
- package/dist/files.d.ts +20 -13
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +52 -47
- package/dist/files.js.map +1 -1
- package/dist/host.d.ts +22 -0
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +37 -0
- package/dist/host.js.map +1 -1
- package/dist/prerender.js +1 -1
- package/dist/prerender.js.map +1 -1
- package/dist/vite.d.ts +2 -6
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +361 -135
- package/dist/vite.js.map +1 -1
- package/package.json +1 -9
- package/dist/compile.d.ts +0 -55
- package/dist/compile.d.ts.map +0 -1
- package/dist/compile.js +0 -141
- package/dist/compile.js.map +0 -1
- package/dist/serve.d.ts +0 -55
- package/dist/serve.d.ts.map +0 -1
- package/dist/serve.js +0 -40
- package/dist/serve.js.map +0 -1
package/dist/vite.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// that global is called, and how a route declares dynamic props, are options —
|
|
6
6
|
// nothing here knows or cares which backend is driving it.
|
|
7
7
|
//
|
|
8
|
-
// import {
|
|
9
|
-
// export default defineConfig({ plugins: [
|
|
8
|
+
// import { rscKit } from '<package>/vite'
|
|
9
|
+
// export default defineConfig({ plugins: [rscKit(), react({ compiler: true })] })
|
|
10
10
|
//
|
|
11
11
|
// The plugin discovers the app/ route tree, generates the three entries that
|
|
12
12
|
// carry the route composition and the worker's render contract, and supplies
|
|
@@ -19,7 +19,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
19
19
|
import rsc from '@vitejs/plugin-rsc';
|
|
20
20
|
import { loadEnv } from 'vite';
|
|
21
21
|
import { httpHostCalls } from './hostCalls.js';
|
|
22
|
-
// Resolved once per
|
|
22
|
+
// Resolved once per rscKit() call. One build runs in one process, so these are
|
|
23
23
|
// module state rather than threaded through every helper.
|
|
24
24
|
let projectRoot;
|
|
25
25
|
let sourceDir;
|
|
@@ -27,7 +27,6 @@ let outDir;
|
|
|
27
27
|
let appDir;
|
|
28
28
|
let genDir;
|
|
29
29
|
let publicAssetsDir;
|
|
30
|
-
let assetsBaseUrl;
|
|
31
30
|
let hotFile;
|
|
32
31
|
let hostCallOptions;
|
|
33
32
|
let packageDir;
|
|
@@ -36,7 +35,7 @@ let interceptManifestFile;
|
|
|
36
35
|
let packageAlias;
|
|
37
36
|
/** Dev-server origin; empty in a build. See devUrls.ts. */
|
|
38
37
|
let devOrigin;
|
|
39
|
-
/** 'server' or 'export' — see
|
|
38
|
+
/** 'server' or 'export' — see RscKitOptions.output. */
|
|
40
39
|
let output;
|
|
41
40
|
/** Where an exported site is written. */
|
|
42
41
|
let exportPath;
|
|
@@ -47,11 +46,11 @@ let exportPath;
|
|
|
47
46
|
*/
|
|
48
47
|
let staticPayloads;
|
|
49
48
|
let routeConfig;
|
|
50
|
-
/** Whether `vite build` freezes pages when it finishes — see
|
|
49
|
+
/** Whether `vite build` freezes pages when it finishes — see RscKitOptions. */
|
|
51
50
|
let prerenderAfterBuild;
|
|
52
51
|
/** True during `vite build --watch`, where re-rendering every route is noise. */
|
|
53
52
|
let isWatch = false;
|
|
54
|
-
/** Host functions to generate stubs for — see
|
|
53
|
+
/** Host functions to generate stubs for — see RscKitOptions.hostActions. */
|
|
55
54
|
let hostActions;
|
|
56
55
|
/**
|
|
57
56
|
* This file's directory.
|
|
@@ -148,10 +147,27 @@ function resolvePaths(options) {
|
|
|
148
147
|
// Generated entries live under the (in-project) out dir so module resolution
|
|
149
148
|
// can walk up to the project's node_modules (@vitejs/plugin-rsc, react, ...).
|
|
150
149
|
genDir = join(outDir, '.gen');
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
150
|
+
// Not a migration guard — those are not worth carrying in a pre-release.
|
|
151
|
+
// This is the silent-failure guard the rest of this file is written to be.
|
|
152
|
+
//
|
|
153
|
+
// Nitro publishes the assets and serves them from its own root. A prefix set
|
|
154
|
+
// here would be emitted into the markup and answered by nobody: every asset
|
|
155
|
+
// 404s while every page still renders, so the app arrives unstyled, never
|
|
156
|
+
// hydrates, and logs nothing anywhere. Vite does not typecheck a config, so
|
|
157
|
+
// removing the option from the type is not enough to stop it.
|
|
158
|
+
//
|
|
159
|
+
// There is deliberately no check for `nitro`. It is gone from the type, and
|
|
160
|
+
// a config still passing `nitro: true` is asking for exactly what it gets.
|
|
161
|
+
const removed = ['assetsDir', 'assetsUrl'].filter((key) => options[key] !== undefined);
|
|
162
|
+
if (removed.length > 0) {
|
|
163
|
+
throw new Error(`[rsc-kit] ${removed.join(' and ')} ${removed.length === 1 ? 'is' : 'are'} no longer an option.\n\n` +
|
|
164
|
+
'Nitro publishes the browser assets to .output/public and serves them from its own\n' +
|
|
165
|
+
'root, so there is no prefix to set. That directory is the deployment — point nginx\n' +
|
|
166
|
+
'or a CDN at it if something other than the app should serve them.');
|
|
167
|
+
}
|
|
168
|
+
// Vite's own default. Nitro overrides it with .output/public, which is why
|
|
169
|
+
// there is nothing here to configure.
|
|
170
|
+
publicAssetsDir = resolve(join(projectRoot, 'dist/client'));
|
|
155
171
|
hotFile = options.hotFile || process.env.RSC_HOT_FILE || '';
|
|
156
172
|
hostCallOptions = options.hostCall;
|
|
157
173
|
packageDir = resolve(options.packageDir || process.env.RSC_PACKAGE_DIR || thisDir());
|
|
@@ -177,7 +193,7 @@ function resolvePaths(options) {
|
|
|
177
193
|
hostActions = options.hostActions ?? fileHostActions(projectRoot);
|
|
178
194
|
}
|
|
179
195
|
function log(...args) {
|
|
180
|
-
console.error('[rsc-
|
|
196
|
+
console.error('[rsc-kit]', ...args);
|
|
181
197
|
}
|
|
182
198
|
// ── The route manifest ───────────────────────────────────────────────────────
|
|
183
199
|
/**
|
|
@@ -455,15 +471,42 @@ function reportAllDynamic() {
|
|
|
455
471
|
|
|
456
472
|
${routes.length} dynamic — prerendering is off`);
|
|
457
473
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
474
|
+
/**
|
|
475
|
+
* The rsc bundle the build just wrote, whatever it decided to call it.
|
|
476
|
+
*
|
|
477
|
+
* Two things were assumed here and both were wrong for somebody. The directory
|
|
478
|
+
* was assumed to be <outDir>/dist/rsc, which Nitro does not use — it builds the
|
|
479
|
+
* environment under node_modules/.nitro. And the file was assumed to be
|
|
480
|
+
* index.js, which Vite only emits for a package that declares `type: module`;
|
|
481
|
+
* everyone else got index.mjs. Either miss returned quietly, and a build that
|
|
482
|
+
* prerendered nothing printed exactly what a build with no pages to freeze
|
|
483
|
+
* prints.
|
|
484
|
+
*/
|
|
485
|
+
function resolveRscBundle(dir) {
|
|
486
|
+
for (const name of ['index.js', 'index.mjs']) {
|
|
487
|
+
const candidate = join(dir, name);
|
|
488
|
+
if (existsSync(candidate))
|
|
489
|
+
return candidate;
|
|
490
|
+
}
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
async function prerenderAfterBundles(bundle, staticDir, assetsDir) {
|
|
494
|
+
// A missing bundle used to be a silent `return`, and under Nitro it was the
|
|
495
|
+
// normal case: the path was assumed to be <outDir>/dist/rsc/index.js, Nitro
|
|
496
|
+
// builds the rsc environment somewhere else entirely, and so every Nitro app
|
|
497
|
+
// — every scaffolded app, and Laravel — prerendered nothing at all. No
|
|
498
|
+
// classification printed, no route frozen, and every page renders per
|
|
499
|
+
// visitor. The caller resolves the path from the environment now, so this is
|
|
500
|
+
// back to being the impossible case it reads as.
|
|
501
|
+
if (!bundle) {
|
|
502
|
+
throw new Error('[rsc-kit] The build produced no rsc bundle to prerender from.\n' +
|
|
503
|
+
'Prerendering renders the app, so it needs the bundle the build just wrote. ' +
|
|
504
|
+
'Build with prerender: false to render every page on demand instead.');
|
|
505
|
+
}
|
|
462
506
|
const [{ prerender, summary, legend }, { writeTo }] = await Promise.all([
|
|
463
507
|
import('./prerender.js'),
|
|
464
508
|
import('./files.js'),
|
|
465
509
|
]);
|
|
466
|
-
const staticDir = join(outDir, 'static');
|
|
467
510
|
// Cleared first: a route that changes classification between builds
|
|
468
511
|
// otherwise leaves its old shell on disk and the host goes on serving it.
|
|
469
512
|
// Nothing warns — the page loads, with content from the previous build.
|
|
@@ -488,10 +531,52 @@ ${legend(results)}
|
|
|
488
531
|
|
|
489
532
|
${summary(results)}`);
|
|
490
533
|
if (failed > 0) {
|
|
491
|
-
throw new Error(`[rsc-
|
|
534
|
+
throw new Error(`[rsc-kit] ${failed} route${failed === 1 ? '' : 's'} failed to render.\n` +
|
|
492
535
|
'Prerendering runs your app: whatever those pages need at render time has to be\n' +
|
|
493
536
|
'reachable from the build. Fix them, or build with prerender: false and render on demand.');
|
|
494
537
|
}
|
|
538
|
+
if (output === 'export')
|
|
539
|
+
await exportAfterPrerender(results, staticDir, assetsDir);
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Turn the frozen output into a directory a static host can serve.
|
|
543
|
+
*
|
|
544
|
+
* Part of the build rather than a script the app writes, for the same reason
|
|
545
|
+
* prerendering is: it needs the results the build already has, and the engine
|
|
546
|
+
* bundle they came from is somewhere only the build knows. That path used to be
|
|
547
|
+
* stable enough to hard-code in an example — `build/dist/rsc/index.js` — and it
|
|
548
|
+
* is not any more, because Nitro builds the rsc environment under
|
|
549
|
+
* node_modules. A script asking for it would be a script that breaks.
|
|
550
|
+
*
|
|
551
|
+
* RSC_EXPORT_FORCE writes the site anyway and reports what it left out, which
|
|
552
|
+
* is how an app is moved towards being exportable. Without it a route that
|
|
553
|
+
* could not be frozen fails the build, because a shell on a static host is a
|
|
554
|
+
* page that loads and then stays empty forever.
|
|
555
|
+
*/
|
|
556
|
+
async function exportAfterPrerender(results, staticDir, assetsDir) {
|
|
557
|
+
const [{ exportSite, NotExportable }, { writeTo, prerenderedFrom, copyAssets }] = await Promise.all([
|
|
558
|
+
import('./export.js'),
|
|
559
|
+
import('./files.js'),
|
|
560
|
+
]);
|
|
561
|
+
try {
|
|
562
|
+
const { pages, refused } = await exportSite({
|
|
563
|
+
results,
|
|
564
|
+
read: prerenderedFrom(staticDir),
|
|
565
|
+
write: writeTo(exportPath),
|
|
566
|
+
manifest: routeManifest(),
|
|
567
|
+
assets: copyAssets(join(assetsDir, 'assets'), exportPath, '/assets/'),
|
|
568
|
+
force: process.env.RSC_EXPORT_FORCE === '1',
|
|
569
|
+
});
|
|
570
|
+
console.log(`\n Exported ${pages} page${pages === 1 ? '' : 's'} to ${relative(projectRoot, exportPath)}`);
|
|
571
|
+
if (refused.length > 0) {
|
|
572
|
+
console.log(` Left out ${refused.length}: ${refused.map((r) => r.url).join(', ')}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
if (!(error instanceof NotExportable))
|
|
577
|
+
throw error;
|
|
578
|
+
throw new Error(`[rsc-kit] ${error.message}`);
|
|
579
|
+
}
|
|
495
580
|
}
|
|
496
581
|
/** `/posts/[slug]` — the pattern, in the shape the app writes its links in. */
|
|
497
582
|
function patternOf(segments) {
|
|
@@ -664,6 +749,160 @@ function hasStaticParams(absPath) {
|
|
|
664
749
|
return /export\s+((async\s+)?function\s+generateStaticParams|const\s+generateStaticParams)/.test(src);
|
|
665
750
|
}
|
|
666
751
|
// ── Codegen ──────────────────────────────────────────────────────────────────
|
|
752
|
+
/**
|
|
753
|
+
* The dev fall-through, emitted only when there is a backend to hand a url to.
|
|
754
|
+
*
|
|
755
|
+
* A JavaScript host has no backend — it IS the backend — so its entry should
|
|
756
|
+
* not carry this code, its constants, or the branch that tests them. Nothing
|
|
757
|
+
* generated is cheaper than something generated that returns early, and it
|
|
758
|
+
* keeps every backend-shaped idea out of a runtime that has no backend.
|
|
759
|
+
*/
|
|
760
|
+
const FALLBACK_CONSTS = `const FALLBACK_ORIGIN = __ORIGIN__
|
|
761
|
+
const FALLBACK_MARKER = 'x-rsc-renderer-fallback'
|
|
762
|
+
const PROXIED_MARKER = 'x-rsc-proxied-by-backend'
|
|
763
|
+
`;
|
|
764
|
+
const FALLBACK_BODY = ` const answer = await devHandler(request)
|
|
765
|
+
|
|
766
|
+
if (answer) return answer
|
|
767
|
+
|
|
768
|
+
// Nothing here owns this url. In development the backend usually does — a
|
|
769
|
+
// Blade page, /login, a webhook, an uploaded file under /storage — so the
|
|
770
|
+
// request is handed on rather than refused, and this origin is the whole
|
|
771
|
+
// application instead of the RSC half of it.
|
|
772
|
+
//
|
|
773
|
+
// FALLBACK_MARKER is what stops this looping. The backend's own fallback
|
|
774
|
+
// forwards what it cannot route BACK to this server, so without a marker a
|
|
775
|
+
// url neither side owns would bounce between them until something gave out.
|
|
776
|
+
// Seeing it, the backend answers 404 itself.
|
|
777
|
+
// Came from the backend's own proxy, so it has already been through that
|
|
778
|
+
// route table and the answer there was no. Sending it back asks the same
|
|
779
|
+
// question a second time.
|
|
780
|
+
if (!FALLBACK_ORIGIN || request.headers.has(PROXIED_MARKER)) {
|
|
781
|
+
return new Response('Not found', { status: 404 })
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Built from the origin rather than by assigning onto a copy of this url.
|
|
785
|
+
// The URL host setter keeps whatever port is already there when the value it
|
|
786
|
+
// is given has none, so a portless backend — every Herd or Valet site —
|
|
787
|
+
// would inherit the dev server's own port and this server would call itself.
|
|
788
|
+
const here = new URL(request.url)
|
|
789
|
+
const target = new URL(here.pathname + here.search, FALLBACK_ORIGIN)
|
|
790
|
+
|
|
791
|
+
const headers = new Headers(request.headers)
|
|
792
|
+
|
|
793
|
+
// Never forwarded: a vhost server routes on it, so telling Herd the host is
|
|
794
|
+
// localhost:5173 means it has no such site and answers 404. fetch sets it
|
|
795
|
+
// from the target instead.
|
|
796
|
+
headers.delete('host')
|
|
797
|
+
headers.set(FALLBACK_MARKER, '1')
|
|
798
|
+
|
|
799
|
+
// What the browser actually asked for. Without these the backend generates
|
|
800
|
+
// absolute urls — url(), route(), redirects, form actions — against its own
|
|
801
|
+
// origin rather than this one, and a redirect walks the browser off this
|
|
802
|
+
// server onto the backend.
|
|
803
|
+
//
|
|
804
|
+
// They only take effect if the backend trusts this proxy: Laravel needs the
|
|
805
|
+
// renderer's address in trustProxies. Sent regardless, because a header an
|
|
806
|
+
// untrusting backend ignores costs nothing, and the alternative is that
|
|
807
|
+
// there is no way to get it right at all.
|
|
808
|
+
headers.set('x-forwarded-host', here.host)
|
|
809
|
+
headers.set('x-forwarded-proto', here.protocol.replace(':', ''))
|
|
810
|
+
|
|
811
|
+
const hasBody = request.method !== 'GET' && request.method !== 'HEAD'
|
|
812
|
+
|
|
813
|
+
try {
|
|
814
|
+
return await fetch(target, {
|
|
815
|
+
method: request.method,
|
|
816
|
+
headers,
|
|
817
|
+
body: hasBody ? request.body : undefined,
|
|
818
|
+
// A redirect is the backend's answer and belongs to the browser.
|
|
819
|
+
// Following it here would return the destination's body under this url.
|
|
820
|
+
redirect: 'manual',
|
|
821
|
+
...(hasBody ? { duplex: 'half' } : {}),
|
|
822
|
+
} as RequestInit)
|
|
823
|
+
} catch (error) {
|
|
824
|
+
return new Response(
|
|
825
|
+
// The cause, not just the wrapper: fetch reports every network failure
|
|
826
|
+
// as the same 'TypeError: fetch failed', and the refused address
|
|
827
|
+
// underneath it is the whole of the diagnosis.
|
|
828
|
+
'The backend at ' + FALLBACK_ORIGIN + ' is not answering: ' +
|
|
829
|
+
String((error as { cause?: unknown }).cause ?? error),
|
|
830
|
+
{ status: 502 },
|
|
831
|
+
)
|
|
832
|
+
}
|
|
833
|
+
`;
|
|
834
|
+
/**
|
|
835
|
+
* Wire host calls when Nitro is the server.
|
|
836
|
+
*
|
|
837
|
+
* Every other arrangement installs this from outside: the dev server does it in
|
|
838
|
+
* configureServer, and a generated server.ts passes hostCalls to
|
|
839
|
+
* createRscHandler. Under Nitro there is no such file — this module IS the
|
|
840
|
+
* server — so an rpc() page renders its loading fallback forever and the
|
|
841
|
+
* backend never hears from it. Silent, which is the worst kind.
|
|
842
|
+
*
|
|
843
|
+
* Read at request time rather than at build time: the endpoint belongs to the
|
|
844
|
+
* deployment, and baking it in would mean rebuilding to change where the
|
|
845
|
+
* backend is.
|
|
846
|
+
*/
|
|
847
|
+
/**
|
|
848
|
+
* What a generated server.ts passes, for the arrangement that has no server.ts.
|
|
849
|
+
*
|
|
850
|
+
* props: a page reading `params` gets its url params from the engine, and the
|
|
851
|
+
* query string is merged in here — `params.q` should mean the same thing
|
|
852
|
+
* whether it arrived in the path or after the ?. Only the Laravel template
|
|
853
|
+
* passed this before, so the other hosts quietly did not have it.
|
|
854
|
+
*
|
|
855
|
+
* version: the client compares it on every navigation and falls back to a full
|
|
856
|
+
* load when it changes. Without one, a browser keeps talking to a deployment
|
|
857
|
+
* that is gone — worst behind a CDN, where the shell it holds may already be
|
|
858
|
+
* older than the payloads it asks for.
|
|
859
|
+
*/
|
|
860
|
+
/**
|
|
861
|
+
* Where frozen pages live inside `.output/server`, written by the build and
|
|
862
|
+
* found by the generated entry. One constant because the two halves are in
|
|
863
|
+
* different processes — a name written down twice is a name that drifts, and
|
|
864
|
+
* the failure is silent: the server finds no directory, decides nothing was
|
|
865
|
+
* frozen, and renders every page live exactly as it did before.
|
|
866
|
+
*/
|
|
867
|
+
const NITRO_STATIC_DIR = 'rsc-static';
|
|
868
|
+
/**
|
|
869
|
+
* Serve what the build froze.
|
|
870
|
+
*
|
|
871
|
+
* `import.meta.env.PROD` rather than an unconditional reader: in development
|
|
872
|
+
* there is no build output to serve, and a stale one would be worse than none.
|
|
873
|
+
* Vite replaces it with a literal, so the dev bundle keeps no reference to it.
|
|
874
|
+
*/
|
|
875
|
+
const NITRO_PRERENDERED = ` prerendered: import.meta.env.PROD
|
|
876
|
+
? prerenderedBeside(import.meta.url, ${JSON.stringify(NITRO_STATIC_DIR)})
|
|
877
|
+
: undefined,
|
|
878
|
+
`;
|
|
879
|
+
const NITRO_HANDLER_OPTIONS = ` props: (match, request) => ({
|
|
880
|
+
...match.params,
|
|
881
|
+
...Object.fromEntries(new URL(request.url).searchParams),
|
|
882
|
+
}),
|
|
883
|
+
version: process.env.RSC_BUILD_VERSION,
|
|
884
|
+
`;
|
|
885
|
+
const NITRO_HOST_CALLS = `
|
|
886
|
+
let hostInstalled = false
|
|
887
|
+
|
|
888
|
+
function installHostCallsOnce(): void {
|
|
889
|
+
if (hostInstalled) return
|
|
890
|
+
hostInstalled = true
|
|
891
|
+
|
|
892
|
+
const origin = process.env.RSC_BACKEND ?? process.env.APP_URL
|
|
893
|
+
const secret = process.env.RSC_HOST_CALL_SECRET
|
|
894
|
+
|
|
895
|
+
// Both, or neither: a secret without a backend has nowhere to go, and a
|
|
896
|
+
// backend without one is refused at the door. See the dev server, which
|
|
897
|
+
// gates on exactly the same pair.
|
|
898
|
+
if (!origin || !secret) return
|
|
899
|
+
|
|
900
|
+
const path = process.env.RSC_HOST_CALL_PATH ?? '/__rsc/host-call'
|
|
901
|
+
|
|
902
|
+
installHostFn(httpHostCalls({ endpoint: origin.replace(/\\/$/, '') + path, secret }))
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
`;
|
|
667
906
|
function generateEntryRsc(fallbackOrigin = '') {
|
|
668
907
|
const imports = [];
|
|
669
908
|
const mapEntries = [];
|
|
@@ -691,7 +930,7 @@ function generateEntryRsc(fallbackOrigin = '') {
|
|
|
691
930
|
// The engine's own modules are named without an extension: this plugin runs
|
|
692
931
|
// from src/ in its own repo and from dist/ once published, and Vite resolves
|
|
693
932
|
// either. Naming .tsx here builds fine from source and fails after publish.
|
|
694
|
-
return `// GENERATED by
|
|
933
|
+
return `// GENERATED by rscKit() — do not edit.
|
|
695
934
|
import { SegmentBoundary } from ${JSON.stringify(join(packageDir, "js/SegmentBoundary"))}
|
|
696
935
|
import { DocumentTitle } from ${JSON.stringify(join(packageDir, "js/DocumentTitle"))}
|
|
697
936
|
import { SlotBoundary } from ${JSON.stringify(join(packageDir, "js/SlotBoundary"))}
|
|
@@ -700,6 +939,8 @@ import { PathnameProvider } from ${JSON.stringify(join(packageDir, "js/PathnameP
|
|
|
700
939
|
import { searchParams as requestSearchParams } from ${JSON.stringify(join(packageDir, "request"))}
|
|
701
940
|
import { redirectDigest } from ${JSON.stringify(join(packageDir, "redirectDigest"))}
|
|
702
941
|
import { createRscHandler } from ${JSON.stringify(join(packageDir, "host"))}
|
|
942
|
+
import { httpHostCalls } from ${JSON.stringify(join(packageDir, 'hostCalls'))}
|
|
943
|
+
import { prerenderedBeside } from ${JSON.stringify(join(packageDir, 'files'))}
|
|
703
944
|
import { renderToReadableStream, decodeReply, loadServerAction } from '@vitejs/plugin-rsc/rsc'
|
|
704
945
|
import { Suspense, createElement, Fragment } from 'react'
|
|
705
946
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
@@ -783,8 +1024,8 @@ export function installHostFn(fn: HostFn) {
|
|
|
783
1024
|
* a time and silently wrong for two: the second overwrites the first's saved
|
|
784
1025
|
* value, and both pages then call whichever closure assigned last, so
|
|
785
1026
|
* usedDynamicApis is recorded against the wrong page and routes are
|
|
786
|
-
* misclassified. Benign for a
|
|
787
|
-
*
|
|
1027
|
+
* misclassified. Benign for a host that installs none; wrong for any host
|
|
1028
|
+
* that does.
|
|
788
1029
|
*/
|
|
789
1030
|
const probeHost = new AsyncLocalStorage<(...args: unknown[]) => Promise<unknown>>()
|
|
790
1031
|
|
|
@@ -970,7 +1211,7 @@ function buildElement(
|
|
|
970
1211
|
|
|
971
1212
|
// Resolve route metadata into React elements. React 19 hoists <title>/<meta>
|
|
972
1213
|
// rendered anywhere in the tree into <head> — so the "vite way" for metadata is
|
|
973
|
-
// to render it as elements,
|
|
1214
|
+
// to render it as elements, rather than a backend injecting a <head> string.
|
|
974
1215
|
async function renderTree(
|
|
975
1216
|
component: string,
|
|
976
1217
|
props: Record<string, unknown>,
|
|
@@ -1202,7 +1443,7 @@ function flightOnError(error: unknown): string | undefined {
|
|
|
1202
1443
|
|
|
1203
1444
|
if (digest) return digest
|
|
1204
1445
|
|
|
1205
|
-
console.error('[rsc-
|
|
1446
|
+
console.error('[rsc-kit]', error)
|
|
1206
1447
|
|
|
1207
1448
|
return undefined
|
|
1208
1449
|
}
|
|
@@ -1622,13 +1863,13 @@ export async function handleRscPayload(
|
|
|
1622
1863
|
|
|
1623
1864
|
// PPR shell + classification (worker: rsc-ppr-shell — build-time).
|
|
1624
1865
|
//
|
|
1625
|
-
//
|
|
1866
|
+
// rpc() is replaced by a probe that records the call and never resolves, so
|
|
1626
1867
|
// every subtree depending on per-request data stays suspended while everything
|
|
1627
1868
|
// static renders normally. Whatever React has flushed when the deadline passes
|
|
1628
1869
|
// IS the shell: layouts, static markup, and Suspense fallbacks.
|
|
1629
1870
|
//
|
|
1630
1871
|
// The two flags this returns are what the prerender pipeline classifies on:
|
|
1631
|
-
// usedDynamicApis — the page touched
|
|
1872
|
+
// usedDynamicApis — the page touched rpc(), so it cannot be frozen whole
|
|
1632
1873
|
// timedOut — the render never finished, i.e. it is still waiting on
|
|
1633
1874
|
// data, so only the shell is safe to cache
|
|
1634
1875
|
// A page that sets neither is genuinely static and can be prerendered fully.
|
|
@@ -1660,13 +1901,14 @@ export async function handleRscPprShell(
|
|
|
1660
1901
|
// once at build time.
|
|
1661
1902
|
//
|
|
1662
1903
|
// A host that opened a callback socket for the build says true, and then the
|
|
1663
|
-
// call is made and its answer stored. That is the only way a
|
|
1664
|
-
// be static at all, since a host call is
|
|
1904
|
+
// call is made and its answer stored. That is the only way a page whose data
|
|
1905
|
+
// lives in the host can be static at all, since a host call is its only route
|
|
1906
|
+
// to that data — and
|
|
1665
1907
|
// connection() is how such a page opts back out.
|
|
1666
1908
|
//
|
|
1667
1909
|
// Asked of the caller rather than read from whatever host happens to be
|
|
1668
1910
|
// installed, because those are different statements: a test that installed
|
|
1669
|
-
// one is not a build that can reach
|
|
1911
|
+
// one is not a build that can reach the host.
|
|
1670
1912
|
canReachHost = false,
|
|
1671
1913
|
): Promise<{ shellHtml: string; clientChunks: unknown; timedOut: boolean; usedDynamicApis: boolean; error?: string }> {
|
|
1672
1914
|
// Deliberately no middleware here. The probe is asking whether the content is
|
|
@@ -1717,7 +1959,7 @@ export async function handleRscPprShell(
|
|
|
1717
1959
|
if (controller.signal.aborted) return undefined
|
|
1718
1960
|
|
|
1719
1961
|
renderFailure ??= e instanceof Error ? e.message : String(e)
|
|
1720
|
-
console.error('[rsc-
|
|
1962
|
+
console.error('[rsc-kit]', e)
|
|
1721
1963
|
|
|
1722
1964
|
return undefined
|
|
1723
1965
|
}
|
|
@@ -1810,17 +2052,21 @@ export async function handleRscPprShell(
|
|
|
1810
2052
|
* step and no NODE_ENV to match — this is React's development build because
|
|
1811
2053
|
* Vite is running in development.
|
|
1812
2054
|
*
|
|
1813
|
-
* Assets
|
|
1814
|
-
*
|
|
1815
|
-
*
|
|
2055
|
+
* Assets are deliberately absent: Vite serves its own.
|
|
2056
|
+
*
|
|
2057
|
+
* Frozen pages are absent *in development* — a frozen page is a build
|
|
2058
|
+
* artifact, and serving one here would hand back the last build's HTML for a
|
|
2059
|
+
* file just edited. Under Nitro this same file is also the production entry,
|
|
2060
|
+
* and reading that reason as applying to both is what left every built Nitro
|
|
2061
|
+
* server rendering live pages it had already frozen. The gate is the mode, not
|
|
2062
|
+
* the file.
|
|
1816
2063
|
*/
|
|
1817
|
-
|
|
1818
|
-
const FALLBACK_MARKER = 'x-rsc-renderer-fallback'
|
|
1819
|
-
const PROXIED_MARKER = 'x-rsc-proxied-by-backend'
|
|
1820
|
-
|
|
2064
|
+
${fallbackOrigin ? FALLBACK_CONSTS.replace('__ORIGIN__', JSON.stringify(fallbackOrigin)) : ''}${NITRO_HOST_CALLS}
|
|
1821
2065
|
let devHandler: ((request: Request) => Promise<Response | null>) | null = null
|
|
1822
2066
|
|
|
1823
2067
|
export default async function handler(request: Request): Promise<Response> {
|
|
2068
|
+
installHostCallsOnce()
|
|
2069
|
+
|
|
1824
2070
|
devHandler ??= createRscHandler({
|
|
1825
2071
|
engine: {
|
|
1826
2072
|
manifest,
|
|
@@ -1837,83 +2083,14 @@ export default async function handler(request: Request): Promise<Response> {
|
|
|
1837
2083
|
resolveMetadata,
|
|
1838
2084
|
runRouteMiddleware,
|
|
1839
2085
|
} as never,
|
|
1840
|
-
})
|
|
1841
|
-
|
|
1842
|
-
const answer = await devHandler(request)
|
|
2086
|
+
${NITRO_HANDLER_OPTIONS}${NITRO_PRERENDERED} })
|
|
1843
2087
|
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
// Nothing here owns this url. In development the backend usually does — a
|
|
1847
|
-
// Blade page, /login, a webhook, an uploaded file under /storage — so the
|
|
1848
|
-
// request is handed on rather than refused, and this origin is the whole
|
|
1849
|
-
// application instead of the RSC half of it.
|
|
1850
|
-
//
|
|
1851
|
-
// FALLBACK_MARKER is what stops this looping. The backend's own fallback
|
|
1852
|
-
// forwards what it cannot route BACK to this server, so without a marker a
|
|
1853
|
-
// url neither side owns would bounce between them until something gave out.
|
|
1854
|
-
// Seeing it, the backend answers 404 itself.
|
|
1855
|
-
// Came from the backend's own proxy, so it has already been through that
|
|
1856
|
-
// route table and the answer there was no. Sending it back asks the same
|
|
1857
|
-
// question a second time.
|
|
1858
|
-
if (!FALLBACK_ORIGIN || request.headers.has(PROXIED_MARKER)) {
|
|
1859
|
-
return new Response('Not found', { status: 404 })
|
|
1860
|
-
}
|
|
1861
|
-
|
|
1862
|
-
// Built from the origin rather than by assigning onto a copy of this url.
|
|
1863
|
-
// The URL host setter keeps whatever port is already there when the value it
|
|
1864
|
-
// is given has none, so a portless backend — every Herd or Valet site —
|
|
1865
|
-
// would inherit the dev server's own port and this server would call itself.
|
|
1866
|
-
const here = new URL(request.url)
|
|
1867
|
-
const target = new URL(here.pathname + here.search, FALLBACK_ORIGIN)
|
|
1868
|
-
|
|
1869
|
-
const headers = new Headers(request.headers)
|
|
1870
|
-
|
|
1871
|
-
// Never forwarded: a vhost server routes on it, so telling Herd the host is
|
|
1872
|
-
// localhost:5173 means it has no such site and answers 404. fetch sets it
|
|
1873
|
-
// from the target instead.
|
|
1874
|
-
headers.delete('host')
|
|
1875
|
-
headers.set(FALLBACK_MARKER, '1')
|
|
1876
|
-
|
|
1877
|
-
// What the browser actually asked for. Without these the backend generates
|
|
1878
|
-
// absolute urls — url(), route(), redirects, form actions — against its own
|
|
1879
|
-
// origin rather than this one, and a redirect walks the browser off this
|
|
1880
|
-
// server onto the backend.
|
|
1881
|
-
//
|
|
1882
|
-
// They only take effect if the backend trusts this proxy: Laravel needs the
|
|
1883
|
-
// renderer's address in trustProxies. Sent regardless, because a header an
|
|
1884
|
-
// untrusting backend ignores costs nothing, and the alternative is that
|
|
1885
|
-
// there is no way to get it right at all.
|
|
1886
|
-
headers.set('x-forwarded-host', here.host)
|
|
1887
|
-
headers.set('x-forwarded-proto', here.protocol.replace(':', ''))
|
|
1888
|
-
|
|
1889
|
-
const hasBody = request.method !== 'GET' && request.method !== 'HEAD'
|
|
1890
|
-
|
|
1891
|
-
try {
|
|
1892
|
-
return await fetch(target, {
|
|
1893
|
-
method: request.method,
|
|
1894
|
-
headers,
|
|
1895
|
-
body: hasBody ? request.body : undefined,
|
|
1896
|
-
// A redirect is the backend's answer and belongs to the browser.
|
|
1897
|
-
// Following it here would return the destination's body under this url.
|
|
1898
|
-
redirect: 'manual',
|
|
1899
|
-
...(hasBody ? { duplex: 'half' } : {}),
|
|
1900
|
-
} as RequestInit)
|
|
1901
|
-
} catch (error) {
|
|
1902
|
-
return new Response(
|
|
1903
|
-
// The cause, not just the wrapper: fetch reports every network failure
|
|
1904
|
-
// as the same 'TypeError: fetch failed', and the refused address
|
|
1905
|
-
// underneath it is the whole of the diagnosis.
|
|
1906
|
-
'The backend at ' + FALLBACK_ORIGIN + ' is not answering: ' +
|
|
1907
|
-
String((error as { cause?: unknown }).cause ?? error),
|
|
1908
|
-
{ status: 502 },
|
|
1909
|
-
)
|
|
1910
|
-
}
|
|
1911
|
-
}
|
|
2088
|
+
${fallbackOrigin ? FALLBACK_BODY : " return (await devHandler(request)) ?? new Response('Not found', { status: 404 })\n"}}
|
|
1912
2089
|
`;
|
|
1913
2090
|
}
|
|
1914
2091
|
function generateEntrySsr() {
|
|
1915
2092
|
const devUrls = join(packageDir, 'devUrls');
|
|
1916
|
-
return `// GENERATED by
|
|
2093
|
+
return `// GENERATED by rscKit() — do not edit.
|
|
1917
2094
|
import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr'
|
|
1918
2095
|
import { renderToReadableStream, resume } from 'react-dom/server.edge'
|
|
1919
2096
|
import { prerender } from 'react-dom/static.edge'
|
|
@@ -1945,7 +2122,7 @@ export async function handleSsr(
|
|
|
1945
2122
|
const html = await renderToReadableStream(root as any, {
|
|
1946
2123
|
bootstrapScriptContent,
|
|
1947
2124
|
nonce,
|
|
1948
|
-
onError: onError ?? ((error: unknown) => { console.error('[rsc-
|
|
2125
|
+
onError: onError ?? ((error: unknown) => { console.error('[rsc-kit:ssr]', error) }),
|
|
1949
2126
|
})
|
|
1950
2127
|
|
|
1951
2128
|
return DEV_ORIGIN ? rewriteViteDevUrlStream(html, DEV_ORIGIN) : html
|
|
@@ -2010,13 +2187,34 @@ export async function handleSsrResume(
|
|
|
2010
2187
|
|
|
2011
2188
|
const html = await resume(root as any, postponed as any, {
|
|
2012
2189
|
nonce,
|
|
2013
|
-
onError: (error: unknown) => { console.error('[rsc-
|
|
2190
|
+
onError: (error: unknown) => { console.error('[rsc-kit:resume]', error) },
|
|
2014
2191
|
})
|
|
2015
2192
|
|
|
2016
2193
|
return DEV_ORIGIN ? rewriteViteDevUrlStream(html, DEV_ORIGIN) : html
|
|
2017
2194
|
}
|
|
2018
|
-
`;
|
|
2195
|
+
${SSR_SERVICE}`;
|
|
2196
|
+
}
|
|
2197
|
+
/**
|
|
2198
|
+
* What Nitro calls this environment through.
|
|
2199
|
+
*
|
|
2200
|
+
* Nitro addresses each Vite environment as a service and calls fetch on it —
|
|
2201
|
+
* `mod = _mod.default || _mod; mod.fetch(req)` — so an entry that exports only
|
|
2202
|
+
* named functions is a 500 on every request. The named ones stay: the rsc
|
|
2203
|
+
* entry still reaches them through loadModule. This only adds the door Nitro
|
|
2204
|
+
* knocks on, and the request goes straight back to the rsc entry, which is the
|
|
2205
|
+
* one that owns rendering.
|
|
2206
|
+
*/
|
|
2207
|
+
const SSR_SERVICE = `
|
|
2208
|
+
export default {
|
|
2209
|
+
fetch: async (request: Request): Promise<Response> => {
|
|
2210
|
+
const rsc = await import.meta.viteRsc.loadModule<{
|
|
2211
|
+
default: (request: Request) => Promise<Response>
|
|
2212
|
+
}>('rsc', 'index')
|
|
2213
|
+
|
|
2214
|
+
return rsc.default(request)
|
|
2215
|
+
},
|
|
2019
2216
|
}
|
|
2217
|
+
`;
|
|
2020
2218
|
function generateEntryBrowser() {
|
|
2021
2219
|
const clientBootstrap = join(packageDir, 'js/createViteRscApp');
|
|
2022
2220
|
// Only for an exported build, and only what the client needs to work out how
|
|
@@ -2029,7 +2227,7 @@ function generateEntryBrowser() {
|
|
|
2029
2227
|
? routeManifest().routes.map((route) => ({ segments: route.segments, layouts: route.layouts }))
|
|
2030
2228
|
: null;
|
|
2031
2229
|
const refreshModule = join(packageDir, 'js/navigate');
|
|
2032
|
-
return `// GENERATED by
|
|
2230
|
+
return `// GENERATED by rscKit() — do not edit.
|
|
2033
2231
|
import { createViteRscApp } from ${JSON.stringify(clientBootstrap)}
|
|
2034
2232
|
import { refresh } from ${JSON.stringify(refreshModule)}
|
|
2035
2233
|
|
|
@@ -2176,20 +2374,20 @@ function validateLoadingBoundaries() {
|
|
|
2176
2374
|
// ── Plugin ───────────────────────────────────────────────────────────────────
|
|
2177
2375
|
/** Names of plugins that transform JSX and must run after rsc() has split it. */
|
|
2178
2376
|
const JSX_PLUGIN_PATTERN = /react|babel|oxc/i;
|
|
2179
|
-
export function
|
|
2377
|
+
export function rscKit(options = {}) {
|
|
2180
2378
|
resolvePaths(options);
|
|
2181
2379
|
const routesPlugin = {
|
|
2182
|
-
name: 'rsc-
|
|
2380
|
+
name: 'rsc-kit',
|
|
2183
2381
|
config(_config, env) {
|
|
2184
2382
|
if (!existsSync(appDir)) {
|
|
2185
|
-
throw new Error(`[rsc-
|
|
2383
|
+
throw new Error(`[rsc-kit] No app directory at ${appDir} — nothing to build.`);
|
|
2186
2384
|
}
|
|
2187
2385
|
components.clear();
|
|
2188
2386
|
discover(appDir);
|
|
2189
2387
|
log(`Discovered ${components.size} route components:`, [...components.keys()].join(', '));
|
|
2190
2388
|
const loadingErrors = validateLoadingBoundaries();
|
|
2191
2389
|
if (loadingErrors.length) {
|
|
2192
|
-
throw new Error('[rsc-
|
|
2390
|
+
throw new Error('[rsc-kit] A page that blocks before it can paint needs a loading.tsx boundary.\n\n' +
|
|
2193
2391
|
loadingErrors.join('\n') +
|
|
2194
2392
|
'\n\nAdd loading.tsx in the page directory (or a parent), or move the slow work\n' +
|
|
2195
2393
|
'into a child component wrapped in its own <Suspense> so the page can paint.');
|
|
@@ -2206,13 +2404,20 @@ export function rscRoutes(options = {}) {
|
|
|
2206
2404
|
// cannot end up pointing at different backends. Development only: a
|
|
2207
2405
|
// build's server.ts decides this for itself.
|
|
2208
2406
|
const backendEnv = loadEnv(env.mode, projectRoot, '');
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2407
|
+
// Only a backend this server could actually call. The shared secret is
|
|
2408
|
+
// what makes one a backend rather than a url that happens to be in the
|
|
2409
|
+
// environment — APP_URL is not a Laravel-only name, and a JavaScript
|
|
2410
|
+
// host that sets it for its own reasons must not find its 404s being
|
|
2411
|
+
// posted to it. Host calls gate on exactly the same pair, so the two
|
|
2412
|
+
// cannot end up disagreeing about whether a backend is there.
|
|
2413
|
+
//
|
|
2414
|
+
// Naming devFallback explicitly opts in regardless: someone who wrote
|
|
2415
|
+
// the address down means it.
|
|
2416
|
+
const backendSecret = hostCallOptions?.secret ?? backendEnv.RSC_HOST_CALL_SECRET;
|
|
2417
|
+
const detected = backendSecret
|
|
2418
|
+
? (hostCallOptions?.endpoint ?? backendEnv.RSC_BACKEND ?? backendEnv.APP_URL ?? '')
|
|
2419
|
+
: '';
|
|
2420
|
+
const fallbackOrigin = options.devFallback === false ? '' : (options.devFallback ?? detected);
|
|
2216
2421
|
writeFileSync(join(genDir, 'entry.rsc.tsx'), generateEntryRsc(fallbackOrigin));
|
|
2217
2422
|
writeFileSync(join(genDir, 'entry.ssr.tsx'), generateEntrySsr());
|
|
2218
2423
|
writeFileSync(join(genDir, 'entry.browser.tsx'), generateEntryBrowser());
|
|
@@ -2273,7 +2478,7 @@ export function rscRoutes(options = {}) {
|
|
|
2273
2478
|
// "The server is configured with a public base URL", which reads as a
|
|
2274
2479
|
// routing bug rather than as this line. In dev the pages are the root;
|
|
2275
2480
|
// the assets come from the same origin either way.
|
|
2276
|
-
base:
|
|
2481
|
+
base: '/',
|
|
2277
2482
|
root: outDir,
|
|
2278
2483
|
// Force single instances of React/RSC runtime — critical when the
|
|
2279
2484
|
// package is symlinked (local dev / monorepo), else "use client"
|
|
@@ -2350,7 +2555,7 @@ export function rscRoutes(options = {}) {
|
|
|
2350
2555
|
// Reported rather than thrown: the dev server is still useful for
|
|
2351
2556
|
// every page that needs no data, and a failure here would otherwise
|
|
2352
2557
|
// look like the server refusing to start.
|
|
2353
|
-
server.config.logger.warn(`[rsc-
|
|
2558
|
+
server.config.logger.warn(`[rsc-kit] could not wire host calls to ${endpoint}: ` +
|
|
2354
2559
|
(error instanceof Error ? error.message : String(error)));
|
|
2355
2560
|
}
|
|
2356
2561
|
});
|
|
@@ -2403,7 +2608,7 @@ export function rscRoutes(options = {}) {
|
|
|
2403
2608
|
const restart = (file) => {
|
|
2404
2609
|
if (!affectsRouting(file))
|
|
2405
2610
|
return;
|
|
2406
|
-
server.config.logger.info(`[rsc-
|
|
2611
|
+
server.config.logger.info(`[rsc-kit] route tree changed (${file.slice(sourceDir.length + 1)}) — restarting`);
|
|
2407
2612
|
void server.restart();
|
|
2408
2613
|
};
|
|
2409
2614
|
// Watched explicitly: the Vite root is the *out* directory, so the app's
|
|
@@ -2428,7 +2633,7 @@ export function rscRoutes(options = {}) {
|
|
|
2428
2633
|
* Skipped in watch mode. A rebuild on every keystroke that also re-renders
|
|
2429
2634
|
* every route is not a feedback loop anyone wants.
|
|
2430
2635
|
*/
|
|
2431
|
-
async buildApp() {
|
|
2636
|
+
async buildApp(builder) {
|
|
2432
2637
|
if (isWatch)
|
|
2433
2638
|
return;
|
|
2434
2639
|
// Say what the build did, even when it stored nothing.
|
|
@@ -2442,7 +2647,25 @@ export function rscRoutes(options = {}) {
|
|
|
2442
2647
|
reportAllDynamic();
|
|
2443
2648
|
return;
|
|
2444
2649
|
}
|
|
2445
|
-
|
|
2650
|
+
// Asked of the build rather than assumed from `outDir`. The two layouts
|
|
2651
|
+
// differ — <outDir>/dist/rsc on its own, node_modules/.nitro/… under
|
|
2652
|
+
// Nitro — and hard-coding the first is what silently skipped the second.
|
|
2653
|
+
// Both environments emit `index.js`, so this is one path, not a branch.
|
|
2654
|
+
const rscOut = builder?.environments?.rsc?.config?.build?.outDir ?? join(outDir, 'dist/rsc');
|
|
2655
|
+
const bundle = resolveRscBundle(rscOut);
|
|
2656
|
+
// Where the frozen pages go, which is not the same question.
|
|
2657
|
+
//
|
|
2658
|
+
// On its own the plugin owns the output and server.ts reads <outDir>/static
|
|
2659
|
+
// from the repository. Under Nitro the deployment is `.output/` and
|
|
2660
|
+
// nothing outside it is copied to the server — so pages written anywhere
|
|
2661
|
+
// else exist on the build machine and nowhere after that. They go beside
|
|
2662
|
+
// the server bundle instead, and buildApp runs before Nitro assembles, so
|
|
2663
|
+
// they are in place by the time it does.
|
|
2664
|
+
const clientOut = builder?.environments?.client?.config?.build?.outDir;
|
|
2665
|
+
const staticDir = clientOut
|
|
2666
|
+
? join(dirname(clientOut), 'server', NITRO_STATIC_DIR)
|
|
2667
|
+
: join(outDir, NITRO_STATIC_DIR);
|
|
2668
|
+
await prerenderAfterBundles(bundle, staticDir, clientOut ?? publicAssetsDir);
|
|
2446
2669
|
},
|
|
2447
2670
|
configResolved(config) {
|
|
2448
2671
|
isWatch = config.build?.watch != null;
|
|
@@ -2453,18 +2676,21 @@ export function rscRoutes(options = {}) {
|
|
|
2453
2676
|
const rscAt = names.findIndex((n) => n === 'rsc' || n.startsWith('rsc:'));
|
|
2454
2677
|
const jsxAt = names.findIndex((n) => JSX_PLUGIN_PATTERN.test(n));
|
|
2455
2678
|
if (rscAt !== -1 && jsxAt !== -1 && jsxAt < rscAt) {
|
|
2456
|
-
throw new Error(`[rsc-
|
|
2679
|
+
throw new Error(`[rsc-kit] Plugin "${names[jsxAt]}" is resolved ahead of rsc(), so it would ` +
|
|
2457
2680
|
'transform JSX before the client/server split.\n' +
|
|
2458
|
-
'Put
|
|
2681
|
+
'Put rscKit() first in your plugins array. If it already is, that plugin ' +
|
|
2459
2682
|
"sets enforce: 'pre' and needs to be moved after rsc() explicitly.");
|
|
2460
2683
|
}
|
|
2461
2684
|
},
|
|
2462
2685
|
};
|
|
2463
2686
|
// rsc() ships as several plugins, and it has to lead. A promise is a legal
|
|
2464
2687
|
// member of a Vite plugins array and is flattened in place, so this keeps
|
|
2465
|
-
//
|
|
2688
|
+
// rscKit() one entry in the app's config while still resolving the
|
|
2466
2689
|
// plugin at call time — see appPluginRsc for why that matters.
|
|
2467
|
-
|
|
2690
|
+
// With Nitro, the server handler is Nitro's — it takes the rsc entry's
|
|
2691
|
+
// default export and builds the server around it. Leaving plugin-rsc's own
|
|
2692
|
+
// handler in place means two things claiming the same role.
|
|
2693
|
+
return [appPluginRsc({ serverHandler: false }), routesPlugin];
|
|
2468
2694
|
}
|
|
2469
2695
|
/**
|
|
2470
2696
|
* @vitejs/plugin-rsc, resolved from the app rather than from here.
|
|
@@ -2483,18 +2709,18 @@ export function rscRoutes(options = {}) {
|
|
|
2483
2709
|
* Resolving from the project root gets the app's copy, whose own `vite` import
|
|
2484
2710
|
* then resolves to the app's Vite as well — one pair, and the check passes.
|
|
2485
2711
|
*/
|
|
2486
|
-
async function appPluginRsc() {
|
|
2712
|
+
async function appPluginRsc(options = {}) {
|
|
2487
2713
|
try {
|
|
2488
2714
|
// Resolved against a file *in* the root, since a directory specifier
|
|
2489
2715
|
// resolves relative to its parent.
|
|
2490
2716
|
const fromApp = createRequire(join(projectRoot, 'package.json'));
|
|
2491
2717
|
const entry = fromApp.resolve('@vitejs/plugin-rsc');
|
|
2492
2718
|
const mod = (await import(pathToFileURL(entry).href));
|
|
2493
|
-
return (mod.default ?? rsc)();
|
|
2719
|
+
return (mod.default ?? rsc)(options);
|
|
2494
2720
|
}
|
|
2495
2721
|
catch {
|
|
2496
2722
|
// The app does not have its own; one copy, and the bundled import is it.
|
|
2497
|
-
return rsc();
|
|
2723
|
+
return rsc(options);
|
|
2498
2724
|
}
|
|
2499
2725
|
}
|
|
2500
2726
|
//# sourceMappingURL=vite.js.map
|