1+ import { createHmac , randomBytes } from "node:crypto" ;
12import { createRequire } from "node:module" ;
23import path from "pathslash" ;
34import { pathToFileURL } from "node:url" ;
@@ -166,6 +167,30 @@ function hasFunctionDirective(
166167 ) ;
167168}
168169
170+ function getFunctionDirectiveExportNames (
171+ transforms : RscTransforms ,
172+ ast : Program ,
173+ directive : string ,
174+ ) : Set < string > {
175+ const names = new Set < string > ( ) ;
176+ for ( const group of transforms . scanModuleExports ( ast ) ) {
177+ const entries =
178+ group . type === "declaration"
179+ ? [ group . export ]
180+ : group . type === "variable-declaration"
181+ ? group . declarators . flatMap ( ( declarator ) => declarator . exports )
182+ : group . type === "specifiers"
183+ ? group . exports
184+ : group . type === "default"
185+ ? [ { exportName : "default" , meta : group . meta } ]
186+ : [ ] ;
187+ for ( const entry of entries ) {
188+ if ( hasFunctionDirective ( entry . meta , directive ) ) names . add ( entry . exportName ) ;
189+ }
190+ }
191+ return names ;
192+ }
193+
169194function getCacheWrapperOptions (
170195 options : Options ,
171196 id : string ,
@@ -193,6 +218,13 @@ export async function createUseCacheCallablePlugin(options: Options): Promise<Pl
193218 pathToFileURL ( rscModulePath ) . href
194219 ) ;
195220 const transforms : RscTransforms = await import ( pathToFileURL ( transformsPath ) . href ) ;
221+ // Cache functions use React's server-reference transport when they are passed
222+ // to Client Components or invoked by the Response Store. The upstream RSC
223+ // plugin's reference key is a public, deterministic module-path hash, so the
224+ // original export name must not also be the remotely addressable name. A
225+ // per-plugin secret keeps aliases stable across every environment/build pass
226+ // in one Vite build without making sibling exports derivable from each other.
227+ const referenceSecret = randomBytes ( 32 ) ;
196228 let manager : RscPluginManager | undefined ;
197229
198230 return {
@@ -241,6 +273,13 @@ export async function createUseCacheCallablePlugin(options: Options): Promise<Pl
241273 }
242274
243275 const reference = manager . serverReferences . resolve ( id , "rsc" ) ;
276+ const relativeImportId = manager . toRelativeId ( reference . importId ) ;
277+ const secureExportName = ( name : string ) =>
278+ `$$vinext_cache_${ createHmac ( "sha256" , referenceSecret )
279+ . update ( relativeImportId )
280+ . update ( "\0" )
281+ . update ( name )
282+ . digest ( "hex" ) } `;
244283 const isRsc = this . environment . name === "rsc" ;
245284
246285 if ( ! isRsc ) {
@@ -263,6 +302,11 @@ export async function createUseCacheCallablePlugin(options: Options): Promise<Pl
263302 return ;
264303 }
265304
305+ const useServerExportNames = getFunctionDirectiveExportNames (
306+ transforms ,
307+ ast ,
308+ "use server" ,
309+ ) ;
266310 const result = transforms . transformDirectiveProxyExport ( ast , {
267311 code,
268312 directive : moduleDirective ,
@@ -272,7 +316,7 @@ export async function createUseCacheCallablePlugin(options: Options): Promise<Pl
272316 return true ;
273317 } ,
274318 runtime : ( name ) =>
275- `$$ReactClient.createServerReference(${ JSON . stringify ( `${ reference . referenceKey } #${ name } ` ) } ,$$ReactClient.callServer,undefined,${ this . environment . mode === "dev" ? "$$ReactClient.findSourceMapURL" : "undefined" } ,${ JSON . stringify ( name ) } )` ,
319+ `$$ReactClient.createServerReference(${ JSON . stringify ( `${ reference . referenceKey } #${ useServerExportNames . has ( name ) ? name : secureExportName ( name ) } ` ) } ,$$ReactClient.callServer,undefined,${ this . environment . mode === "dev" ? "$$ReactClient.findSourceMapURL" : "undefined" } ,${ JSON . stringify ( name ) } )` ,
276320 } ) ;
277321 if ( ! result ?. output . hasChanged ( ) ) {
278322 manager . serverReferences . deleteClaim ( PLUGIN_NAME , id ) ;
@@ -281,7 +325,9 @@ export async function createUseCacheCallablePlugin(options: Options): Promise<Pl
281325
282326 manager . serverReferences . replaceClaim ( PLUGIN_NAME , id , {
283327 ...reference ,
284- exportNames : result . exportNames ,
328+ exportNames : result . exportNames . map ( ( name ) =>
329+ useServerExportNames . has ( name ) ? name : secureExportName ( name ) ,
330+ ) ,
285331 } ) ;
286332 const runtimeEnvironment = this . environment . name === "client" ? "browser" : "ssr" ;
287333 result . output . prepend (
@@ -290,6 +336,7 @@ export async function createUseCacheCallablePlugin(options: Options): Promise<Pl
290336 return magicStringTransformResult ( result . output , { hires : "boundary" , source : id } ) ;
291337 }
292338
339+ const secureExports = new Set < string > ( ) ;
293340 const wrap = (
294341 value : string ,
295342 name : string ,
@@ -298,9 +345,11 @@ export async function createUseCacheCallablePlugin(options: Options): Promise<Pl
298345 isModuleDirective : boolean ,
299346 ) => {
300347 const variant = directiveMatch [ 1 ] ?? "" ;
348+ const secureName = secureExportName ( name ) ;
349+ secureExports . add ( secureName ) ;
301350 const wrapperOptions = {
302351 ...getCacheWrapperOptions ( options , id , name , isModuleDirective , meta ) ,
303- serverReferenceId : `${ reference . referenceKey } #${ name } ` ,
352+ serverReferenceId : `${ reference . referenceKey } #${ secureName } ` ,
304353 } ;
305354 return `$$cacheRuntime.registerCachedFunction(${ value } , ${ JSON . stringify ( `${ id } :${ name } ` ) } , ${ JSON . stringify ( variant ) } , ${ JSON . stringify ( wrapperOptions ) } )` ;
306355 } ;
@@ -313,8 +362,9 @@ export async function createUseCacheCallablePlugin(options: Options): Promise<Pl
313362 isModuleDirective : boolean ,
314363 ) => {
315364 const cached = wrap ( value , name , directiveMatch , meta , isModuleDirective ) ;
365+ const secureName = secureExportName ( name ) ;
316366 needsReactServer = true ;
317- return `$$ VinextReactServer.registerServerReference(${ cached } , ${ JSON . stringify ( reference . referenceKey ) } , ${ JSON . stringify ( name ) } )` ;
367+ return `( ${ secureName } = $$ VinextReactServer.registerServerReference(${ cached } , ${ JSON . stringify ( reference . referenceKey ) } , ${ JSON . stringify ( secureName ) } ) )` ;
318368 } ;
319369
320370 const result = moduleDirective
@@ -336,6 +386,7 @@ export async function createUseCacheCallablePlugin(options: Options): Promise<Pl
336386 directive : USE_CACHE_DIRECTIVE_CANDIDATE ,
337387 rejectNonAsyncFunction : true ,
338388 hoistRuntime : true ,
389+ noExport : true ,
339390 runtime : ( value , name , meta ) =>
340391 runtime ( value , name , matchUseCacheDirective ( meta . directiveMatch [ 0 ] ) , meta , false ) ,
341392 encode : ( value ) => `$$cacheRuntime.encryptCacheCaptures(${ value } )` ,
@@ -348,7 +399,7 @@ export async function createUseCacheCallablePlugin(options: Options): Promise<Pl
348399
349400 manager . serverReferences . replaceClaim ( PLUGIN_NAME , id , {
350401 ...reference ,
351- exportNames : "names" in result ? result . names : result . exportNames ,
402+ exportNames : [ ... secureExports ] ,
352403 } ) ;
353404 const importPosition =
354405 ast . body . find ( ( node ) => ! ( "directive" in node ) ) ?. start ?? code . length ;
@@ -358,10 +409,14 @@ export async function createUseCacheCallablePlugin(options: Options): Promise<Pl
358409 `import * as $$cacheRuntime from ${ JSON . stringify ( options . cacheRuntime ) } ;` ,
359410 needsReactServer &&
360411 `import * as $$VinextReactServer from "@vitejs/plugin-rsc/react/rsc/server";` ,
412+ secureExports . size > 0 && `let ${ [ ...secureExports ] . join ( "," ) } ;` ,
361413 ]
362414 . filter ( Boolean )
363415 . join ( "\n" ) + "\n" ,
364416 ) ;
417+ if ( secureExports . size > 0 ) {
418+ result . output . append ( `\nexport { ${ [ ...secureExports ] . join ( "," ) } };\n` ) ;
419+ }
365420 return magicStringTransformResult ( result . output , { hires : "boundary" , source : id } ) ;
366421 } ,
367422 } ,
0 commit comments