1
- import type { Plugin } from 'vite'
1
+ import type { DevEnvironment , Plugin , Rollup } from 'vite'
2
2
3
3
// https://github.com/vercel/next.js/blob/90f564d376153fe0b5808eab7b83665ee5e08aaf/packages/next/src/build/webpack-config.ts#L1249-L1280
4
4
// https://github.com/pcattori/vite-env-only/blob/68a0cc8546b9a37c181c0b0a025eb9b62dbedd09/src/deny-imports.ts
@@ -8,37 +8,131 @@ export function validateImportPlugin(): Plugin {
8
8
name : 'rsc:validate-imports' ,
9
9
resolveId : {
10
10
order : 'pre' ,
11
- async handler ( source , importer , options ) {
11
+ async handler ( source , _importer , options ) {
12
12
// optimizer is not aware of server/client boudnary so skip
13
13
if ( 'scan' in options && options . scan ) {
14
14
return
15
15
}
16
16
17
17
// 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
- )
18
+ if ( source === 'client-only' || source === 'server-only' ) {
19
+ if (
20
+ ( source === 'client-only' && this . environment . name === 'rsc' ) ||
21
+ ( source === 'server-only' && this . environment . name !== 'rsc' )
22
+ ) {
23
+ return {
24
+ id : `\0virtual:vite-rsc/validate-imports/invalid/${ source } ` ,
25
+ moduleSideEffects : true ,
26
+ }
23
27
}
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
- )
28
+ return {
29
+ id : `\0virtual:vite-rsc/validate-imports/valid/${ source } ` ,
30
+ moduleSideEffects : false ,
31
31
}
32
- return { id : `\0virtual:vite-rsc/empty` , moduleSideEffects : false }
33
32
}
34
33
35
34
return
36
35
} ,
37
36
} ,
38
37
load ( id ) {
39
- if ( id . startsWith ( '\0virtual:vite-rsc/empty' ) ) {
38
+ if ( id . startsWith ( '\0virtual:vite-rsc/validate-imports/invalid/' ) ) {
39
+ // it should surface as build error but we make a runtime error just in case.
40
+ const source = id . slice ( id . lastIndexOf ( '/' ) + 1 )
41
+ return `throw new Error("invalid import of '${ source } '")`
42
+ }
43
+ if ( id . startsWith ( '\0virtual:vite-rsc/validate-imports/' ) ) {
40
44
return `export {}`
41
45
}
42
46
} ,
47
+ // need a different way to probe module graph for dev and build
48
+ transform : {
49
+ order : 'post' ,
50
+ async handler ( _code , id ) {
51
+ if ( this . environment . mode === 'dev' ) {
52
+ if ( id . startsWith ( `\0virtual:vite-rsc/validate-imports/invalid/` ) ) {
53
+ const chain = getImportChainDev ( this . environment , id )
54
+ const error = formatError ( chain , this . environment . name )
55
+ if ( error ) {
56
+ this . error ( {
57
+ id : chain [ 1 ] ,
58
+ message : error ,
59
+ } )
60
+ }
61
+ }
62
+ }
63
+ } ,
64
+ } ,
65
+ generateBundle ( ) {
66
+ if ( this . environment . mode === 'build' ) {
67
+ const serverOnly = getImportChainBuild (
68
+ this ,
69
+ '\0virtual:vite-rsc/validate-imports/invalid/server-only' ,
70
+ )
71
+ const serverOnlyError = formatError ( serverOnly , this . environment . name )
72
+ if ( serverOnlyError ) {
73
+ throw new Error ( serverOnlyError )
74
+ }
75
+ const clientOnly = getImportChainBuild (
76
+ this ,
77
+ '\0virtual:vite-rsc/validate-imports/invalid/client-only' ,
78
+ )
79
+ const clientOnlyError = formatError ( clientOnly , this . environment . name )
80
+ if ( clientOnlyError ) {
81
+ throw new Error ( clientOnlyError )
82
+ }
83
+ }
84
+ } ,
85
+ }
86
+ }
87
+
88
+ function getImportChainDev ( environment : DevEnvironment , id : string ) {
89
+ const chain : string [ ] = [ ]
90
+ const recurse = ( id : string ) => {
91
+ if ( chain . includes ( id ) ) return
92
+ const info = environment . moduleGraph . getModuleById ( id )
93
+ if ( ! info ) return
94
+ chain . push ( id )
95
+ const next = [ ...info . importers ] [ 0 ]
96
+ if ( next && next . id ) {
97
+ recurse ( next . id )
98
+ }
99
+ }
100
+ recurse ( id )
101
+ return chain
102
+ }
103
+
104
+ function getImportChainBuild ( ctx : Rollup . PluginContext , id : string ) : string [ ] {
105
+ const chain : string [ ] = [ ]
106
+ const recurse = ( id : string ) => {
107
+ if ( chain . includes ( id ) ) return
108
+ const info = ctx . getModuleInfo ( id )
109
+ if ( ! info ) return
110
+ chain . push ( id )
111
+ const next = info . importers [ 0 ]
112
+ if ( next ) {
113
+ recurse ( next )
114
+ }
115
+ }
116
+ recurse ( id )
117
+ return chain
118
+ }
119
+
120
+ function formatError (
121
+ chain : string [ ] ,
122
+ environmentName : string ,
123
+ ) : string | undefined {
124
+ if ( chain . length === 0 ) return
125
+ const id = chain [ 0 ] !
126
+ const source = id . slice ( id . lastIndexOf ( '/' ) + 1 )
127
+ let result = `'${ source } ' cannot be imported in '${ environmentName } ' environment:\n`
128
+ result += chain
129
+ . slice ( 1 , 6 )
130
+ . map (
131
+ ( id , i ) => ' ' . repeat ( i + 1 ) + `imported by ${ id . replaceAll ( '\0' , '' ) } \n` ,
132
+ )
133
+ . join ( '' )
134
+ if ( chain . length > 6 ) {
135
+ result += ' ' . repeat ( 7 ) + '...\n'
43
136
}
137
+ return result
44
138
}
0 commit comments