Skip to content

Commit aa8bf1b

Browse files
committed
workflow(sfc-playground): improve module rewrite
1 parent 3ac661b commit aa8bf1b

File tree

2 files changed

+178
-70
lines changed

2 files changed

+178
-70
lines changed
Lines changed: 164 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,146 +1,215 @@
11
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'
38
import { babelParserDefaultPlugins } from '@vue/shared'
4-
import { Identifier, Node } from '@babel/types'
9+
import { ExportSpecifier, Identifier, Node, ObjectProperty } from '@babel/types'
510

611
export function compileModulesForPreview() {
712
return processFile(store.files[MAIN_FILE]).reverse()
813
}
914

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
1021
function processFile(file: File, seen = new Set<File>()) {
1122
if (seen.has(file)) {
1223
return []
1324
}
1425
seen.add(file)
1526

1627
const { js, css } = file.compiled
28+
29+
const s = new MagicString(js)
30+
1731
const ast = babelParse(js, {
1832
sourceFilename: file.filename,
1933
sourceType: 'module',
2034
plugins: [...babelParserDefaultPlugins]
2135
}).program.body
2236

37+
const idToImportMap = new Map<string, string>()
38+
const declaredConst = new Set<string>()
2339
const importedFiles = new Set<string>()
2440
const importToIdMap = new Map<string, string>()
2541

26-
const s = new MagicString(js)
27-
28-
function registerImport(source: string) {
42+
function defineImport(node: Node, source: string) {
2943
const filename = source.replace(/^\.\/+/, '')
3044
if (!(filename in store.files)) {
3145
throw new Error(`File "${filename}" does not exist.`)
3246
}
3347
if (importedFiles.has(filename)) {
34-
return importToIdMap.get(filename)
48+
return importToIdMap.get(filename)!
3549
}
3650
importedFiles.add(filename)
3751
const id = `__import_${importedFiles.size}__`
3852
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+
)
4057
return id
4158
}
4259

60+
function defineExport(name: string, local = name) {
61+
s.append(`\n${exportKey}(${moduleKey}, "${name}", () => ${local})`)
62+
}
63+
64+
// 0. instantiate module
4365
s.prepend(
44-
`const mod = __modules__[${JSON.stringify(
66+
`const ${moduleKey} = __modules__[${JSON.stringify(
4567
file.filename
46-
)}] = Object.create(null)\n\n`
68+
)}] = { [Symbol.toStringTag]: "Module" }\n\n`
4769
)
4870

71+
// 1. check all import statements and record id -> importName map
4972
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__
5076
if (node.type === 'ImportDeclaration') {
5177
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)
6580
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}`
7785
)
86+
} else if (spec.type === 'ImportDefaultSpecifier') {
87+
idToImportMap.set(spec.local.name, `${importId}.default`)
7888
} else {
79-
// namespace import
80-
s.prependRight(node.start!, `const ${spec.local.name} = ${id}`)
89+
// namespace specifier
90+
idToImportMap.set(spec.local.name, importId)
8191
}
8292
}
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+
}
83103
}
84104
}
105+
}
85106

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
91110
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) {
105112
if (
106113
node.declaration.type === 'FunctionDeclaration' ||
107114
node.declaration.type === 'ClassDeclaration'
108115
) {
109116
// 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)
112118
} else if (node.declaration.type === 'VariableDeclaration') {
113119
// export const foo = 1, bar = 2
114120
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)
117124
}
118125
}
119126
}
120127
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!)
121138
} else {
122-
let code = ``
139+
// export { foo, bar }
123140
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)
129144
}
130-
s.overwrite(node.start!, node.end!, code)
145+
s.remove(node.start!, node.end!)
131146
}
132147
}
133148

149+
// default export
150+
if (node.type === 'ExportDefaultDeclaration') {
151+
s.overwrite(node.start!, node.start! + 14, `${moduleKey}.default =`)
152+
}
153+
154+
// export * from './foo'
134155
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+
}`)
137163
}
138164
}
139165

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+
}
144213
}
145214
}
146215
})
@@ -161,6 +230,13 @@ function processFile(file: File, seen = new Set<File>()) {
161230
return processed
162231
}
163232

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+
164240
function extractIdentifiers(
165241
param: Node,
166242
nodes: Identifier[] = []
@@ -205,3 +281,21 @@ function extractIdentifiers(
205281

206282
return nodes
207283
}
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+
}

packages/sfc-playground/src/output/srcdoc.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,20 @@
1111
<script type="module">
1212
let scriptEl
1313

14+
window.__modules__ = {}
15+
16+
window.__export__ = (mod, key, get) => {
17+
Object.defineProperty(mod, key, {
18+
enumerable: true,
19+
configurable: true,
20+
get
21+
})
22+
}
23+
24+
window.__dynamic_import__ = key => {
25+
return Promise.resolve(window.__modules__[key])
26+
}
27+
1428
function handle_message(ev) {
1529
let { action, cmd_id } = ev.data;
1630
const send_message = (payload) => parent.postMessage( { ...payload }, ev.origin);

0 commit comments

Comments
 (0)