]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
workflow(sfc-playground): improve module rewrite
authorEvan You <yyx990803@gmail.com>
Mon, 29 Mar 2021 01:55:22 +0000 (21:55 -0400)
committerEvan You <yyx990803@gmail.com>
Mon, 29 Mar 2021 01:55:22 +0000 (21:55 -0400)
packages/sfc-playground/src/output/moduleCompiler.ts
packages/sfc-playground/src/output/srcdoc.html

index 82ffd2239570f71e31053235aa2d13e90f08e85e..31f6bc067997f360a243e9fe9ef1065db9cc242b 100644 (file)
@@ -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
+}
index 31e033542f2a027960d013c8c293afc086ab6629..063a522d456ab5c0d6d557607ff5edd6788a9366 100644 (file)
                <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);