Browse Source

feat: further align compiler-sfc api + expose rewriteDefault

Evan You 4 years ago
parent
commit
8ce585d70c

+ 2 - 1
packages/compiler-sfc/src/index.ts

@@ -4,9 +4,10 @@ export { compileTemplate } from './compileTemplate'
 export { compileStyle, compileStyleAsync } from './compileStyle'
 export { compileScript } from './compileScript'
 export { generateCodeFrame } from 'compiler/codeframe'
+export { rewriteDefault } from './rewriteDefault'
 
 // types
-export { CompilerOptions } from 'types/compiler'
+export { CompilerOptions, WarningMessage } from 'types/compiler'
 export { TemplateCompiler } from './types'
 export {
   SFCBlock,

+ 3 - 3
packages/compiler-sfc/src/parse.ts

@@ -22,7 +22,7 @@ export interface ParseOptions {
   compiler?: TemplateCompiler
   compilerParseOptions?: VueTemplateCompilerParseOptions
   sourceRoot?: string
-  needMap?: boolean
+  sourceMap?: boolean
 }
 
 export function parse(options: ParseOptions): SFCDescriptor {
@@ -32,7 +32,7 @@ export function parse(options: ParseOptions): SFCDescriptor {
     compiler,
     compilerParseOptions = { pad: false } as VueTemplateCompilerParseOptions,
     sourceRoot = '',
-    needMap = true
+    sourceMap = true
   } = options
   const cacheKey = hash(
     filename + source + JSON.stringify(compilerParseOptions)
@@ -55,7 +55,7 @@ export function parse(options: ParseOptions): SFCDescriptor {
   output.shouldForceReload = prevImports =>
     hmrShouldReload(prevImports, output!)
 
-  if (needMap) {
+  if (sourceMap) {
     if (output.script && !output.script.src) {
       output.script.map = generateSourceMap(
         filename,

+ 1 - 1
packages/compiler-sfc/src/parseComponent.ts

@@ -17,12 +17,12 @@ export interface SFCCustomBlock {
   attrs: { [key: string]: string | true }
   start: number
   end: number
+  src?: string
   map?: RawSourceMap
 }
 
 export interface SFCBlock extends SFCCustomBlock {
   lang?: string
-  src?: string
   scoped?: boolean
   module?: string | boolean
 }

+ 110 - 0
packages/compiler-sfc/src/rewriteDefault.ts

@@ -0,0 +1,110 @@
+import { parse, ParserPlugin } from '@babel/parser'
+import MagicString from 'magic-string'
+
+const defaultExportRE = /((?:^|\n|;)\s*)export(\s*)default/
+const namedDefaultExportRE = /((?:^|\n|;)\s*)export(.+)(?:as)?(\s*)default/s
+const exportDefaultClassRE =
+  /((?:^|\n|;)\s*)export\s+default\s+class\s+([\w$]+)/
+
+/**
+ * Utility for rewriting `export default` in a script block into a variable
+ * declaration so that we can inject things into it
+ */
+export function rewriteDefault(
+  input: string,
+  as: string,
+  parserPlugins?: ParserPlugin[]
+): string {
+  if (!hasDefaultExport(input)) {
+    return input + `\nconst ${as} = {}`
+  }
+
+  let replaced: string | undefined
+
+  const classMatch = input.match(exportDefaultClassRE)
+  if (classMatch) {
+    replaced =
+      input.replace(exportDefaultClassRE, '$1class $2') +
+      `\nconst ${as} = ${classMatch[2]}`
+  } else {
+    replaced = input.replace(defaultExportRE, `$1const ${as} =`)
+  }
+  if (!hasDefaultExport(replaced)) {
+    return replaced
+  }
+
+  // if the script somehow still contains `default export`, it probably has
+  // multi-line comments or template strings. fallback to a full parse.
+  const s = new MagicString(input)
+  const ast = parse(input, {
+    sourceType: 'module',
+    plugins: parserPlugins
+  }).program.body
+  ast.forEach(node => {
+    if (node.type === 'ExportDefaultDeclaration') {
+      s.overwrite(node.start!, node.declaration.start!, `const ${as} = `)
+    }
+    if (node.type === 'ExportNamedDeclaration') {
+      for (const specifier of node.specifiers) {
+        if (
+          specifier.type === 'ExportSpecifier' &&
+          specifier.exported.type === 'Identifier' &&
+          specifier.exported.name === 'default'
+        ) {
+          if (node.source) {
+            if (specifier.local.name === 'default') {
+              const end = specifierEnd(input, specifier.local.end!, node.end)
+              s.prepend(
+                `import { default as __VUE_DEFAULT__ } from '${node.source.value}'\n`
+              )
+              s.overwrite(specifier.start!, end, ``)
+              s.append(`\nconst ${as} = __VUE_DEFAULT__`)
+              continue
+            } else {
+              const end = specifierEnd(input, specifier.exported.end!, node.end)
+              s.prepend(
+                `import { ${input.slice(
+                  specifier.local.start!,
+                  specifier.local.end!
+                )} } from '${node.source.value}'\n`
+              )
+              s.overwrite(specifier.start!, end, ``)
+              s.append(`\nconst ${as} = ${specifier.local.name}`)
+              continue
+            }
+          }
+          const end = specifierEnd(input, specifier.end!, node.end)
+          s.overwrite(specifier.start!, end, ``)
+          s.append(`\nconst ${as} = ${specifier.local.name}`)
+        }
+      }
+    }
+  })
+  return s.toString()
+}
+
+export function hasDefaultExport(input: string): boolean {
+  return defaultExportRE.test(input) || namedDefaultExportRE.test(input)
+}
+
+function specifierEnd(
+  input: string,
+  end: number,
+  nodeEnd: number | undefined | null
+) {
+  // export { default   , foo } ...
+  let hasCommas = false
+  let oldEnd = end
+  while (end < nodeEnd!) {
+    if (/\s/.test(input.charAt(end))) {
+      end++
+    } else if (input.charAt(end) === ',') {
+      end++
+      hasCommas = true
+      break
+    } else if (input.charAt(end) === '}') {
+      break
+    }
+  }
+  return hasCommas ? end : oldEnd
+}