Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/vinext/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@
"types": "./dist/server/multi-stage.d.ts",
"import": "./dist/server/multi-stage.js"
},
"./server/request-stage": {
"types": "./dist/server/request-stage.d.ts",
"import": "./dist/server/request-stage.js"
},
"./server/response-stage": {
"types": "./dist/server/response-stage.d.ts",
"import": "./dist/server/response-stage.js"
},
"./server/app-router-entry": {
"types": "./dist/server/app-router-entry.d.ts",
"import": "./dist/server/app-router-entry.js"
Expand Down
82 changes: 82 additions & 0 deletions packages/vinext/src/build/client-build-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,23 @@ export function isRscFrameworkModule(id: string): boolean {
return pkg !== null && (FRAMEWORK_PACKAGES as readonly string[]).includes(pkg);
}

/**
* Keep virtual entry ids out of emitted RSC chunk filenames.
*
* Rolldown's entries-aware chunk names can contain the `\\0` virtual-id marker,
* which is not a portable module-specifier or filesystem name. Preserve every
* other character and remove both the printable and actual-NUL forms.
*/
export function sanitizeRscChunkFileName(name: string): string {
const withoutVirtualMarkers = name.replaceAll("\\0", "");
const invalid = new Set(["<", ">", ":", '"', "/", "\\", "|", "?", "*"]);
let sanitized = "";
for (const character of withoutVirtualMarkers) {
sanitized += character.charCodeAt(0) <= 31 || invalid.has(character) ? "_" : character;
}
return sanitized;
}

/**
* Output config that isolates React (and the RSC flight runtime) into a
* dedicated "framework" chunk in the RSC server build. See
Expand All @@ -245,6 +262,7 @@ export function isRscFrameworkModule(id: string): boolean {
*/
export function createRscFrameworkChunkOutputConfig() {
return {
sanitizeFileName: sanitizeRscChunkFileName,
codeSplitting: {
groups: [
{
Expand Down Expand Up @@ -287,3 +305,67 @@ export function withBuildBundlerOptions(
): Partial<VinextBuildConfig> {
return { rolldownOptions: bundlerOptions };
}

type VinextBuildOutput = Exclude<
NonNullable<VinextBuildBundlerOptions["output"]>,
readonly unknown[]
>;
type VinextCodeSplittingConfig = Exclude<NonNullable<VinextBuildOutput["codeSplitting"]>, boolean>;
type ChunkFileNames = NonNullable<VinextBuildOutput["chunkFileNames"]>;
type ChunkFileNameFunction = Exclude<ChunkFileNames, string>;

/**
* Keep vinext modules partitioned by the stage entries that actually use them.
* Without an entry-aware catch-all, Rolldown may merge a small helper shared by
* request/response entries into a response-heavy chunk; importing that helper
* then evaluates React and renderer code on a request-stage cache hit.
*/
export function createMultiStageCodeSplittingConfig(
existing: VinextBuildOutput["codeSplitting"],
): VinextCodeSplittingConfig & { groups: NonNullable<VinextCodeSplittingConfig["groups"]> } {
const base = existing && typeof existing === "object" ? existing : {};
return {
...base,
groups: [
{
name: "vinext-stage-runtime",
test: /(?:^|[/\\])(?:packages[/\\]vinext[/\\]src|(?:packages[/\\]vinext|node_modules[/\\](?:\.pnpm[/\\][^/\\]+[/\\]node_modules[/\\])?vinext)[/\\]dist)[/\\]/,
entriesAware: true,
},
...(base.groups ?? []),
],
};
}

/**
* Keep router stage chunks beside the server entry so their generated
* `./vinext-client-assets.js` external continues to resolve. Other chunks keep
* the host's existing output pattern (or vinext's server-assets default).
*/
export function createMultiStageChunkFileNames(
assetsDir: string,
existing: VinextBuildOutput["chunkFileNames"],
): ChunkFileNameFunction {
return (chunk) => {
const name = sanitizeRscChunkFileName(chunk.name);
if (
chunk.moduleIds?.some((id) =>
/\/server\/app-ssr-entry\.[cm]?[jt]sx?$/.test(toSlash(id.split("?", 1)[0] ?? "")),
) ||
[
"app-router-entry",
"pages-router-entry",
"app-response-stage-entry",
"pages-request-stage-entry",
"pages-response-stage-entry",
"virtual_vinext-rsc-entry",
"virtual_vinext-response-stage",
].some((entryName) => name.includes(entryName))
) {
return `${name}-[hash].js`;
}
if (typeof existing === "function") return existing({ ...chunk, name });
const pattern = existing ?? joinAssetFileNamePattern(assetsDir, "[name]-[hash].js");
return pattern.replaceAll("[name]", name);
};
}
7 changes: 5 additions & 2 deletions packages/vinext/src/cache/cache-adapters-virtual.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* request.
*/
import { flattenPluginOptions } from "../utils/plugin-options.js";
import type { VinextMultiStageOutput } from "../server/multi-stage.js";

/**
* A serializable pointer to a cache adapter module — the shape of each `cache`
Expand Down Expand Up @@ -69,6 +70,8 @@ export type CdnCacheAdapterCapabilities = {
};

export type CacheAdapterBuildOutput = {
/** Omitted for finalizer-only outputs; staged outputs use `multi-stage`. */
type?: undefined;
/** Whether this adapter owns output for the resolved build platform. */
matchesBuild?: (build: { plugins: readonly { name?: string }[] }) => boolean;
/** Finalize an emitted directory after other platform output hooks. */
Expand All @@ -87,8 +90,8 @@ export type CacheAdapterDescriptor<O extends Record<string, unknown> = Record<st
adapter: string;
/** JSON-serializable options forwarded to the factory at runtime. */
options?: O;
/** Optional adapter-owned finalization for platform-generated build output. */
output?: CacheAdapterBuildOutput;
/** Optional adapter-owned platform finalization or generic staged output. */
output?: CacheAdapterBuildOutput | VinextMultiStageOutput;
/** Build-time cache semantics used by shared request protocol code. */
capabilities?: CdnCacheAdapterCapabilities;
};
Expand Down
Loading
Loading