|
|
@@ -1,12 +1,23 @@
|
|
|
import { store, MAIN_FILE, SANDBOX_VUE_URL, File } from '../store'
|
|
|
-import { babelParse, MagicString, walk } from '@vue/compiler-sfc'
|
|
|
+import {
|
|
|
+ babelParse,
|
|
|
+ MagicString,
|
|
|
+ walk,
|
|
|
+ walkIdentifiers
|
|
|
+} from '@vue/compiler-sfc'
|
|
|
import { babelParserDefaultPlugins } from '@vue/shared'
|
|
|
-import { Identifier, Node } from '@babel/types'
|
|
|
+import { ExportSpecifier, Identifier, Node, ObjectProperty } from '@babel/types'
|
|
|
|
|
|
export function compileModulesForPreview() {
|
|
|
return processFile(store.files[MAIN_FILE]).reverse()
|
|
|
}
|
|
|
|
|
|
+const modulesKey = `__modules__`
|
|
|
+const exportKey = `__export__`
|
|
|
+const dynamicImportKey = `__dynamic_import__`
|
|
|
+const moduleKey = `__module__`
|
|
|
+
|
|
|
+// similar logic with Vite's SSR transform, except this is targeting the browser
|
|
|
function processFile(file: File, seen = new Set<File>()) {
|
|
|
if (seen.has(file)) {
|
|
|
return []
|
|
|
@@ -14,133 +25,191 @@ function processFile(file: File, seen = new Set<File>()) {
|
|
|
seen.add(file)
|
|
|
|
|
|
const { js, css } = file.compiled
|
|
|
+
|
|
|
+ const s = new MagicString(js)
|
|
|
+
|
|
|
const ast = babelParse(js, {
|
|
|
sourceFilename: file.filename,
|
|
|
sourceType: 'module',
|
|
|
plugins: [...babelParserDefaultPlugins]
|
|
|
}).program.body
|
|
|
|
|
|
+ const idToImportMap = new Map<string, string>()
|
|
|
+ const declaredConst = new Set<string>()
|
|
|
const importedFiles = new Set<string>()
|
|
|
const importToIdMap = new Map<string, string>()
|
|
|
|
|
|
- const s = new MagicString(js)
|
|
|
-
|
|
|
- function registerImport(source: string) {
|
|
|
+ function defineImport(node: Node, source: string) {
|
|
|
const filename = source.replace(/^\.\/+/, '')
|
|
|
if (!(filename in store.files)) {
|
|
|
throw new Error(`File "${filename}" does not exist.`)
|
|
|
}
|
|
|
if (importedFiles.has(filename)) {
|
|
|
- return importToIdMap.get(filename)
|
|
|
+ return importToIdMap.get(filename)!
|
|
|
}
|
|
|
importedFiles.add(filename)
|
|
|
const id = `__import_${importedFiles.size}__`
|
|
|
importToIdMap.set(filename, id)
|
|
|
- s.prepend(`const ${id} = __modules__[${JSON.stringify(filename)}]\n`)
|
|
|
+ s.appendLeft(
|
|
|
+ node.start!,
|
|
|
+ `const ${id} = ${modulesKey}[${JSON.stringify(filename)}]\n`
|
|
|
+ )
|
|
|
return id
|
|
|
}
|
|
|
|
|
|
+ function defineExport(name: string, local = name) {
|
|
|
+ s.append(`\n${exportKey}(${moduleKey}, "${name}", () => ${local})`)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 0. instantiate module
|
|
|
s.prepend(
|
|
|
- `const mod = __modules__[${JSON.stringify(
|
|
|
+ `const ${moduleKey} = __modules__[${JSON.stringify(
|
|
|
file.filename
|
|
|
- )}] = Object.create(null)\n\n`
|
|
|
+ )}] = { [Symbol.toStringTag]: "Module" }\n\n`
|
|
|
)
|
|
|
|
|
|
+ // 1. check all import statements and record id -> importName map
|
|
|
for (const node of ast) {
|
|
|
+ // import foo from 'foo' --> foo -> __import_foo__.default
|
|
|
+ // import { baz } from 'foo' --> baz -> __import_foo__.baz
|
|
|
+ // import * as ok from 'foo' --> ok -> __import_foo__
|
|
|
if (node.type === 'ImportDeclaration') {
|
|
|
const source = node.source.value
|
|
|
- if (source === 'vue') {
|
|
|
- // rewrite Vue imports
|
|
|
- s.overwrite(
|
|
|
- node.source.start!,
|
|
|
- node.source.end!,
|
|
|
- `"${SANDBOX_VUE_URL}"`
|
|
|
- )
|
|
|
- } else if (source.startsWith('./')) {
|
|
|
- // rewrite the import to retrieve the import from global registry
|
|
|
- s.remove(node.start!, node.end!)
|
|
|
-
|
|
|
- const id = registerImport(source)
|
|
|
-
|
|
|
+ if (source.startsWith('./')) {
|
|
|
+ const importId = defineImport(node, node.source.value)
|
|
|
for (const spec of node.specifiers) {
|
|
|
- if (spec.type === 'ImportDefaultSpecifier') {
|
|
|
- s.prependRight(
|
|
|
- node.start!,
|
|
|
- `const ${spec.local.name} = ${id}.default\n`
|
|
|
- )
|
|
|
- } else if (spec.type === 'ImportSpecifier') {
|
|
|
- s.prependRight(
|
|
|
- node.start!,
|
|
|
- `const ${spec.local.name} = ${id}.${
|
|
|
- (spec.imported as Identifier).name
|
|
|
- }\n`
|
|
|
+ if (spec.type === 'ImportSpecifier') {
|
|
|
+ idToImportMap.set(
|
|
|
+ spec.local.name,
|
|
|
+ `${importId}.${(spec.imported as Identifier).name}`
|
|
|
)
|
|
|
+ } else if (spec.type === 'ImportDefaultSpecifier') {
|
|
|
+ idToImportMap.set(spec.local.name, `${importId}.default`)
|
|
|
} else {
|
|
|
- // namespace import
|
|
|
- s.prependRight(node.start!, `const ${spec.local.name} = ${id}`)
|
|
|
+ // namespace specifier
|
|
|
+ idToImportMap.set(spec.local.name, importId)
|
|
|
}
|
|
|
}
|
|
|
+ s.remove(node.start!, node.end!)
|
|
|
+ } else {
|
|
|
+ if (source === 'vue') {
|
|
|
+ // rewrite Vue imports
|
|
|
+ s.overwrite(
|
|
|
+ node.source.start!,
|
|
|
+ node.source.end!,
|
|
|
+ `"${SANDBOX_VUE_URL}"`
|
|
|
+ )
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- if (node.type === 'ExportDefaultDeclaration') {
|
|
|
- // export default -> mod.default = ...
|
|
|
- s.overwrite(node.start!, node.declaration.start!, 'mod.default = ')
|
|
|
- }
|
|
|
-
|
|
|
+ // 2. check all export statements and define exports
|
|
|
+ for (const node of ast) {
|
|
|
+ // named exports
|
|
|
if (node.type === 'ExportNamedDeclaration') {
|
|
|
- if (node.source) {
|
|
|
- // export { foo } from '...' -> mode.foo = __import_x__.foo
|
|
|
- const id = registerImport(node.source.value)
|
|
|
- let code = ``
|
|
|
- for (const spec of node.specifiers) {
|
|
|
- if (spec.type === 'ExportSpecifier') {
|
|
|
- code += `mod.${(spec.exported as Identifier).name} = ${id}.${
|
|
|
- spec.local.name
|
|
|
- }\n`
|
|
|
- }
|
|
|
- }
|
|
|
- s.overwrite(node.start!, node.end!, code)
|
|
|
- } else if (node.declaration) {
|
|
|
+ if (node.declaration) {
|
|
|
if (
|
|
|
node.declaration.type === 'FunctionDeclaration' ||
|
|
|
node.declaration.type === 'ClassDeclaration'
|
|
|
) {
|
|
|
// export function foo() {}
|
|
|
- const name = node.declaration.id!.name
|
|
|
- s.appendLeft(node.end!, `\nmod.${name} = ${name}\n`)
|
|
|
+ defineExport(node.declaration.id!.name)
|
|
|
} else if (node.declaration.type === 'VariableDeclaration') {
|
|
|
// export const foo = 1, bar = 2
|
|
|
for (const decl of node.declaration.declarations) {
|
|
|
- for (const { name } of extractIdentifiers(decl.id)) {
|
|
|
- s.appendLeft(node.end!, `\nmod.${name} = ${name}`)
|
|
|
+ const names = extractNames(decl.id as any)
|
|
|
+ for (const name of names) {
|
|
|
+ defineExport(name)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
s.remove(node.start!, node.declaration.start!)
|
|
|
+ } else if (node.source) {
|
|
|
+ // export { foo, bar } from './foo'
|
|
|
+ const importId = defineImport(node, node.source.value)
|
|
|
+ for (const spec of node.specifiers) {
|
|
|
+ defineExport(
|
|
|
+ (spec.exported as Identifier).name,
|
|
|
+ `${importId}.${(spec as ExportSpecifier).local.name}`
|
|
|
+ )
|
|
|
+ }
|
|
|
+ s.remove(node.start!, node.end!)
|
|
|
} else {
|
|
|
- let code = ``
|
|
|
+ // export { foo, bar }
|
|
|
for (const spec of node.specifiers) {
|
|
|
- if (spec.type === 'ExportSpecifier') {
|
|
|
- code += `mod.${(spec.exported as Identifier).name} = ${
|
|
|
- spec.local.name
|
|
|
- }\n`
|
|
|
- }
|
|
|
+ const local = (spec as ExportSpecifier).local.name
|
|
|
+ const binding = idToImportMap.get(local)
|
|
|
+ defineExport((spec.exported as Identifier).name, binding || local)
|
|
|
}
|
|
|
- s.overwrite(node.start!, node.end!, code)
|
|
|
+ s.remove(node.start!, node.end!)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // default export
|
|
|
+ if (node.type === 'ExportDefaultDeclaration') {
|
|
|
+ s.overwrite(node.start!, node.start! + 14, `${moduleKey}.default =`)
|
|
|
+ }
|
|
|
+
|
|
|
+ // export * from './foo'
|
|
|
if (node.type === 'ExportAllDeclaration') {
|
|
|
- const id = registerImport(node.source.value)
|
|
|
- s.overwrite(node.start!, node.end!, `Object.assign(mod, ${id})`)
|
|
|
+ const importId = defineImport(node, node.source.value)
|
|
|
+ s.remove(node.start!, node.end!)
|
|
|
+ s.append(`\nfor (const key in ${importId}) {
|
|
|
+ if (key !== 'default') {
|
|
|
+ ${exportKey}(${moduleKey}, key, () => ${importId}[key])
|
|
|
+ }
|
|
|
+ }`)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // dynamic import
|
|
|
- walk(ast as any, {
|
|
|
- enter(node) {
|
|
|
- if (node.type === 'ImportExpression') {
|
|
|
+ // 3. convert references to import bindings
|
|
|
+ for (const node of ast) {
|
|
|
+ if (node.type === 'ImportDeclaration') continue
|
|
|
+ walkIdentifiers(node, (id, parent, parentStack) => {
|
|
|
+ const binding = idToImportMap.get(id.name)
|
|
|
+ if (!binding) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (isStaticProperty(parent) && parent.shorthand) {
|
|
|
+ // let binding used in a property shorthand
|
|
|
+ // { foo } -> { foo: __import_x__.foo }
|
|
|
+ // skip for destructure patterns
|
|
|
+ if (
|
|
|
+ !(parent as any).inPattern ||
|
|
|
+ isInDestructureAssignment(parent, parentStack)
|
|
|
+ ) {
|
|
|
+ s.appendLeft(id.end!, `: ${binding}`)
|
|
|
+ }
|
|
|
+ } else if (
|
|
|
+ parent.type === 'ClassDeclaration' &&
|
|
|
+ id === parent.superClass
|
|
|
+ ) {
|
|
|
+ if (!declaredConst.has(id.name)) {
|
|
|
+ declaredConst.add(id.name)
|
|
|
+ // locate the top-most node containing the class declaration
|
|
|
+ const topNode = parentStack[1]
|
|
|
+ s.prependRight(topNode.start!, `const ${id.name} = ${binding};\n`)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ s.overwrite(id.start!, id.end!, binding)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. convert dynamic imports
|
|
|
+ ;(walk as any)(ast, {
|
|
|
+ enter(node: Node, parent: Node) {
|
|
|
+ if (node.type === 'Import' && parent.type === 'CallExpression') {
|
|
|
+ const arg = parent.arguments[0]
|
|
|
+ if (arg.type === 'StringLiteral' && arg.value.startsWith('./')) {
|
|
|
+ s.overwrite(node.start!, node.start! + 6, dynamicImportKey)
|
|
|
+ s.overwrite(
|
|
|
+ arg.start!,
|
|
|
+ arg.end!,
|
|
|
+ JSON.stringify(arg.value.replace(/^\.\/+/, ''))
|
|
|
+ )
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
})
|
|
|
@@ -161,6 +230,13 @@ function processFile(file: File, seen = new Set<File>()) {
|
|
|
return processed
|
|
|
}
|
|
|
|
|
|
+const isStaticProperty = (node: Node): node is ObjectProperty =>
|
|
|
+ node.type === 'ObjectProperty' && !node.computed
|
|
|
+
|
|
|
+function extractNames(param: Node): string[] {
|
|
|
+ return extractIdentifiers(param).map(id => id.name)
|
|
|
+}
|
|
|
+
|
|
|
function extractIdentifiers(
|
|
|
param: Node,
|
|
|
nodes: Identifier[] = []
|
|
|
@@ -205,3 +281,21 @@ function extractIdentifiers(
|
|
|
|
|
|
return nodes
|
|
|
}
|
|
|
+
|
|
|
+function isInDestructureAssignment(parent: Node, parentStack: Node[]): boolean {
|
|
|
+ if (
|
|
|
+ parent &&
|
|
|
+ (parent.type === 'ObjectProperty' || parent.type === 'ArrayPattern')
|
|
|
+ ) {
|
|
|
+ let i = parentStack.length
|
|
|
+ while (i--) {
|
|
|
+ const p = parentStack[i]
|
|
|
+ if (p.type === 'AssignmentExpression') {
|
|
|
+ return true
|
|
|
+ } else if (p.type !== 'ObjectProperty' && !p.type.endsWith('Pattern')) {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false
|
|
|
+}
|