1
- import type { Plugin } from 'vite'
1
+ import path from 'node:path'
2
+ import type { DevEnvironment , Plugin , Rollup } from 'vite'
2
3
3
4
// https://github.com/vercel/next.js/blob/90f564d376153fe0b5808eab7b83665ee5e08aaf/packages/next/src/build/webpack-config.ts#L1249-L1280
4
5
// https://github.com/pcattori/vite-env-only/blob/68a0cc8546b9a37c181c0b0a025eb9b62dbedd09/src/deny-imports.ts
@@ -8,37 +9,141 @@ export function validateImportPlugin(): Plugin {
8
9
name : 'rsc:validate-imports' ,
9
10
resolveId : {
10
11
order : 'pre' ,
11
- async handler ( source , importer , options ) {
12
+ async handler ( source , _importer , options ) {
12
13
// optimizer is not aware of server/client boudnary so skip
13
14
if ( 'scan' in options && options . scan ) {
14
15
return
15
16
}
16
17
17
18
// 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
+ }
23
28
}
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 ,
31
32
}
32
- return { id : `\0virtual:vite-rsc/empty` , moduleSideEffects : false }
33
33
}
34
34
35
35
return
36
36
} ,
37
37
} ,
38
38
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/' ) ) {
40
45
return `export {}`
41
46
}
42
47
} ,
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 ] } )
43
147
}
148
+ throw error
44
149
}
0 commit comments