1- import type { Plugin } from 'vite'
1+ import path from 'node:path'
2+ import type { DevEnvironment , Plugin , Rollup } from 'vite'
23
34// https://github.com/vercel/next.js/blob/90f564d376153fe0b5808eab7b83665ee5e08aaf/packages/next/src/build/webpack-config.ts#L1249-L1280
45// https://github.com/pcattori/vite-env-only/blob/68a0cc8546b9a37c181c0b0a025eb9b62dbedd09/src/deny-imports.ts
@@ -8,37 +9,141 @@ export function validateImportPlugin(): Plugin {
89 name : 'rsc:validate-imports' ,
910 resolveId : {
1011 order : 'pre' ,
11- async handler ( source , importer , options ) {
12+ async handler ( source , _importer , options ) {
1213 // optimizer is not aware of server/client boudnary so skip
1314 if ( 'scan' in options && options . scan ) {
1415 return
1516 }
1617
1718 // Validate client-only imports in server environments
18- if ( source === 'client-only' ) {
19- if ( this . environment . name === 'rsc' ) {
20- throw new Error (
21- `'client-only' cannot be imported in server build (importer: '${ importer ?? 'unknown' } ', environment: ${ this . environment . name } )` ,
22- )
19+ if ( source === 'client-only' || source === 'server-only' ) {
20+ if (
21+ ( source === 'client-only' && this . environment . name === 'rsc' ) ||
22+ ( source === 'server-only' && this . environment . name !== 'rsc' )
23+ ) {
24+ return {
25+ id : `\0virtual:vite-rsc/validate-imports/invalid/${ source } ` ,
26+ moduleSideEffects : true ,
27+ }
2328 }
24- return { id : `\0virtual:vite-rsc/empty` , moduleSideEffects : false }
25- }
26- if ( source === 'server-only' ) {
27- if ( this . environment . name !== 'rsc' ) {
28- throw new Error (
29- `'server-only' cannot be imported in client build (importer: '${ importer ?? 'unknown' } ', environment: ${ this . environment . name } )` ,
30- )
29+ return {
30+ id : `\0virtual:vite-rsc/validate-imports/valid/${ source } ` ,
31+ moduleSideEffects : false ,
3132 }
32- return { id : `\0virtual:vite-rsc/empty` , moduleSideEffects : false }
3333 }
3434
3535 return
3636 } ,
3737 } ,
3838 load ( id ) {
39- if ( id . startsWith ( '\0virtual:vite-rsc/empty' ) ) {
39+ if ( id . startsWith ( '\0virtual:vite-rsc/validate-imports/invalid/' ) ) {
40+ // it should surface as build error but we make a runtime error just in case.
41+ const source = id . slice ( id . lastIndexOf ( '/' ) + 1 )
42+ return `throw new Error("invalid import of '${ source } '")`
43+ }
44+ if ( id . startsWith ( '\0virtual:vite-rsc/validate-imports/' ) ) {
4045 return `export {}`
4146 }
4247 } ,
48+ // for dev, use DevEnvironment.moduleGraph during post transform
49+ transform : {
50+ order : 'post' ,
51+ async handler ( _code , id ) {
52+ if ( this . environment . mode === 'dev' ) {
53+ if ( id . startsWith ( `\0virtual:vite-rsc/validate-imports/invalid/` ) ) {
54+ const chain = getImportChainDev ( this . environment , id )
55+ validateImportChain (
56+ chain ,
57+ this . environment . name ,
58+ this . environment . config . root ,
59+ )
60+ }
61+ }
62+ } ,
63+ } ,
64+ // for build, use PluginContext.getModuleInfo during buildEnd.
65+ // rollup shows multiple errors if there are other build error from `buildEnd(error)`.
66+ buildEnd ( ) {
67+ if ( this . environment . mode === 'build' ) {
68+ const serverOnly = getImportChainBuild (
69+ this ,
70+ '\0virtual:vite-rsc/validate-imports/invalid/server-only' ,
71+ )
72+ validateImportChain (
73+ serverOnly ,
74+ this . environment . name ,
75+ this . environment . config . root ,
76+ )
77+ const clientOnly = getImportChainBuild (
78+ this ,
79+ '\0virtual:vite-rsc/validate-imports/invalid/client-only' ,
80+ )
81+ validateImportChain (
82+ clientOnly ,
83+ this . environment . name ,
84+ this . environment . config . root ,
85+ )
86+ }
87+ } ,
88+ }
89+ }
90+
91+ function getImportChainDev ( environment : DevEnvironment , id : string ) {
92+ const chain : string [ ] = [ ]
93+ const recurse = ( id : string ) => {
94+ if ( chain . includes ( id ) ) return
95+ const info = environment . moduleGraph . getModuleById ( id )
96+ if ( ! info ) return
97+ chain . push ( id )
98+ const next = [ ...info . importers ] [ 0 ]
99+ if ( next && next . id ) {
100+ recurse ( next . id )
101+ }
102+ }
103+ recurse ( id )
104+ return chain
105+ }
106+
107+ function getImportChainBuild ( ctx : Rollup . PluginContext , id : string ) : string [ ] {
108+ const chain : string [ ] = [ ]
109+ const recurse = ( id : string ) => {
110+ if ( chain . includes ( id ) ) return
111+ const info = ctx . getModuleInfo ( id )
112+ if ( ! info ) return
113+ chain . push ( id )
114+ const next = info . importers [ 0 ]
115+ if ( next ) {
116+ recurse ( next )
117+ }
118+ }
119+ recurse ( id )
120+ return chain
121+ }
122+
123+ function validateImportChain (
124+ chain : string [ ] ,
125+ environmentName : string ,
126+ root : string ,
127+ ) {
128+ if ( chain . length === 0 ) return
129+ const id = chain [ 0 ] !
130+ const source = id . slice ( id . lastIndexOf ( '/' ) + 1 )
131+ const buildName = source === 'server-only' ? 'client' : 'server'
132+ let result = `'${ source } ' cannot be imported in ${ buildName } build ('${ environmentName } ' environment):\n`
133+ result += chain
134+ . slice ( 1 , 6 )
135+ . map (
136+ ( id , i ) =>
137+ ' ' . repeat ( i + 1 ) +
138+ `imported by ${ path . relative ( root , id ) . replaceAll ( '\0' , '' ) } \n` ,
139+ )
140+ . join ( '' )
141+ if ( chain . length > 6 ) {
142+ result += ' ' . repeat ( 7 ) + '...\n'
143+ }
144+ const error = new Error ( result )
145+ if ( chain [ 1 ] ) {
146+ Object . assign ( error , { id : chain [ 1 ] } )
43147 }
148+ throw error
44149}
0 commit comments