From f6437f032179c48a8931e8594d82480a613ea27f Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 11 May 2026 12:15:03 +0200 Subject: [PATCH] feat(plugin-rsc): expose RSC compatibility version --- packages/plugin-rsc/README.md | 40 +++ packages/plugin-rsc/e2e/basic.test.ts | 66 ++++ .../basic/src/framework/entry.rsc.tsx | 6 + packages/plugin-rsc/src/index.ts | 1 + packages/plugin-rsc/src/plugin.test.ts | 234 +++++++++++++ packages/plugin-rsc/src/plugin.ts | 311 +++++++++++++++++- packages/plugin-rsc/types/index.d.ts | 2 + packages/plugin-rsc/types/virtual.d.ts | 4 + 8 files changed, 663 insertions(+), 1 deletion(-) create mode 100644 packages/plugin-rsc/src/plugin.test.ts create mode 100644 packages/plugin-rsc/types/virtual.d.ts diff --git a/packages/plugin-rsc/README.md b/packages/plugin-rsc/README.md index a8780f9fc..bece7b4ec 100644 --- a/packages/plugin-rsc/README.md +++ b/packages/plugin-rsc/README.md @@ -442,6 +442,46 @@ export default defineConfig({ }) ``` +### Framework compatibility manifest + +Frameworks can import `virtual:vite-rsc/compatibility-manifest` from the RSC or +SSR environment to access compiler-owned deployment compatibility metadata. This +is intended for deployment skew protection: a framework can include +`compatibilityManifest.compatibilityVersion` in RSC responses and trigger a +document reload when a later RSC request comes from an incompatible client. + +```js +import compatibilityManifest from 'virtual:vite-rsc/compatibility-manifest' + +export function getRscResponseMetadata() { + return { + compatibilityVersion: compatibilityManifest.compatibilityVersion, + } +} +``` + +The manifest includes the Vite base path, RSC runtime package versions, a hash +of the final assets manifest, final output bundle hashes, client reference keys +with rendered exports, server reference keys with exported functions, and a hash +of the server-action encryption key when action closure encryption is actually +emitted. + +Frameworks with a custom build pipeline can use `getPluginApi(config).manager` +after the real RSC and client builds have completed: + +```js +import { getPluginApi } from '@vitejs/plugin-rsc' + +const manager = getPluginApi(config).manager +const manifest = manager.finalizeCompatibilityManifest() +const version = manifest.compatibilityVersion +``` + +`manager.getCompatibilityManifest()` and `manager.getCompatibilityVersion()` +throw during production builds until the manifest has been finalized. This +prevents scan builds or incomplete custom pipelines from accidentally emitting a +partial compatibility version. + ## RSC runtime (react-server-dom) API ### `@vitejs/plugin-rsc/rsc` diff --git a/packages/plugin-rsc/e2e/basic.test.ts b/packages/plugin-rsc/e2e/basic.test.ts index c3350b80f..90bfe13b7 100644 --- a/packages/plugin-rsc/e2e/basic.test.ts +++ b/packages/plugin-rsc/e2e/basic.test.ts @@ -18,6 +18,20 @@ import { waitForHydration, } from './helper' +function readCompatibilityManifest(f: Fixture, environmentName: string) { + return JSON.parse( + readFileSync( + path.join( + f.root, + 'dist', + environmentName, + '__vite_rsc_compatibility_manifest.js', + ), + 'utf-8', + ).slice('export default '.length), + ) +} + test.describe('dev-default', () => { const f = useFixture({ root: 'examples/basic', mode: 'dev' }) defineTest(f) @@ -428,6 +442,58 @@ function defineTest(f: Fixture) { manifest.clientReferenceDeps[hashString('src/routes/client.tsx')] expect(srcs).toEqual(expect.arrayContaining(deps.js)) }) + + test('compatibility manifest', async ({ page }) => { + const response = await page.request.get( + f.url('__test_compatibility_manifest'), + ) + expect(response.ok()).toBe(true) + const runtimeManifest = await response.json() + const rscManifest = readCompatibilityManifest(f, 'rsc') + const ssrManifest = readCompatibilityManifest(f, 'ssr') + + expect(runtimeManifest).toEqual(rscManifest) + expect(ssrManifest).toEqual(rscManifest) + expect(rscManifest).toMatchObject({ + version: 1, + compatibilityVersion: expect.stringMatching(/^[a-f0-9]{64}$/), + base: '/', + runtime: expect.objectContaining({ + '@vitejs/plugin-rsc': expect.any(String), + react: expect.any(String), + 'react-dom': expect.any(String), + vite: expect.any(String), + }), + assetsManifestHash: expect.stringMatching(/^[a-f0-9]{64}$/), + bundles: { + client: expect.stringMatching(/^[a-f0-9]{64}$/), + rsc: expect.stringMatching(/^[a-f0-9]{64}$/), + ssr: expect.stringMatching(/^[a-f0-9]{64}$/), + }, + clientReferences: expect.arrayContaining([ + expect.objectContaining({ + id: 'src/routes/client.tsx', + renderedExports: expect.arrayContaining([ + 'ClientCounter', + 'Hydrated', + ]), + }), + ]), + serverReferences: expect.arrayContaining([ + expect.objectContaining({ + id: 'src/routes/action/action.tsx', + exportNames: expect.arrayContaining([ + 'changeServerCounter', + 'getServerCounter', + 'resetServerCounter', + ]), + }), + ]), + }) + expect(rscManifest.compatibilityVersion).not.toBe( + rscManifest.assetsManifestHash, + ) + }) }) test.describe(() => { diff --git a/packages/plugin-rsc/examples/basic/src/framework/entry.rsc.tsx b/packages/plugin-rsc/examples/basic/src/framework/entry.rsc.tsx index e26f0b674..df2d403e2 100644 --- a/packages/plugin-rsc/examples/basic/src/framework/entry.rsc.tsx +++ b/packages/plugin-rsc/examples/basic/src/framework/entry.rsc.tsx @@ -128,6 +128,12 @@ async function handleRequest({ async function handler(request: Request): Promise { const url = new URL(request.url) + if (url.pathname === '/__test_compatibility_manifest') { + const { default: compatibilityManifest } = + await import('virtual:vite-rsc/compatibility-manifest') + return Response.json(compatibilityManifest) + } + const { Root } = await import('../routes/root.tsx') const nonce = !process.env.NO_CSP ? crypto.randomUUID() : undefined // https://vite.dev/guide/features.html#content-security-policy-csp diff --git a/packages/plugin-rsc/src/index.ts b/packages/plugin-rsc/src/index.ts index b3c2a7f0b..04229f923 100644 --- a/packages/plugin-rsc/src/index.ts +++ b/packages/plugin-rsc/src/index.ts @@ -1,6 +1,7 @@ export { default, type RscPluginOptions, + type RscCompatibilityManifest, getPluginApi, type PluginApi, } from './plugin' diff --git a/packages/plugin-rsc/src/plugin.test.ts b/packages/plugin-rsc/src/plugin.test.ts new file mode 100644 index 000000000..9dbfa95bf --- /dev/null +++ b/packages/plugin-rsc/src/plugin.test.ts @@ -0,0 +1,234 @@ +import { describe, expect, test } from 'vitest' +import { vitePluginRscMinimal, type PluginApi } from './plugin' + +describe('RscPluginManager compatibility version', () => { + test('throws during build before finalization', () => { + const manager = createManager() + + expect(() => manager.getCompatibilityManifest()).toThrow( + /compatibility manifest is not ready/, + ) + expect(() => manager.finalizeCompatibilityManifest()).toThrow( + /requires the final assets manifest/, + ) + }) + + test('serializes normalized references and final build fingerprints', () => { + const manager = createFinalizedManager({ + root: '/workspace/app', + base: '/base/', + }) + + expect(manager.getCompatibilityManifest()).toMatchObject({ + version: 1, + compatibilityVersion: expect.stringMatching(/^[a-f0-9]{64}$/), + base: '/base/', + assetsManifestHash: expect.stringMatching(/^[a-f0-9]{64}$/), + bundles: { + client: expect.stringMatching(/^[a-f0-9]{64}$/), + rsc: expect.stringMatching(/^[a-f0-9]{64}$/), + }, + clientReferences: [ + { + id: 'src/button.tsx', + referenceKey: 'button', + renderedExports: ['Button'], + }, + ], + serverReferences: [ + { + id: 'src/actions.ts', + referenceKey: 'actions', + exportNames: ['save'], + }, + ], + }) + expect(manager.getCompatibilityVersion()).toBe( + manager.getCompatibilityManifest().compatibilityVersion, + ) + }) + + test('ignores client exports that are not rendered', () => { + const manager = createFinalizedManager() + const before = manager.getCompatibilityVersion() + + manager.clientReferenceMetaMap[ + '/workspace/app/src/button.tsx' + ]!.exportNames = ['Button', 'Unused'] + manager.finalizeCompatibilityManifest() + + expect(manager.getCompatibilityVersion()).toBe(before) + }) + + test('changes when the rendered client export ABI changes', () => { + const manager = createFinalizedManager() + const before = manager.getCompatibilityVersion() + + manager.clientReferenceMetaMap[ + '/workspace/app/src/button.tsx' + ]!.renderedExports = ['Button', 'ButtonIcon'] + manager.finalizeCompatibilityManifest() + + expect(manager.getCompatibilityVersion()).not.toBe(before) + }) + + test('changes when the server reference ABI changes', () => { + const manager = createFinalizedManager() + const before = manager.getCompatibilityVersion() + + manager.serverReferenceMetaMap[ + '/workspace/app/src/actions.ts' + ]!.exportNames = ['delete', 'save'] + manager.finalizeCompatibilityManifest() + + expect(manager.getCompatibilityVersion()).not.toBe(before) + }) + + test('changes when the client assets manifest changes', () => { + const manager = createFinalizedManager() + const before = manager.getCompatibilityVersion() + + manager.buildAssetsManifest = { + ...manager.buildAssetsManifest!, + clientReferenceDeps: { + button: { + js: ['/assets/button.new.js'], + css: [], + }, + }, + } + manager.finalizeCompatibilityManifest() + + expect(manager.getCompatibilityVersion()).not.toBe(before) + }) + + test('changes when final client bundle content changes', () => { + const manager = createFinalizedManager() + const before = manager.getCompatibilityVersion() + + manager.bundles.client = createBundle({ + 'assets/button.js': 'export const Button = "new"', + }) + manager.finalizeCompatibilityManifest() + + expect(manager.getCompatibilityVersion()).not.toBe(before) + }) + + test('changes when final rsc bundle content changes', () => { + const manager = createFinalizedManager() + const before = manager.getCompatibilityVersion() + + manager.bundles.rsc = createBundle({ + 'index.js': 'export const root = "new-rsc"', + }) + manager.finalizeCompatibilityManifest() + + expect(manager.getCompatibilityVersion()).not.toBe(before) + }) + + test('changes when server action encryption key identity changes', () => { + const manager = createFinalizedManager() + manager.serverActionEncryptionKeyHash = 'key-a' + manager.finalizeCompatibilityManifest() + const before = manager.getCompatibilityVersion() + + manager.serverActionEncryptionKeyHash = 'key-b' + manager.finalizeCompatibilityManifest() + + expect(manager.getCompatibilityVersion()).not.toBe(before) + }) + + test('is stable across different absolute roots', () => { + const first = createFinalizedManager({ root: '/first/root' }) + first.clientReferenceMetaMap = { + '/first/root/src/button.tsx': { + importId: '/first/root/src/button.tsx', + referenceKey: 'button', + exportNames: ['Button'], + renderedExports: ['Button'], + }, + } + first.finalizeCompatibilityManifest() + + const second = createFinalizedManager({ root: '/second/root' }) + second.clientReferenceMetaMap = { + '/second/root/src/button.tsx': { + importId: '/second/root/src/button.tsx', + referenceKey: 'button', + exportNames: ['Button'], + renderedExports: ['Button'], + }, + } + second.finalizeCompatibilityManifest() + + expect(second.getCompatibilityVersion()).toBe( + first.getCompatibilityVersion(), + ) + }) +}) + +type ManagerOptions = { + base?: string + root?: string +} + +function createFinalizedManager(options: ManagerOptions = {}) { + const manager = createManager(options) + manager.clientReferenceMetaMap = { + [`${options.root ?? '/workspace/app'}/src/button.tsx`]: { + importId: `${options.root ?? '/workspace/app'}/src/button.tsx`, + referenceKey: 'button', + exportNames: ['Button'], + renderedExports: ['Button'], + }, + } + manager.serverReferenceMetaMap = { + [`${options.root ?? '/workspace/app'}/src/actions.ts`]: { + importId: `${options.root ?? '/workspace/app'}/src/actions.ts`, + referenceKey: 'actions', + exportNames: ['save'], + }, + } + manager.buildAssetsManifest = { + bootstrapScriptContent: 'import("/assets/index.js")', + clientReferenceDeps: { + button: { + js: ['/assets/button.js'], + css: [], + }, + }, + } + manager.bundles = { + client: createBundle({ + 'assets/button.js': 'export const Button = "old"', + }), + rsc: createBundle({ + 'index.js': 'export const root = "rsc"', + }), + } + manager.finalizeCompatibilityManifest() + return manager +} + +function createManager({ + base = '/', + root = '/workspace/app', +}: ManagerOptions = {}) { + const [plugin] = vitePluginRscMinimal() + const manager = (plugin as { api: PluginApi }).api.manager + manager.config = { base, command: 'build', root } as any + return manager +} + +function createBundle(chunks: Record) { + return Object.fromEntries( + Object.entries(chunks).map(([fileName, code]) => [ + fileName, + { + type: 'chunk', + fileName, + code, + }, + ]), + ) as any +} diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 5cbe58502..f732941e2 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -1,4 +1,5 @@ import assert from 'node:assert' +import { createHash } from 'node:crypto' import fs from 'node:fs' import { createRequire } from 'node:module' import path from 'node:path' @@ -77,6 +78,15 @@ import { createRpcServer } from './utils/rpc' const isRolldownVite = 'rolldownVersion' in vite const BUILD_ASSETS_MANIFEST_NAME = '__vite_rsc_assets_manifest.js' +const BUILD_COMPATIBILITY_MANIFEST_NAME = '__vite_rsc_compatibility_manifest.js' + +const COMPATIBILITY_RUNTIME_PACKAGES = [ + '@vitejs/plugin-rsc', + 'react', + 'react-dom', + 'react-server-dom-webpack', + 'vite', +] type ClientReferenceMeta = { importId: string @@ -97,6 +107,27 @@ type ServerRerferenceMeta = { exportNames: string[] } +export type RscCompatibilityManifest = { + version: 1 + compatibilityVersion: string + base: string + runtime: Record + assetsManifestHash: string + bundles: Record + clientReferences: Array<{ + id: string + referenceKey: string + packageSource?: string + renderedExports: string[] + }> + serverReferences: Array<{ + id: string + referenceKey: string + exportNames: string[] + }> + serverActionEncryptionKeyHash?: string +} + const PKG_NAME = '@vitejs/plugin-rsc' const REACT_SERVER_DOM_NAME = `${PKG_NAME}/vendor/react-server-dom` @@ -121,11 +152,13 @@ class RscPluginManager { config!: ResolvedConfig bundles: Record = {} buildAssetsManifest: AssetsManifest | undefined + buildCompatibilityManifest: RscCompatibilityManifest | undefined isScanBuild: boolean = false clientReferenceMetaMap: Record = {} clientReferenceGroups: Record = {} serverReferenceMetaMap: Record = {} + serverActionEncryptionKeyHash: string | undefined serverResourcesMetaMap: Record = {} environmentImportMetaMap: Record< string, // sourceEnv @@ -166,6 +199,229 @@ class RscPluginManager { writeEnvironmentImportsManifest(): void { writeEnvironmentImportsManifest(this) } + + finalizeCompatibilityManifest(): RscCompatibilityManifest { + if (this.isScanBuild) { + throw new Error( + `[vite-rsc] compatibility manifest cannot be finalized during a scan build`, + ) + } + if (!this.buildAssetsManifest) { + throw new Error( + `[vite-rsc] compatibility manifest requires the final assets manifest. ` + + `Run the client build before calling 'manager.finalizeCompatibilityManifest()'.`, + ) + } + if (!this.bundles.client || !this.bundles.rsc) { + throw new Error( + `[vite-rsc] compatibility manifest requires final client and rsc bundles. ` + + `Run the real client and rsc builds before calling 'manager.finalizeCompatibilityManifest()'.`, + ) + } + this.buildCompatibilityManifest = createCompatibilityManifest(this, { + assetsManifestHash: hashCompatibilityObject(this.buildAssetsManifest), + bundles: hashCompatibilityBundles(this.bundles), + }) + return this.buildCompatibilityManifest + } + + writeCompatibilityManifest(environmentNames: string[]): void { + const compatibilityManifestCode = `export default ${JSON.stringify( + this.getCompatibilityManifest(), + null, + 2, + )}` + for (const name of environmentNames) { + const manifestPath = path.join( + this.config.environments[name]!.build.outDir, + BUILD_COMPATIBILITY_MANIFEST_NAME, + ) + fs.writeFileSync(manifestPath, compatibilityManifestCode) + } + } + + getCompatibilityManifest(): RscCompatibilityManifest { + if (this.config.command !== 'build') { + return createCompatibilityManifest(this, { + assetsManifestHash: 'dev', + bundles: {}, + }) + } + if (!this.buildCompatibilityManifest) { + throw new Error( + `[vite-rsc] compatibility manifest is not ready. ` + + `Call 'manager.finalizeCompatibilityManifest()' after the final client build, ` + + `or import 'virtual:vite-rsc/compatibility-manifest' at runtime.`, + ) + } + return this.buildCompatibilityManifest + } + + getCompatibilityVersion(): string { + return this.getCompatibilityManifest().compatibilityVersion + } + + toCompatibilityId(id: string): string { + return normalizePath(path.isAbsolute(id) ? this.toRelativeId(id) : id) + } +} + +type RscCompatibilityManifestPayload = Omit< + RscCompatibilityManifest, + 'compatibilityVersion' +> + +function createCompatibilityManifest( + manager: RscPluginManager, + { + assetsManifestHash, + bundles, + }: { + assetsManifestHash: string + bundles: Record + }, +): RscCompatibilityManifest { + const payload: RscCompatibilityManifestPayload = { + version: 1, + base: manager.config.base, + runtime: Object.fromEntries( + COMPATIBILITY_RUNTIME_PACKAGES.map((packageName) => [ + packageName, + getPackageVersion(packageName), + ]), + ), + assetsManifestHash, + bundles, + clientReferences: Object.values(manager.clientReferenceMetaMap) + .map((meta) => ({ + id: manager.toCompatibilityId(meta.importId), + referenceKey: meta.referenceKey, + packageSource: meta.packageSource, + renderedExports: [...meta.renderedExports].sort(), + })) + .sort(compareClientCompatibilityReferences), + serverReferences: Object.values(manager.serverReferenceMetaMap) + .map((meta) => ({ + id: manager.toCompatibilityId(meta.importId), + referenceKey: meta.referenceKey, + exportNames: [...meta.exportNames].sort(), + })) + .sort(compareServerCompatibilityReferences), + } + if (manager.serverActionEncryptionKeyHash) { + payload.serverActionEncryptionKeyHash = + manager.serverActionEncryptionKeyHash + } + const compatibilityVersion = hashCompatibilityObject(payload) + return { + version: payload.version, + compatibilityVersion, + base: payload.base, + runtime: payload.runtime, + assetsManifestHash: payload.assetsManifestHash, + bundles: payload.bundles, + clientReferences: payload.clientReferences, + serverReferences: payload.serverReferences, + ...(payload.serverActionEncryptionKeyHash + ? { + serverActionEncryptionKeyHash: payload.serverActionEncryptionKeyHash, + } + : {}), + } +} + +function compareClientCompatibilityReferences( + a: RscCompatibilityManifest['clientReferences'][number], + b: RscCompatibilityManifest['clientReferences'][number], +): number { + return ( + a.referenceKey.localeCompare(b.referenceKey) || + a.id.localeCompare(b.id) || + (a.packageSource ?? '').localeCompare(b.packageSource ?? '') + ) +} + +function compareServerCompatibilityReferences( + a: RscCompatibilityManifest['serverReferences'][number], + b: RscCompatibilityManifest['serverReferences'][number], +): number { + return ( + a.referenceKey.localeCompare(b.referenceKey) || a.id.localeCompare(b.id) + ) +} + +function hashCompatibilityValue(value: string | Uint8Array): string { + return createHash('sha256').update(value).digest('hex') +} + +function hashCompatibilityObject(value: unknown): string { + return hashCompatibilityValue( + JSON.stringify(normalizeCompatibilityValue(value)), + ) +} + +function normalizeCompatibilityValue(value: unknown): unknown { + if (value instanceof RuntimeAsset) { + return { runtime: value.runtime } + } + if (Array.isArray(value)) { + return value.map((item) => normalizeCompatibilityValue(item)) + } + if (value && typeof value === 'object') { + return Object.fromEntries( + Object.entries(value as Record) + .filter(([, child]) => child !== undefined) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, child]) => [key, normalizeCompatibilityValue(child)]), + ) + } + return value +} + +function hashCompatibilityBundles( + bundles: Record, +): Record { + return Object.fromEntries( + Object.entries(bundles) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([name, bundle]) => [name, hashCompatibilityBundle(bundle)]), + ) +} + +function hashCompatibilityBundle(bundle: Rollup.OutputBundle): string { + return hashCompatibilityObject( + Object.values(bundle) + .filter((output) => !output.fileName.endsWith('.map')) + .map((output) => { + if (output.type === 'chunk') { + return { + type: output.type, + fileName: output.fileName, + codeHash: hashCompatibilityValue(output.code), + } + } + return { + type: output.type, + fileName: output.fileName, + sourceHash: hashCompatibilityValue( + typeof output.source === 'string' ? output.source : output.source, + ), + } + }) + .sort((a, b) => a.fileName.localeCompare(b.fileName)), + ) +} + +function getPackageVersion(packageName: string): string { + try { + const packageJsonPath = require.resolve(`${packageName}/package.json`) + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) + return typeof packageJson.version === 'string' + ? packageJson.version + : 'unknown' + } catch { + return 'unknown' + } } export type RscPluginOptions = { @@ -338,7 +594,7 @@ export function vitePluginRscMinimal( ...vitePluginRscCore(), ...vitePluginUseClient(rscPluginOptions, manager), ...vitePluginUseServer(rscPluginOptions, manager), - ...vitePluginDefineEncryptionKey(rscPluginOptions), + ...vitePluginDefineEncryptionKey(rscPluginOptions, manager), { name: 'rsc:reference-validation', apply: 'serve', @@ -448,6 +704,8 @@ export default function vitePluginRsc( } manager.writeAssetsManifest(['ssr', 'rsc']) + manager.finalizeCompatibilityManifest() + manager.writeCompatibilityManifest(['ssr', 'rsc']) manager.writeEnvironmentImportsManifest() } @@ -1221,6 +1479,53 @@ export function createRpcClient(params) { return }, }, + { + name: 'rsc:virtual:vite-rsc/compatibility-manifest', + resolveId: { + filter: { id: exactRegex('virtual:vite-rsc/compatibility-manifest') }, + handler(source) { + if (source === 'virtual:vite-rsc/compatibility-manifest') { + if (this.environment.mode === 'build') { + return { id: source, external: true } + } + return `\0` + source + } + }, + }, + load: { + filter: { + id: exactRegex('\0virtual:vite-rsc/compatibility-manifest'), + }, + handler(id) { + if (id === '\0virtual:vite-rsc/compatibility-manifest') { + assert(this.environment.name !== 'client') + assert(this.environment.mode === 'dev') + return `export default ${JSON.stringify( + manager.getCompatibilityManifest(), + null, + 2, + )}` + } + }, + }, + renderChunk(code, chunk) { + if (code.includes('virtual:vite-rsc/compatibility-manifest')) { + assert(this.environment.name !== 'client') + const replacement = normalizeRelativePath( + path.relative( + path.join(chunk.fileName, '..'), + BUILD_COMPATIBILITY_MANIFEST_NAME, + ), + ) + code = code.replaceAll( + 'virtual:vite-rsc/compatibility-manifest', + () => replacement, + ) + return { code } + } + return + }, + }, createVirtualPlugin('vite-rsc/bootstrap-script-content', function () { assert(this.environment.name !== 'client') return `\ @@ -1820,6 +2125,7 @@ function vitePluginDefineEncryptionKey( RscPluginOptions, 'defineEncryptionKey' | 'environment' >, + manager: RscPluginManager, ): Plugin[] { let defineEncryptionKey: string let emitEncryptionKey = false @@ -1833,6 +2139,7 @@ function vitePluginDefineEncryptionKey( name: 'rsc:encryption-key', async configEnvironment(name, _config, env) { if (name === serverEnvironmentName && !env.isPreview) { + manager.serverActionEncryptionKeyHash = undefined defineEncryptionKey = useServerPluginOptions.defineEncryptionKey ?? JSON.stringify(toBase64(await generateEncryptionKey())) @@ -1863,6 +2170,8 @@ function vitePluginDefineEncryptionKey( if (code.includes(KEY_PLACEHOLDER)) { assert.equal(this.environment.name, serverEnvironmentName) emitEncryptionKey = true + manager.serverActionEncryptionKeyHash = + hashCompatibilityValue(defineEncryptionKey) const normalizedPath = normalizeRelativePath( path.relative(path.join(chunk.fileName, '..'), KEY_FILE), ) diff --git a/packages/plugin-rsc/types/index.d.ts b/packages/plugin-rsc/types/index.d.ts index 5eafb4ee1..ead173ac9 100644 --- a/packages/plugin-rsc/types/index.d.ts +++ b/packages/plugin-rsc/types/index.d.ts @@ -1,3 +1,5 @@ +/// + declare global { interface ImportMeta { readonly viteRsc: { diff --git a/packages/plugin-rsc/types/virtual.d.ts b/packages/plugin-rsc/types/virtual.d.ts new file mode 100644 index 000000000..c06b2217f --- /dev/null +++ b/packages/plugin-rsc/types/virtual.d.ts @@ -0,0 +1,4 @@ +declare module 'virtual:vite-rsc/compatibility-manifest' { + const compatibilityManifest: import('@vitejs/plugin-rsc').RscCompatibilityManifest + export default compatibilityManifest +}