1
1
import { store , MAIN_FILE , SANDBOX_VUE_URL , File } from '../store'
2
- import { babelParse , MagicString , walk } from '@vue/compiler-sfc'
2
+ import {
3
+ babelParse ,
4
+ MagicString ,
5
+ walk ,
6
+ walkIdentifiers
7
+ } from '@vue/compiler-sfc'
3
8
import { babelParserDefaultPlugins } from '@vue/shared'
4
- import { Identifier , Node } from '@babel/types'
9
+ import { ExportSpecifier , Identifier , Node , ObjectProperty } from '@babel/types'
5
10
6
11
export function compileModulesForPreview ( ) {
7
12
return processFile ( store . files [ MAIN_FILE ] ) . reverse ( )
8
13
}
9
14
15
+ const modulesKey = `__modules__`
16
+ const exportKey = `__export__`
17
+ const dynamicImportKey = `__dynamic_import__`
18
+ const moduleKey = `__module__`
19
+
20
+ // similar logic with Vite's SSR transform, except this is targeting the browser
10
21
function processFile ( file : File , seen = new Set < File > ( ) ) {
11
22
if ( seen . has ( file ) ) {
12
23
return [ ]
13
24
}
14
25
seen . add ( file )
15
26
16
27
const { js, css } = file . compiled
28
+
29
+ const s = new MagicString ( js )
30
+
17
31
const ast = babelParse ( js , {
18
32
sourceFilename : file . filename ,
19
33
sourceType : 'module' ,
20
34
plugins : [ ...babelParserDefaultPlugins ]
21
35
} ) . program . body
22
36
37
+ const idToImportMap = new Map < string , string > ( )
38
+ const declaredConst = new Set < string > ( )
23
39
const importedFiles = new Set < string > ( )
24
40
const importToIdMap = new Map < string , string > ( )
25
41
26
- const s = new MagicString ( js )
27
-
28
- function registerImport ( source : string ) {
42
+ function defineImport ( node : Node , source : string ) {
29
43
const filename = source . replace ( / ^ \. \/ + / , '' )
30
44
if ( ! ( filename in store . files ) ) {
31
45
throw new Error ( `File "${ filename } " does not exist.` )
32
46
}
33
47
if ( importedFiles . has ( filename ) ) {
34
- return importToIdMap . get ( filename )
48
+ return importToIdMap . get ( filename ) !
35
49
}
36
50
importedFiles . add ( filename )
37
51
const id = `__import_${ importedFiles . size } __`
38
52
importToIdMap . set ( filename , id )
39
- s . prepend ( `const ${ id } = __modules__[${ JSON . stringify ( filename ) } ]\n` )
53
+ s . appendLeft (
54
+ node . start ! ,
55
+ `const ${ id } = ${ modulesKey } [${ JSON . stringify ( filename ) } ]\n`
56
+ )
40
57
return id
41
58
}
42
59
60
+ function defineExport ( name : string , local = name ) {
61
+ s . append ( `\n${ exportKey } (${ moduleKey } , "${ name } ", () => ${ local } )` )
62
+ }
63
+
64
+ // 0. instantiate module
43
65
s . prepend (
44
- `const mod = __modules__[${ JSON . stringify (
66
+ `const ${ moduleKey } = __modules__[${ JSON . stringify (
45
67
file . filename
46
- ) } ] = Object.create(null) \n\n`
68
+ ) } ] = { [Symbol.toStringTag]: "Module" } \n\n`
47
69
)
48
70
71
+ // 1. check all import statements and record id -> importName map
49
72
for ( const node of ast ) {
73
+ // import foo from 'foo' --> foo -> __import_foo__.default
74
+ // import { baz } from 'foo' --> baz -> __import_foo__.baz
75
+ // import * as ok from 'foo' --> ok -> __import_foo__
50
76
if ( node . type === 'ImportDeclaration' ) {
51
77
const source = node . source . value
52
- if ( source === 'vue' ) {
53
- // rewrite Vue imports
54
- s . overwrite (
55
- node . source . start ! ,
56
- node . source . end ! ,
57
- `"${ SANDBOX_VUE_URL } "`
58
- )
59
- } else if ( source . startsWith ( './' ) ) {
60
- // rewrite the import to retrieve the import from global registry
61
- s . remove ( node . start ! , node . end ! )
62
-
63
- const id = registerImport ( source )
64
-
78
+ if ( source . startsWith ( './' ) ) {
79
+ const importId = defineImport ( node , node . source . value )
65
80
for ( const spec of node . specifiers ) {
66
- if ( spec . type === 'ImportDefaultSpecifier' ) {
67
- s . prependRight (
68
- node . start ! ,
69
- `const ${ spec . local . name } = ${ id } .default\n`
70
- )
71
- } else if ( spec . type === 'ImportSpecifier' ) {
72
- s . prependRight (
73
- node . start ! ,
74
- `const ${ spec . local . name } = ${ id } .${
75
- ( spec . imported as Identifier ) . name
76
- } \n`
81
+ if ( spec . type === 'ImportSpecifier' ) {
82
+ idToImportMap . set (
83
+ spec . local . name ,
84
+ `${ importId } .${ ( spec . imported as Identifier ) . name } `
77
85
)
86
+ } else if ( spec . type === 'ImportDefaultSpecifier' ) {
87
+ idToImportMap . set ( spec . local . name , `${ importId } .default` )
78
88
} else {
79
- // namespace import
80
- s . prependRight ( node . start ! , `const ${ spec . local . name } = ${ id } ` )
89
+ // namespace specifier
90
+ idToImportMap . set ( spec . local . name , importId )
81
91
}
82
92
}
93
+ s . remove ( node . start ! , node . end ! )
94
+ } else {
95
+ if ( source === 'vue' ) {
96
+ // rewrite Vue imports
97
+ s . overwrite (
98
+ node . source . start ! ,
99
+ node . source . end ! ,
100
+ `"${ SANDBOX_VUE_URL } "`
101
+ )
102
+ }
83
103
}
84
104
}
105
+ }
85
106
86
- if ( node . type === 'ExportDefaultDeclaration' ) {
87
- // export default -> mod.default = ...
88
- s . overwrite ( node . start ! , node . declaration . start ! , 'mod.default = ' )
89
- }
90
-
107
+ // 2. check all export statements and define exports
108
+ for ( const node of ast ) {
109
+ // named exports
91
110
if ( node . type === 'ExportNamedDeclaration' ) {
92
- if ( node . source ) {
93
- // export { foo } from '...' -> mode.foo = __import_x__.foo
94
- const id = registerImport ( node . source . value )
95
- let code = ``
96
- for ( const spec of node . specifiers ) {
97
- if ( spec . type === 'ExportSpecifier' ) {
98
- code += `mod.${ ( spec . exported as Identifier ) . name } = ${ id } .${
99
- spec . local . name
100
- } \n`
101
- }
102
- }
103
- s . overwrite ( node . start ! , node . end ! , code )
104
- } else if ( node . declaration ) {
111
+ if ( node . declaration ) {
105
112
if (
106
113
node . declaration . type === 'FunctionDeclaration' ||
107
114
node . declaration . type === 'ClassDeclaration'
108
115
) {
109
116
// export function foo() {}
110
- const name = node . declaration . id ! . name
111
- s . appendLeft ( node . end ! , `\nmod.${ name } = ${ name } \n` )
117
+ defineExport ( node . declaration . id ! . name )
112
118
} else if ( node . declaration . type === 'VariableDeclaration' ) {
113
119
// export const foo = 1, bar = 2
114
120
for ( const decl of node . declaration . declarations ) {
115
- for ( const { name } of extractIdentifiers ( decl . id ) ) {
116
- s . appendLeft ( node . end ! , `\nmod.${ name } = ${ name } ` )
121
+ const names = extractNames ( decl . id as any )
122
+ for ( const name of names ) {
123
+ defineExport ( name )
117
124
}
118
125
}
119
126
}
120
127
s . remove ( node . start ! , node . declaration . start ! )
128
+ } else if ( node . source ) {
129
+ // export { foo, bar } from './foo'
130
+ const importId = defineImport ( node , node . source . value )
131
+ for ( const spec of node . specifiers ) {
132
+ defineExport (
133
+ ( spec . exported as Identifier ) . name ,
134
+ `${ importId } .${ ( spec as ExportSpecifier ) . local . name } `
135
+ )
136
+ }
137
+ s . remove ( node . start ! , node . end ! )
121
138
} else {
122
- let code = ``
139
+ // export { foo, bar }
123
140
for ( const spec of node . specifiers ) {
124
- if ( spec . type === 'ExportSpecifier' ) {
125
- code += `mod.${ ( spec . exported as Identifier ) . name } = ${
126
- spec . local . name
127
- } \n`
128
- }
141
+ const local = ( spec as ExportSpecifier ) . local . name
142
+ const binding = idToImportMap . get ( local )
143
+ defineExport ( ( spec . exported as Identifier ) . name , binding || local )
129
144
}
130
- s . overwrite ( node . start ! , node . end ! , code )
145
+ s . remove ( node . start ! , node . end ! )
131
146
}
132
147
}
133
148
149
+ // default export
150
+ if ( node . type === 'ExportDefaultDeclaration' ) {
151
+ s . overwrite ( node . start ! , node . start ! + 14 , `${ moduleKey } .default =` )
152
+ }
153
+
154
+ // export * from './foo'
134
155
if ( node . type === 'ExportAllDeclaration' ) {
135
- const id = registerImport ( node . source . value )
136
- s . overwrite ( node . start ! , node . end ! , `Object.assign(mod, ${ id } )` )
156
+ const importId = defineImport ( node , node . source . value )
157
+ s . remove ( node . start ! , node . end ! )
158
+ s . append ( `\nfor (const key in ${ importId } ) {
159
+ if (key !== 'default') {
160
+ ${ exportKey } (${ moduleKey } , key, () => ${ importId } [key])
161
+ }
162
+ }` )
137
163
}
138
164
}
139
165
140
- // dynamic import
141
- walk ( ast as any , {
142
- enter ( node ) {
143
- if ( node . type === 'ImportExpression' ) {
166
+ // 3. convert references to import bindings
167
+ for ( const node of ast ) {
168
+ if ( node . type === 'ImportDeclaration' ) continue
169
+ walkIdentifiers ( node , ( id , parent , parentStack ) => {
170
+ const binding = idToImportMap . get ( id . name )
171
+ if ( ! binding ) {
172
+ return
173
+ }
174
+ if ( isStaticProperty ( parent ) && parent . shorthand ) {
175
+ // let binding used in a property shorthand
176
+ // { foo } -> { foo: __import_x__.foo }
177
+ // skip for destructure patterns
178
+ if (
179
+ ! ( parent as any ) . inPattern ||
180
+ isInDestructureAssignment ( parent , parentStack )
181
+ ) {
182
+ s . appendLeft ( id . end ! , `: ${ binding } ` )
183
+ }
184
+ } else if (
185
+ parent . type === 'ClassDeclaration' &&
186
+ id === parent . superClass
187
+ ) {
188
+ if ( ! declaredConst . has ( id . name ) ) {
189
+ declaredConst . add ( id . name )
190
+ // locate the top-most node containing the class declaration
191
+ const topNode = parentStack [ 1 ]
192
+ s . prependRight ( topNode . start ! , `const ${ id . name } = ${ binding } ;\n` )
193
+ }
194
+ } else {
195
+ s . overwrite ( id . start ! , id . end ! , binding )
196
+ }
197
+ } )
198
+ }
199
+
200
+ // 4. convert dynamic imports
201
+ ; ( walk as any ) ( ast , {
202
+ enter ( node : Node , parent : Node ) {
203
+ if ( node . type === 'Import' && parent . type === 'CallExpression' ) {
204
+ const arg = parent . arguments [ 0 ]
205
+ if ( arg . type === 'StringLiteral' && arg . value . startsWith ( './' ) ) {
206
+ s . overwrite ( node . start ! , node . start ! + 6 , dynamicImportKey )
207
+ s . overwrite (
208
+ arg . start ! ,
209
+ arg . end ! ,
210
+ JSON . stringify ( arg . value . replace ( / ^ \. \/ + / , '' ) )
211
+ )
212
+ }
144
213
}
145
214
}
146
215
} )
@@ -161,6 +230,13 @@ function processFile(file: File, seen = new Set<File>()) {
161
230
return processed
162
231
}
163
232
233
+ const isStaticProperty = ( node : Node ) : node is ObjectProperty =>
234
+ node . type === 'ObjectProperty' && ! node . computed
235
+
236
+ function extractNames ( param : Node ) : string [ ] {
237
+ return extractIdentifiers ( param ) . map ( id => id . name )
238
+ }
239
+
164
240
function extractIdentifiers (
165
241
param : Node ,
166
242
nodes : Identifier [ ] = [ ]
@@ -205,3 +281,21 @@ function extractIdentifiers(
205
281
206
282
return nodes
207
283
}
284
+
285
+ function isInDestructureAssignment ( parent : Node , parentStack : Node [ ] ) : boolean {
286
+ if (
287
+ parent &&
288
+ ( parent . type === 'ObjectProperty' || parent . type === 'ArrayPattern' )
289
+ ) {
290
+ let i = parentStack . length
291
+ while ( i -- ) {
292
+ const p = parentStack [ i ]
293
+ if ( p . type === 'AssignmentExpression' ) {
294
+ return true
295
+ } else if ( p . type !== 'ObjectProperty' && ! p . type . endsWith ( 'Pattern' ) ) {
296
+ break
297
+ }
298
+ }
299
+ }
300
+ return false
301
+ }
0 commit comments