@rsc-kit/core 0.21.4 → 0.22.1
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/vite.d.ts +42 -0
- package/dist/vite.js +306 -161
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
package/dist/vite.d.ts
CHANGED
|
@@ -388,6 +388,16 @@ export declare function toAlias(name: string): string;
|
|
|
388
388
|
* was controlling so they load uncontrolled. Nothing else runs in development.
|
|
389
389
|
*/
|
|
390
390
|
export declare const DEV_WORKER = "self.addEventListener('install', () => self.skipWaiting())\nself.addEventListener('activate', (event) => {\n event.waitUntil((async () => {\n const keys = await caches.keys()\n await Promise.all(keys.filter((k) => k.startsWith('rsc-kit-')).map((k) => caches.delete(k)))\n await self.clients.claim()\n const pages = await self.clients.matchAll({ type: 'window' })\n await self.registration.unregister()\n for (const page of pages) page.navigate(page.url).catch(() => {})\n })())\n})\n";
|
|
391
|
+
/**
|
|
392
|
+
* A read of import.meta.env.PUBLIC_*, refused by name.
|
|
393
|
+
*
|
|
394
|
+
* PUBLIC_ was a client prefix beside VITE_ until 0.22. Dropped silently, a
|
|
395
|
+
* read of one would compile to undefined - in the browser, for a pixel id or
|
|
396
|
+
* an api url, with nothing said anywhere - so the build names each file that
|
|
397
|
+
* still reads one and the variable to rename, and stops. Server code reading
|
|
398
|
+
* process.env.PUBLIC_* is untouched: that is an ordinary server variable.
|
|
399
|
+
*/
|
|
400
|
+
export declare function publicPrefixReads(code: string): string[];
|
|
391
401
|
export declare function serverRendererInRsc(): Plugin;
|
|
392
402
|
/**
|
|
393
403
|
* The project's typecheck, as part of the build.
|
|
@@ -419,4 +429,36 @@ export type TypecheckOutcome = {
|
|
|
419
429
|
* is the message, because that is what the developer would have read.
|
|
420
430
|
*/
|
|
421
431
|
export declare function typecheckProject(root: string): TypecheckOutcome;
|
|
432
|
+
/**
|
|
433
|
+
* A Nitro runtime plugin, so a rollout stops a server cleanly.
|
|
434
|
+
*
|
|
435
|
+
* On SIGTERM - a rollout, docker stop - srvx stops taking connections and
|
|
436
|
+
* gives in-flight requests SERVER_SHUTDOWN_TIMEOUT seconds (5) to finish. It
|
|
437
|
+
* does not exit: the process ends only once nothing holds it open, and an app
|
|
438
|
+
* with a database pool or a queue client always has something, so it waited
|
|
439
|
+
* for its SIGKILL and lost the log lines written meanwhile. This waits out the
|
|
440
|
+
* same window, calls instrumentation.ts's shutdown() if the app has one -
|
|
441
|
+
* bounded, so a close that hangs cannot hold the pod - and exits.
|
|
442
|
+
*
|
|
443
|
+
* Unref'd throughout: an app with nothing open exits the moment srvx has
|
|
444
|
+
* drained, as it did before. Built servers only - not a Worker, which has no
|
|
445
|
+
* process to signal, and not the dev server or the build, where Ctrl+C should
|
|
446
|
+
* stop at once.
|
|
447
|
+
*/
|
|
448
|
+
export declare const SHUTDOWN_PLUGIN = "export default function rscKitShutdown() {\n if (typeof navigator !== 'undefined' && navigator.userAgent === 'Cloudflare-Workers') return\n if (typeof process === 'undefined' || typeof process.once !== 'function') return\n\n const unref = (timer) => (timer && typeof timer.unref === 'function' ? timer.unref() : timer)\n let stopping = false\n\n const stop = () => {\n if (stopping) return\n stopping = true\n\n const drain = (Number(process.env.SERVER_SHUTDOWN_TIMEOUT) || 5) * 1000\n const limit = (Number(process.env.RSC_SHUTDOWN_TIMEOUT) || 10) * 1000\n\n unref(setTimeout(() => {\n const hook = globalThis.__rscKitShutdown\n\n Promise.race([\n Promise.resolve().then(() => (typeof hook === 'function' ? hook() : undefined)),\n new Promise((resolve) => unref(setTimeout(resolve, limit))),\n ])\n .catch((error) => console.error('[rsc-kit] instrumentation.ts shutdown() failed:', error))\n .finally(() => process.exit(0))\n }, drain + 250))\n }\n\n process.once('SIGTERM', stop)\n process.once('SIGINT', stop)\n}\n";
|
|
449
|
+
/**
|
|
450
|
+
* The PUBLIC_ variables an app still sets, and where each one is set.
|
|
451
|
+
*
|
|
452
|
+
* PUBLIC_ was a client prefix beside VITE_ until 0.22. An app that sets one
|
|
453
|
+
* expects it in the browser, and most read it through a validated env object
|
|
454
|
+
* rather than import.meta.env, so no source scan sees the read: in the
|
|
455
|
+
* browser the value was simply gone, and a required one threw from the
|
|
456
|
+
* validator on the first page that imported it. The variables themselves are
|
|
457
|
+
* the signal. Same files, same order and same directories as nodeEnvSetIn;
|
|
458
|
+
* a name only in the process environment is reported as such.
|
|
459
|
+
*/
|
|
460
|
+
export declare function publicVariablesSet(mode: string, envDir?: string | false, environment?: Record<string, string | undefined>): {
|
|
461
|
+
name: string;
|
|
462
|
+
where: string;
|
|
463
|
+
}[];
|
|
422
464
|
export declare function rscKit(options?: RscKitOptions): PluginOption[];
|
package/dist/vite.js
CHANGED
|
@@ -2475,13 +2475,14 @@ function instrumentationFile() {
|
|
|
2475
2475
|
const file = join(sourceDir, `instrumentation.${ext}`);
|
|
2476
2476
|
if (!existsSync(file))
|
|
2477
2477
|
continue;
|
|
2478
|
-
//
|
|
2479
|
-
//
|
|
2478
|
+
// Every part is optional: a file of imports is a bootstrap, a file with
|
|
2479
|
+
// register() or shutdown() has hooks. The entry must only name one when it
|
|
2480
2480
|
// exists - a namespace import's missing member is a bundler warning on
|
|
2481
2481
|
// every build (IMPORT_IS_UNDEFINED), which is the app being told off for
|
|
2482
2482
|
// a file written exactly as documented.
|
|
2483
|
-
const
|
|
2484
|
-
|
|
2483
|
+
const source = readFileSync(file, "utf-8");
|
|
2484
|
+
const exports = (name) => new RegExp(`export\\s+(?:async\\s+)?(?:function\\s+${name}\\b|const\\s+${name}\\b|let\\s+${name}\\b|\\{[^}]*\\b${name}\\b[^}]*\\})`).test(source);
|
|
2485
|
+
return { file, hasRegister: exports("register"), hasShutdown: exports("shutdown") };
|
|
2485
2486
|
}
|
|
2486
2487
|
return null;
|
|
2487
2488
|
}
|
|
@@ -2551,11 +2552,11 @@ ${
|
|
|
2551
2552
|
// First, before any page: an import's side effects run in import order,
|
|
2552
2553
|
// and a package configured here has to be configured before a page module
|
|
2553
2554
|
// that reads it at evaluation time.
|
|
2554
|
-
instrumentation?.hasRegister
|
|
2555
|
+
instrumentation?.hasRegister || instrumentation?.hasShutdown
|
|
2555
2556
|
? `import * as __instrumentation from ${JSON.stringify(instrumentation.file)}`
|
|
2556
2557
|
: instrumentation
|
|
2557
|
-
? `import ${JSON.stringify(instrumentation.file)}\nconst __instrumentation: { register?: () => unknown } = {}`
|
|
2558
|
-
: "const __instrumentation: { register?: () => unknown } = {}"}
|
|
2558
|
+
? `import ${JSON.stringify(instrumentation.file)}\nconst __instrumentation: { register?: () => unknown; shutdown?: () => unknown } = {}`
|
|
2559
|
+
: "const __instrumentation: { register?: () => unknown; shutdown?: () => unknown } = {}"}
|
|
2559
2560
|
import { SegmentBoundary } from ${JSON.stringify(join(packageDir, "js/SegmentBoundary"))}
|
|
2560
2561
|
import { LoadingBoundary } from ${JSON.stringify(join(packageDir, "js/LoadingBoundary"))}
|
|
2561
2562
|
import { DocumentTitle } from ${JSON.stringify(join(packageDir, "js/DocumentTitle"))}
|
|
@@ -2998,6 +2999,15 @@ function instrumented(): Promise<void> {
|
|
|
2998
2999
|
// start, start now.
|
|
2999
3000
|
const isolateRuntime = typeof navigator !== 'undefined' && navigator.userAgent === 'Cloudflare-Workers'
|
|
3000
3001
|
|
|
3002
|
+
// instrumentation.ts's shutdown(), for the server's shutdown plugin
|
|
3003
|
+
// (SHUTDOWN_PLUGIN in vite.ts), which calls it once in-flight requests have
|
|
3004
|
+
// drained and then exits. Handed over on globalThis rather than through a
|
|
3005
|
+
// request: every request reaches this module, and one carrying a header must
|
|
3006
|
+
// not be able to close the app's database pool.
|
|
3007
|
+
if (!isolateRuntime) {
|
|
3008
|
+
;(globalThis as { __rscKitShutdown?: () => unknown }).__rscKitShutdown = () => __instrumentation.shutdown?.()
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3001
3011
|
if (__instrumentation.register && !isolateRuntime) {
|
|
3002
3012
|
instrumented().catch((error) => {
|
|
3003
3013
|
console.error('[rsc-kit] instrumentation.ts register() failed:', error)
|
|
@@ -5510,6 +5520,35 @@ self.addEventListener('activate', (event) => {
|
|
|
5510
5520
|
})())
|
|
5511
5521
|
})
|
|
5512
5522
|
`;
|
|
5523
|
+
/**
|
|
5524
|
+
* A read of import.meta.env.PUBLIC_*, refused by name.
|
|
5525
|
+
*
|
|
5526
|
+
* PUBLIC_ was a client prefix beside VITE_ until 0.22. Dropped silently, a
|
|
5527
|
+
* read of one would compile to undefined - in the browser, for a pixel id or
|
|
5528
|
+
* an api url, with nothing said anywhere - so the build names each file that
|
|
5529
|
+
* still reads one and the variable to rename, and stops. Server code reading
|
|
5530
|
+
* process.env.PUBLIC_* is untouched: that is an ordinary server variable.
|
|
5531
|
+
*/
|
|
5532
|
+
export function publicPrefixReads(code) {
|
|
5533
|
+
return [...new Set([...code.matchAll(/\bimport\.meta\.env\.(PUBLIC_[A-Za-z0-9_]+)/g)].map((m) => m[1]))];
|
|
5534
|
+
}
|
|
5535
|
+
function publicPrefixRefused() {
|
|
5536
|
+
return {
|
|
5537
|
+
name: "rsc-kit:public-prefix",
|
|
5538
|
+
enforce: "pre",
|
|
5539
|
+
transform(code, id) {
|
|
5540
|
+
if (id.includes("/node_modules/") || !code.includes("import.meta.env.PUBLIC_"))
|
|
5541
|
+
return;
|
|
5542
|
+
const names = publicPrefixReads(code);
|
|
5543
|
+
if (names.length === 0)
|
|
5544
|
+
return;
|
|
5545
|
+
this.error(`${id.split("?")[0]} reads ${names.map((n) => `import.meta.env.${n}`).join(", ")}. ` +
|
|
5546
|
+
"Only VITE_ variables reach the browser now; rename " +
|
|
5547
|
+
names.map((n) => `${n} to VITE_${n.slice("PUBLIC_".length)}`).join(", ") +
|
|
5548
|
+
" in the code and in your .env files.");
|
|
5549
|
+
},
|
|
5550
|
+
};
|
|
5551
|
+
}
|
|
5513
5552
|
/**
|
|
5514
5553
|
* Named imports from a barrel package, unrolled on the server environments.
|
|
5515
5554
|
*
|
|
@@ -5853,6 +5892,52 @@ export default function rscKitStartup() {
|
|
|
5853
5892
|
})
|
|
5854
5893
|
}
|
|
5855
5894
|
`;
|
|
5895
|
+
/**
|
|
5896
|
+
* A Nitro runtime plugin, so a rollout stops a server cleanly.
|
|
5897
|
+
*
|
|
5898
|
+
* On SIGTERM - a rollout, docker stop - srvx stops taking connections and
|
|
5899
|
+
* gives in-flight requests SERVER_SHUTDOWN_TIMEOUT seconds (5) to finish. It
|
|
5900
|
+
* does not exit: the process ends only once nothing holds it open, and an app
|
|
5901
|
+
* with a database pool or a queue client always has something, so it waited
|
|
5902
|
+
* for its SIGKILL and lost the log lines written meanwhile. This waits out the
|
|
5903
|
+
* same window, calls instrumentation.ts's shutdown() if the app has one -
|
|
5904
|
+
* bounded, so a close that hangs cannot hold the pod - and exits.
|
|
5905
|
+
*
|
|
5906
|
+
* Unref'd throughout: an app with nothing open exits the moment srvx has
|
|
5907
|
+
* drained, as it did before. Built servers only - not a Worker, which has no
|
|
5908
|
+
* process to signal, and not the dev server or the build, where Ctrl+C should
|
|
5909
|
+
* stop at once.
|
|
5910
|
+
*/
|
|
5911
|
+
export const SHUTDOWN_PLUGIN = `export default function rscKitShutdown() {
|
|
5912
|
+
if (typeof navigator !== 'undefined' && navigator.userAgent === 'Cloudflare-Workers') return
|
|
5913
|
+
if (typeof process === 'undefined' || typeof process.once !== 'function') return
|
|
5914
|
+
|
|
5915
|
+
const unref = (timer) => (timer && typeof timer.unref === 'function' ? timer.unref() : timer)
|
|
5916
|
+
let stopping = false
|
|
5917
|
+
|
|
5918
|
+
const stop = () => {
|
|
5919
|
+
if (stopping) return
|
|
5920
|
+
stopping = true
|
|
5921
|
+
|
|
5922
|
+
const drain = (Number(process.env.SERVER_SHUTDOWN_TIMEOUT) || 5) * 1000
|
|
5923
|
+
const limit = (Number(process.env.RSC_SHUTDOWN_TIMEOUT) || 10) * 1000
|
|
5924
|
+
|
|
5925
|
+
unref(setTimeout(() => {
|
|
5926
|
+
const hook = globalThis.__rscKitShutdown
|
|
5927
|
+
|
|
5928
|
+
Promise.race([
|
|
5929
|
+
Promise.resolve().then(() => (typeof hook === 'function' ? hook() : undefined)),
|
|
5930
|
+
new Promise((resolve) => unref(setTimeout(resolve, limit))),
|
|
5931
|
+
])
|
|
5932
|
+
.catch((error) => console.error('[rsc-kit] instrumentation.ts shutdown() failed:', error))
|
|
5933
|
+
.finally(() => process.exit(0))
|
|
5934
|
+
}, drain + 250))
|
|
5935
|
+
}
|
|
5936
|
+
|
|
5937
|
+
process.once('SIGTERM', stop)
|
|
5938
|
+
process.once('SIGINT', stop)
|
|
5939
|
+
}
|
|
5940
|
+
`;
|
|
5856
5941
|
/**
|
|
5857
5942
|
* Where NODE_ENV=development came from, and what to do about it.
|
|
5858
5943
|
*
|
|
@@ -5881,6 +5966,45 @@ function nodeEnvSetIn(mode, envDir) {
|
|
|
5881
5966
|
}
|
|
5882
5967
|
return culprit;
|
|
5883
5968
|
}
|
|
5969
|
+
/**
|
|
5970
|
+
* The PUBLIC_ variables an app still sets, and where each one is set.
|
|
5971
|
+
*
|
|
5972
|
+
* PUBLIC_ was a client prefix beside VITE_ until 0.22. An app that sets one
|
|
5973
|
+
* expects it in the browser, and most read it through a validated env object
|
|
5974
|
+
* rather than import.meta.env, so no source scan sees the read: in the
|
|
5975
|
+
* browser the value was simply gone, and a required one threw from the
|
|
5976
|
+
* validator on the first page that imported it. The variables themselves are
|
|
5977
|
+
* the signal. Same files, same order and same directories as nodeEnvSetIn;
|
|
5978
|
+
* a name only in the process environment is reported as such.
|
|
5979
|
+
*/
|
|
5980
|
+
export function publicVariablesSet(mode, envDir, environment = process.env) {
|
|
5981
|
+
const files = [".env", ".env.local", `.env.${mode}`, `.env.${mode}.local`];
|
|
5982
|
+
const found = new Map();
|
|
5983
|
+
const dirs = [...new Set([projectRoot, envDir || projectRoot])].filter((dir) => !!dir);
|
|
5984
|
+
for (const dir of dirs)
|
|
5985
|
+
for (const file of files) {
|
|
5986
|
+
const path = join(dir, file);
|
|
5987
|
+
if (!existsSync(path))
|
|
5988
|
+
continue;
|
|
5989
|
+
readFileSync(path, "utf-8").split("\n").forEach((line, at) => {
|
|
5990
|
+
const name = /^\s*(?:export\s+)?(PUBLIC_[A-Za-z0-9_]+)\s*=/.exec(line)?.[1];
|
|
5991
|
+
if (name)
|
|
5992
|
+
found.set(name, `${path}:${at + 1}`);
|
|
5993
|
+
});
|
|
5994
|
+
}
|
|
5995
|
+
for (const name of Object.keys(environment)) {
|
|
5996
|
+
if (name.startsWith("PUBLIC_") && !found.has(name))
|
|
5997
|
+
found.set(name, "the environment");
|
|
5998
|
+
}
|
|
5999
|
+
return [...found].map(([name, where]) => ({ name, where }));
|
|
6000
|
+
}
|
|
6001
|
+
function refusePublicVariables(set) {
|
|
6002
|
+
return ("[rsc-kit] Only VITE_ variables reach the browser; PUBLIC_ stopped being a client prefix in 0.22. " +
|
|
6003
|
+
"These are still set, and would be undefined in the browser:\n\n" +
|
|
6004
|
+
set.map(({ name, where }) => ` ${name} (${where}) -> VITE_${name.slice("PUBLIC_".length)}`).join("\n") +
|
|
6005
|
+
"\n\nRename each where it is set and where it is read - an env schema's clientPrefix and client " +
|
|
6006
|
+
"keys included.");
|
|
6007
|
+
}
|
|
5884
6008
|
function refuseDevelopmentBuild(config) {
|
|
5885
6009
|
const culprit = nodeEnvSetIn(config.mode, config.envDir);
|
|
5886
6010
|
return ("[rsc-kit] vite build is running as a development build, and the output cannot work: " +
|
|
@@ -6020,6 +6144,12 @@ export function rscKit(options = {}) {
|
|
|
6020
6144
|
"\nexport default () => {};\n";
|
|
6021
6145
|
nitro.options.plugins = ["#rsc-kit/polyfills", ...(nitro.options.plugins ?? [])];
|
|
6022
6146
|
}
|
|
6147
|
+
// Every built server, instrumentation.ts or not: the hang it prevents
|
|
6148
|
+
// is any open connection, wherever the app opened it.
|
|
6149
|
+
if (!nitro.options.dev) {
|
|
6150
|
+
nitro.options.virtual["#rsc-kit/shutdown"] = SHUTDOWN_PLUGIN;
|
|
6151
|
+
nitro.options.plugins = [...(nitro.options.plugins ?? []), "#rsc-kit/shutdown"];
|
|
6152
|
+
}
|
|
6023
6153
|
if (!instrumentationFile())
|
|
6024
6154
|
return;
|
|
6025
6155
|
nitro.options.virtual["#rsc-kit/startup"] = STARTUP_PLUGIN;
|
|
@@ -6225,11 +6355,11 @@ export function rscKit(options = {}) {
|
|
|
6225
6355
|
// saying so at the end of each docker build is not a finding.
|
|
6226
6356
|
rollupOptions: { checks: { pluginTimings: false } },
|
|
6227
6357
|
},
|
|
6228
|
-
// What reaches the browser
|
|
6229
|
-
//
|
|
6230
|
-
//
|
|
6231
|
-
//
|
|
6232
|
-
envPrefix: ["VITE_"
|
|
6358
|
+
// What reaches the browser: Vite's own VITE_ prefix, and only that.
|
|
6359
|
+
// PUBLIC_ was accepted beside it until 0.22; a read of one is now
|
|
6360
|
+
// refused by name (publicPrefixRefused) rather than left undefined.
|
|
6361
|
+
// A config that set its own prefixes keeps them; Vite merges.
|
|
6362
|
+
envPrefix: ["VITE_"],
|
|
6233
6363
|
environments: {
|
|
6234
6364
|
// Server bundles — stay under the (non-public) out dir. `bun` and
|
|
6235
6365
|
// `bun:*` are the runtime's own modules, like `node:*`: nothing to
|
|
@@ -6327,159 +6457,167 @@ export function rscKit(options = {}) {
|
|
|
6327
6457
|
* and restarting on every keystroke would throw away the module graph for
|
|
6328
6458
|
* nothing.
|
|
6329
6459
|
*/
|
|
6330
|
-
configureServer
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
//
|
|
6340
|
-
//
|
|
6341
|
-
//
|
|
6342
|
-
//
|
|
6343
|
-
//
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
res.setHeader("Content-Type", "application/manifest+json");
|
|
6362
|
-
res.end(webManifest(webManifestOptions));
|
|
6363
|
-
return;
|
|
6364
|
-
}
|
|
6365
|
-
const asset = allAppAssets(foundAssets).find((a) => a.href === url);
|
|
6366
|
-
const file = asset ? join(sourceDir, "app", asset.file) : null;
|
|
6367
|
-
if (!asset || !file || !existsSync(file))
|
|
6368
|
-
return next();
|
|
6369
|
-
res.setHeader("Content-Type", typeOf(asset.file));
|
|
6370
|
-
createReadStream(file).pipe(res);
|
|
6371
|
-
});
|
|
6372
|
-
// rpc() has to reach the backend while the dev server is serving.
|
|
6373
|
-
//
|
|
6374
|
-
// A built deployment installs this itself: the server running
|
|
6375
|
-
// createRscHandler passes `hostCalls`. Nothing does that here, and with
|
|
6376
|
-
// no host installed every rpc() is refused — so a page whose data comes
|
|
6377
|
-
// from the backend renders its shell and then blanks, which reads as a
|
|
6378
|
-
// hydration bug rather than a missing wire.
|
|
6379
|
-
server.httpServer?.once("listening", async () => {
|
|
6380
|
-
const env = server.environments?.rsc;
|
|
6381
|
-
if (!env?.runner)
|
|
6382
|
-
return;
|
|
6383
|
-
// The app's own .env, unprefixed. A Laravel app already has both of
|
|
6384
|
-
// these, which is what makes this need no configuring: APP_URL is the
|
|
6385
|
-
// backend and RSC_HOST_CALL_SECRET is the secret it checks.
|
|
6386
|
-
const fromEnv = loadEnv(server.config.mode, projectRoot, "");
|
|
6387
|
-
const secret = hostCallOptions?.secret ?? fromEnv.RSC_HOST_CALL_SECRET;
|
|
6388
|
-
const origin = hostCallOptions?.endpoint ?? fromEnv.RSC_BACKEND ?? fromEnv.APP_URL;
|
|
6389
|
-
if (!secret || !origin)
|
|
6390
|
-
return;
|
|
6391
|
-
const path = hostCallOptions?.path ??
|
|
6392
|
-
fromEnv.RSC_HOST_CALL_PATH ??
|
|
6393
|
-
"/__rsc/host-call";
|
|
6394
|
-
const endpoint = origin.replace(/\/$/, "") + path;
|
|
6395
|
-
try {
|
|
6396
|
-
const entry = await env.runner.import(join(genDir, "entry.rsc.tsx"));
|
|
6397
|
-
const install = entry.installHostFn;
|
|
6398
|
-
install?.(httpHostCalls({ endpoint, secret }));
|
|
6399
|
-
}
|
|
6400
|
-
catch (error) {
|
|
6401
|
-
// Reported rather than thrown: the dev server is still useful for
|
|
6402
|
-
// every page that needs no data, and a failure here would otherwise
|
|
6403
|
-
// look like the server refusing to start.
|
|
6404
|
-
server.config.logger.warn(`[rsc-kit] could not wire host calls to ${endpoint}: ` +
|
|
6405
|
-
(error instanceof Error ? error.message : String(error)));
|
|
6406
|
-
}
|
|
6407
|
-
});
|
|
6408
|
-
// Written once the server is listening, because only then is the port
|
|
6409
|
-
// known. Removed on shutdown so a backend can tell a dev server that is
|
|
6410
|
-
// gone from one that is merely slow to answer.
|
|
6411
|
-
if (hotFile) {
|
|
6412
|
-
const remove = () => {
|
|
6413
|
-
try {
|
|
6414
|
-
if (existsSync(hotFile))
|
|
6415
|
-
rmSync(hotFile);
|
|
6460
|
+
// Ahead of every other plugin's configureServer, so this middleware is
|
|
6461
|
+
// first in the stack. Nitro's dev handler is registered by its own plugin,
|
|
6462
|
+
// which an app lists before this one, and it answers every request that
|
|
6463
|
+
// does not look like an asset itself - /manifest.webmanifest and
|
|
6464
|
+
// /robots.txt went to the app's router and came back 404, or 500 from a
|
|
6465
|
+
// page that could not render for that url, and this never saw them.
|
|
6466
|
+
// Swapping the plugins in the app's config is not a fix: Nitro then loads
|
|
6467
|
+
// .env too late for the app's first request.
|
|
6468
|
+
configureServer: { order: "pre", handler(server) {
|
|
6469
|
+
// The icons and share images live in app/, which nothing serves; the
|
|
6470
|
+
// build copies them beside the client output. There is no output while
|
|
6471
|
+
// developing, so the same hrefs the head tags carry are answered from
|
|
6472
|
+
// app/ here - and the web manifest with them - or the tab has no icon
|
|
6473
|
+
// and the console a 404 for a file that is right there.
|
|
6474
|
+
server.middlewares.use((req, res, next) => {
|
|
6475
|
+
const url = (req.url ?? "").split("?")[0];
|
|
6476
|
+
// A worker registered by a production run on this origin - `bun run
|
|
6477
|
+
// start` on the port the dev server uses next - outlives that run and
|
|
6478
|
+
// answers the dev server's documents from its cache: a stored page is
|
|
6479
|
+
// cache-first, so a refresh brings back yesterday's document with
|
|
6480
|
+
// yesterday's module hashes, and a 504 for each. The browser fetches
|
|
6481
|
+
// /sw.js again on every navigation to look for an update; in
|
|
6482
|
+
// development that fetch gets a worker whose only job is to remove
|
|
6483
|
+
// itself, its caches, and reload the pages it controlled.
|
|
6484
|
+
// robots.txt, a hand-written sitemap.xml, humans.txt: beside the root
|
|
6485
|
+
// layout, served at the root. The build copies them beside the client
|
|
6486
|
+
// output; here they are read from app/.
|
|
6487
|
+
if (url.startsWith("/") && rootFiles.includes(url.slice(1))) {
|
|
6488
|
+
res.setHeader("Content-Type", rootFileType(url.slice(1)));
|
|
6489
|
+
res.end(readFileSync(join(sourceDir, "app", url.slice(1))));
|
|
6490
|
+
return;
|
|
6416
6491
|
}
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
// whose port is already taken on IPv4 binds IPv6 only and keeps the
|
|
6422
|
-
// number — so http://127.0.0.1:<port> is a reachable-looking address
|
|
6423
|
-
// that nothing answers, and the backend reports the renderer as down
|
|
6424
|
-
// while it is plainly running.
|
|
6425
|
-
const resolved = server.resolvedUrls?.local?.[0];
|
|
6426
|
-
const address = server.httpServer?.address();
|
|
6427
|
-
const url = resolved ??
|
|
6428
|
-
(typeof address === "object" && address
|
|
6429
|
-
? `http://${address.family === "IPv6" ? `[${address.address}]` : address.address}:${address.port}`
|
|
6430
|
-
: null);
|
|
6431
|
-
if (!url)
|
|
6492
|
+
if (url === "/sw.js") {
|
|
6493
|
+
res.setHeader("Content-Type", "text/javascript");
|
|
6494
|
+
res.setHeader("Cache-Control", "no-store");
|
|
6495
|
+
res.end(DEV_WORKER);
|
|
6432
6496
|
return;
|
|
6433
|
-
|
|
6434
|
-
|
|
6497
|
+
}
|
|
6498
|
+
if (webManifestOptions && url === MANIFEST_PATH) {
|
|
6499
|
+
res.setHeader("Content-Type", "application/manifest+json");
|
|
6500
|
+
res.end(webManifest(webManifestOptions));
|
|
6501
|
+
return;
|
|
6502
|
+
}
|
|
6503
|
+
const asset = allAppAssets(foundAssets).find((a) => a.href === url);
|
|
6504
|
+
const file = asset ? join(sourceDir, "app", asset.file) : null;
|
|
6505
|
+
if (!asset || !file || !existsSync(file))
|
|
6506
|
+
return next();
|
|
6507
|
+
res.setHeader("Content-Type", typeOf(asset.file));
|
|
6508
|
+
createReadStream(file).pipe(res);
|
|
6435
6509
|
});
|
|
6436
|
-
|
|
6437
|
-
|
|
6510
|
+
// rpc() has to reach the backend while the dev server is serving.
|
|
6511
|
+
//
|
|
6512
|
+
// A built deployment installs this itself: the server running
|
|
6513
|
+
// createRscHandler passes `hostCalls`. Nothing does that here, and with
|
|
6514
|
+
// no host installed every rpc() is refused — so a page whose data comes
|
|
6515
|
+
// from the backend renders its shell and then blanks, which reads as a
|
|
6516
|
+
// hydration bug rather than a missing wire.
|
|
6517
|
+
server.httpServer?.once("listening", async () => {
|
|
6518
|
+
const env = server.environments?.rsc;
|
|
6519
|
+
if (!env?.runner)
|
|
6520
|
+
return;
|
|
6521
|
+
// The app's own .env, unprefixed. A Laravel app already has both of
|
|
6522
|
+
// these, which is what makes this need no configuring: APP_URL is the
|
|
6523
|
+
// backend and RSC_HOST_CALL_SECRET is the secret it checks.
|
|
6524
|
+
const fromEnv = loadEnv(server.config.mode, projectRoot, "");
|
|
6525
|
+
const secret = hostCallOptions?.secret ?? fromEnv.RSC_HOST_CALL_SECRET;
|
|
6526
|
+
const origin = hostCallOptions?.endpoint ?? fromEnv.RSC_BACKEND ?? fromEnv.APP_URL;
|
|
6527
|
+
if (!secret || !origin)
|
|
6528
|
+
return;
|
|
6529
|
+
const path = hostCallOptions?.path ??
|
|
6530
|
+
fromEnv.RSC_HOST_CALL_PATH ??
|
|
6531
|
+
"/__rsc/host-call";
|
|
6532
|
+
const endpoint = origin.replace(/\/$/, "") + path;
|
|
6533
|
+
try {
|
|
6534
|
+
const entry = await env.runner.import(join(genDir, "entry.rsc.tsx"));
|
|
6535
|
+
const install = entry.installHostFn;
|
|
6536
|
+
install?.(httpHostCalls({ endpoint, secret }));
|
|
6537
|
+
}
|
|
6538
|
+
catch (error) {
|
|
6539
|
+
// Reported rather than thrown: the dev server is still useful for
|
|
6540
|
+
// every page that needs no data, and a failure here would otherwise
|
|
6541
|
+
// look like the server refusing to start.
|
|
6542
|
+
server.config.logger.warn(`[rsc-kit] could not wire host calls to ${endpoint}: ` +
|
|
6543
|
+
(error instanceof Error ? error.message : String(error)));
|
|
6544
|
+
}
|
|
6545
|
+
});
|
|
6546
|
+
// Written once the server is listening, because only then is the port
|
|
6547
|
+
// known. Removed on shutdown so a backend can tell a dev server that is
|
|
6548
|
+
// gone from one that is merely slow to answer.
|
|
6549
|
+
if (hotFile) {
|
|
6550
|
+
const remove = () => {
|
|
6551
|
+
try {
|
|
6552
|
+
if (existsSync(hotFile))
|
|
6553
|
+
rmSync(hotFile);
|
|
6554
|
+
}
|
|
6555
|
+
catch { }
|
|
6556
|
+
};
|
|
6557
|
+
server.httpServer?.once("listening", () => {
|
|
6558
|
+
// The url Vite resolved, not one built from the port. A dev server
|
|
6559
|
+
// whose port is already taken on IPv4 binds IPv6 only and keeps the
|
|
6560
|
+
// number — so http://127.0.0.1:<port> is a reachable-looking address
|
|
6561
|
+
// that nothing answers, and the backend reports the renderer as down
|
|
6562
|
+
// while it is plainly running.
|
|
6563
|
+
const resolved = server.resolvedUrls?.local?.[0];
|
|
6564
|
+
const address = server.httpServer?.address();
|
|
6565
|
+
const url = resolved ??
|
|
6566
|
+
(typeof address === "object" && address
|
|
6567
|
+
? `http://${address.family === "IPv6" ? `[${address.address}]` : address.address}:${address.port}`
|
|
6568
|
+
: null);
|
|
6569
|
+
if (!url)
|
|
6570
|
+
return;
|
|
6571
|
+
mkdirSync(dirname(hotFile), { recursive: true });
|
|
6572
|
+
writeFileSync(hotFile, url.replace(/\/$/, ""));
|
|
6573
|
+
});
|
|
6574
|
+
for (const signal of ["SIGINT", "SIGTERM", "exit"]) {
|
|
6575
|
+
process.once(signal, remove);
|
|
6576
|
+
}
|
|
6577
|
+
server.httpServer?.once("close", remove);
|
|
6438
6578
|
}
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
server.watcher.on("unlink", hostActionsChanged);
|
|
6482
|
-
},
|
|
6579
|
+
const shapes = new Set(ROUTE_FILES.map((name) => name));
|
|
6580
|
+
const affectsRouting = (file) => {
|
|
6581
|
+
if (!file.startsWith(sourceDir))
|
|
6582
|
+
return false;
|
|
6583
|
+
const base = file.split("/").pop() ?? "";
|
|
6584
|
+
const stem = base.replace(/\.(tsx|jsx|ts|js)$/, "");
|
|
6585
|
+
// The host's route-config file, whatever it named it. Hardcoding one
|
|
6586
|
+
// here would put a backend's convention back into a plugin that is
|
|
6587
|
+
// published without any — generic-host.test.ts fails if it reappears.
|
|
6588
|
+
return ((base !== stem && shapes.has(stem)) ||
|
|
6589
|
+
SECTION_FILE.test(base) ||
|
|
6590
|
+
(routeConfig !== null && base === routeConfig.file));
|
|
6591
|
+
};
|
|
6592
|
+
const restart = (file) => {
|
|
6593
|
+
if (!affectsRouting(file))
|
|
6594
|
+
return;
|
|
6595
|
+
server.config.logger.info(`[rsc-kit] route tree changed (${file.slice(sourceDir.length + 1)}) — restarting`);
|
|
6596
|
+
void server.restart();
|
|
6597
|
+
};
|
|
6598
|
+
// Watched explicitly: the Vite root is the *out* directory, so the app's
|
|
6599
|
+
// source tree is outside it and nothing would report a file appearing.
|
|
6600
|
+
server.watcher.add(sourceDir);
|
|
6601
|
+
server.watcher.on("add", restart);
|
|
6602
|
+
server.watcher.on("unlink", restart);
|
|
6603
|
+
// The host actions are read once, in config(), from the file the
|
|
6604
|
+
// backend writes; the "use server" stubs are generated from it. A
|
|
6605
|
+
// backend that writes a new action - `make:rsc-action` under a running
|
|
6606
|
+
// dev server - rewrites the file, and until the server started again
|
|
6607
|
+
// the stub was not there to import. Any event: the file appears on a
|
|
6608
|
+
// first action, changes on the next, and goes when the last is removed.
|
|
6609
|
+
const hostActionsPath = join(projectRoot, HOST_ACTIONS_FILE);
|
|
6610
|
+
const hostActionsChanged = (file) => {
|
|
6611
|
+
if (file !== hostActionsPath)
|
|
6612
|
+
return;
|
|
6613
|
+
server.config.logger.info(`[rsc-kit] ${HOST_ACTIONS_FILE} changed — restarting`);
|
|
6614
|
+
void server.restart();
|
|
6615
|
+
};
|
|
6616
|
+
server.watcher.add(hostActionsPath);
|
|
6617
|
+
server.watcher.on("add", hostActionsChanged);
|
|
6618
|
+
server.watcher.on("change", hostActionsChanged);
|
|
6619
|
+
server.watcher.on("unlink", hostActionsChanged);
|
|
6620
|
+
} },
|
|
6483
6621
|
/**
|
|
6484
6622
|
* Freeze what can be frozen, once every bundle exists.
|
|
6485
6623
|
*
|
|
@@ -6578,6 +6716,12 @@ export function rscKit(options = {}) {
|
|
|
6578
6716
|
if (config.command === "build" && !config.isProduction) {
|
|
6579
6717
|
throw new Error(refuseDevelopmentBuild(config));
|
|
6580
6718
|
}
|
|
6719
|
+
// Named before anything is bundled, in development and in a build: a
|
|
6720
|
+
// PUBLIC_ variable is one the app expects in the browser, and it would
|
|
6721
|
+
// arrive there as undefined. See publicVariablesSet.
|
|
6722
|
+
const publicSet = publicVariablesSet(config.mode, config.envDir);
|
|
6723
|
+
if (publicSet.length > 0)
|
|
6724
|
+
throw new Error(refusePublicVariables(publicSet));
|
|
6581
6725
|
isWatch = config.build?.watch != null;
|
|
6582
6726
|
resolvedConfig = config;
|
|
6583
6727
|
const nitroMain = config.plugins.find((p) => p.name === "nitro:main");
|
|
@@ -6627,6 +6771,7 @@ export function rscKit(options = {}) {
|
|
|
6627
6771
|
clientChunks: (meta) => meta.normalizedId,
|
|
6628
6772
|
...actionEncryptionKey(),
|
|
6629
6773
|
}),
|
|
6774
|
+
publicPrefixRefused(),
|
|
6630
6775
|
barrelImportsPlugin(),
|
|
6631
6776
|
useSsrModules(),
|
|
6632
6777
|
serverRendererInRsc(),
|