workflow(sfc-playground): improve module rewrite
This commit is contained in:
parent
3ac661b896
commit
aa8bf1b7a3
@ -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 {
|
||||
let code = ``
|
||||
} else if (node.source) {
|
||||
// export { foo, bar } from './foo'
|
||||
const importId = defineImport(node, node.source.value)
|
||||
for (const spec of node.specifiers) {
|
||||
if (spec.type === 'ExportSpecifier') {
|
||||
code += `mod.${(spec.exported as Identifier).name} = ${
|
||||
spec.local.name
|
||||
}\n`
|
||||
}
|
||||
defineExport(
|
||||
(spec.exported as Identifier).name,
|
||||
`${importId}.${(spec as ExportSpecifier).local.name}`
|
||||
)
|
||||
}
|
||||
s.overwrite(node.start!, node.end!, code)
|
||||
s.remove(node.start!, node.end!)
|
||||
} else {
|
||||
// export { foo, bar }
|
||||
for (const spec of node.specifiers) {
|
||||
const local = (spec as ExportSpecifier).local.name
|
||||
const binding = idToImportMap.get(local)
|
||||
defineExport((spec.exported as Identifier).name, binding || local)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -11,6 +11,20 @@
|
||||
<script type="module">
|
||||
let scriptEl
|
||||
|
||||
window.__modules__ = {}
|
||||
|
||||
window.__export__ = (mod, key, get) => {
|
||||
Object.defineProperty(mod, key, {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get
|
||||
})
|
||||
}
|
||||
|
||||
window.__dynamic_import__ = key => {
|
||||
return Promise.resolve(window.__modules__[key])
|
||||
}
|
||||
|
||||
function handle_message(ev) {
|
||||
let { action, cmd_id } = ev.data;
|
||||
const send_message = (payload) => parent.postMessage( { ...payload }, ev.origin);
|
||||
|
Loading…
Reference in New Issue
Block a user