refactor: simplify sfc script transform usage

This commit is contained in:
Evan You 2020-07-09 18:18:46 -04:00
parent 9f706a9f5e
commit b4f7ab45ea
4 changed files with 122 additions and 99 deletions

View File

@ -1,10 +1,9 @@
import { parse, compileScriptSetup, SFCScriptCompileOptions } from '../src' import { parse, SFCScriptCompileOptions } from '../src'
import { parse as babelParse } from '@babel/parser' import { parse as babelParse } from '@babel/parser'
import { babelParserDefautPlugins } from '@vue/shared' import { babelParserDefautPlugins } from '@vue/shared'
function compile(src: string, options?: SFCScriptCompileOptions) { function compile(src: string, options?: SFCScriptCompileOptions) {
const { descriptor } = parse(src) return parse(src, options).descriptor.scriptTransformed!
return compileScriptSetup(descriptor, options)
} }
function assertCode(code: string) { function assertCode(code: string) {
@ -23,17 +22,19 @@ function assertCode(code: string) {
describe('SFC compile <script setup>', () => { describe('SFC compile <script setup>', () => {
test('should hoist imports', () => { test('should hoist imports', () => {
assertCode(compile(`<script setup>import { ref } from 'vue'</script>`).code) assertCode(
compile(`<script setup>import { ref } from 'vue'</script>`).content
)
}) })
test('explicit setup signature', () => { test('explicit setup signature', () => {
assertCode( assertCode(
compile(`<script setup="props, { emit }">emit('foo')</script>`).code compile(`<script setup="props, { emit }">emit('foo')</script>`).content
) )
}) })
test('import dedupe between <script> and <script setup>', () => { test('import dedupe between <script> and <script setup>', () => {
const code = compile(` const { content } = compile(`
<script> <script>
import { x } from './x' import { x } from './x'
</script> </script>
@ -41,30 +42,30 @@ describe('SFC compile <script setup>', () => {
import { x } from './x' import { x } from './x'
x() x()
</script> </script>
`).code `)
assertCode(code) assertCode(content)
expect(code.indexOf(`import { x }`)).toEqual( expect(content.indexOf(`import { x }`)).toEqual(
code.lastIndexOf(`import { x }`) content.lastIndexOf(`import { x }`)
) )
}) })
describe('exports', () => { describe('exports', () => {
test('export const x = ...', () => { test('export const x = ...', () => {
const { code, bindings } = compile( const { content, bindings } = compile(
`<script setup>export const x = 1</script>` `<script setup>export const x = 1</script>`
) )
assertCode(code) assertCode(content)
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
x: 'setup' x: 'setup'
}) })
}) })
test('export const { x } = ... (destructuring)', () => { test('export const { x } = ... (destructuring)', () => {
const { code, bindings } = compile(`<script setup> const { content, bindings } = compile(`<script setup>
export const [a = 1, { b } = { b: 123 }, ...c] = useFoo() export const [a = 1, { b } = { b: 123 }, ...c] = useFoo()
export const { d = 2, _: [e], ...f } = useBar() export const { d = 2, _: [e], ...f } = useBar()
</script>`) </script>`)
assertCode(code) assertCode(content)
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
a: 'setup', a: 'setup',
b: 'setup', b: 'setup',
@ -76,34 +77,34 @@ describe('SFC compile <script setup>', () => {
}) })
test('export function x() {}', () => { test('export function x() {}', () => {
const { code, bindings } = compile( const { content, bindings } = compile(
`<script setup>export function x(){}</script>` `<script setup>export function x(){}</script>`
) )
assertCode(code) assertCode(content)
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
x: 'setup' x: 'setup'
}) })
}) })
test('export class X() {}', () => { test('export class X() {}', () => {
const { code, bindings } = compile( const { content, bindings } = compile(
`<script setup>export class X {}</script>` `<script setup>export class X {}</script>`
) )
assertCode(code) assertCode(content)
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
X: 'setup' X: 'setup'
}) })
}) })
test('export { x }', () => { test('export { x }', () => {
const { code, bindings } = compile( const { content, bindings } = compile(
`<script setup> `<script setup>
const x = 1 const x = 1
const y = 2 const y = 2
export { x, y } export { x, y }
</script>` </script>`
) )
assertCode(code) assertCode(content)
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
x: 'setup', x: 'setup',
y: 'setup' y: 'setup'
@ -111,12 +112,12 @@ describe('SFC compile <script setup>', () => {
}) })
test(`export { x } from './x'`, () => { test(`export { x } from './x'`, () => {
const { code, bindings } = compile( const { content, bindings } = compile(
`<script setup> `<script setup>
export { x, y } from './x' export { x, y } from './x'
</script>` </script>`
) )
assertCode(code) assertCode(content)
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
x: 'setup', x: 'setup',
y: 'setup' y: 'setup'
@ -124,52 +125,52 @@ describe('SFC compile <script setup>', () => {
}) })
test(`export default from './x'`, () => { test(`export default from './x'`, () => {
const { code, bindings } = compile( const { content, bindings } = compile(
`<script setup> `<script setup>
export default from './x' export default from './x'
</script>`, </script>`,
{ {
parserPlugins: ['exportDefaultFrom'] babelParserPlugins: ['exportDefaultFrom']
} }
) )
assertCode(code) assertCode(content)
expect(bindings).toStrictEqual({}) expect(bindings).toStrictEqual({})
}) })
test(`export { x as default }`, () => { test(`export { x as default }`, () => {
const { code, bindings } = compile( const { content, bindings } = compile(
`<script setup> `<script setup>
import x from './x' import x from './x'
const y = 1 const y = 1
export { x as default, y } export { x as default, y }
</script>` </script>`
) )
assertCode(code) assertCode(content)
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
y: 'setup' y: 'setup'
}) })
}) })
test(`export { x as default } from './x'`, () => { test(`export { x as default } from './x'`, () => {
const { code, bindings } = compile( const { content, bindings } = compile(
`<script setup> `<script setup>
export { x as default, y } from './x' export { x as default, y } from './x'
</script>` </script>`
) )
assertCode(code) assertCode(content)
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
y: 'setup' y: 'setup'
}) })
}) })
test(`export * from './x'`, () => { test(`export * from './x'`, () => {
const { code, bindings } = compile( const { content, bindings } = compile(
`<script setup> `<script setup>
export * from './x' export * from './x'
export const y = 1 export const y = 1
</script>` </script>`
) )
assertCode(code) assertCode(content)
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
y: 'setup' y: 'setup'
// in this case we cannot extract bindings from ./x so it falls back // in this case we cannot extract bindings from ./x so it falls back
@ -178,7 +179,7 @@ describe('SFC compile <script setup>', () => {
}) })
test('export default in <script setup>', () => { test('export default in <script setup>', () => {
const { code, bindings } = compile( const { content, bindings } = compile(
`<script setup> `<script setup>
export default { export default {
props: ['foo'] props: ['foo']
@ -186,7 +187,7 @@ describe('SFC compile <script setup>', () => {
export const y = 1 export const y = 1
</script>` </script>`
) )
assertCode(code) assertCode(content)
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
y: 'setup' y: 'setup'
}) })
@ -195,18 +196,18 @@ describe('SFC compile <script setup>', () => {
describe('<script setup lang="ts">', () => { describe('<script setup lang="ts">', () => {
test('hoist type declarations', () => { test('hoist type declarations', () => {
const { code, bindings } = compile(` const { content, bindings } = compile(`
<script setup lang="ts"> <script setup lang="ts">
export interface Foo {} export interface Foo {}
type Bar = {} type Bar = {}
export const a = 1 export const a = 1
</script>`) </script>`)
assertCode(code) assertCode(content)
expect(bindings).toStrictEqual({ a: 'setup' }) expect(bindings).toStrictEqual({ a: 'setup' })
}) })
test('extract props', () => { test('extract props', () => {
const { code } = compile(` const { content } = compile(`
<script setup="myProps" lang="ts"> <script setup="myProps" lang="ts">
interface Test {} interface Test {}
@ -237,59 +238,57 @@ describe('SFC compile <script setup>', () => {
intersection: Test & {} intersection: Test & {}
} }
</script>`) </script>`)
assertCode(code) assertCode(content)
expect(code).toMatch(`string: { type: String, required: true }`) expect(content).toMatch(`string: { type: String, required: true }`)
expect(code).toMatch(`number: { type: Number, required: true }`) expect(content).toMatch(`number: { type: Number, required: true }`)
expect(code).toMatch(`boolean: { type: Boolean, required: true }`) expect(content).toMatch(`boolean: { type: Boolean, required: true }`)
expect(code).toMatch(`object: { type: Object, required: true }`) expect(content).toMatch(`object: { type: Object, required: true }`)
expect(code).toMatch(`objectLiteral: { type: Object, required: true }`) expect(content).toMatch(`objectLiteral: { type: Object, required: true }`)
expect(code).toMatch(`fn: { type: Function, required: true }`) expect(content).toMatch(`fn: { type: Function, required: true }`)
expect(code).toMatch(`functionRef: { type: Function, required: true }`) expect(content).toMatch(`functionRef: { type: Function, required: true }`)
expect(code).toMatch(`objectRef: { type: Object, required: true }`) expect(content).toMatch(`objectRef: { type: Object, required: true }`)
expect(code).toMatch(`array: { type: Array, required: true }`) expect(content).toMatch(`array: { type: Array, required: true }`)
expect(code).toMatch(`arrayRef: { type: Array, required: true }`) expect(content).toMatch(`arrayRef: { type: Array, required: true }`)
expect(code).toMatch(`tuple: { type: Array, required: true }`) expect(content).toMatch(`tuple: { type: Array, required: true }`)
expect(code).toMatch(`set: { type: Set, required: true }`) expect(content).toMatch(`set: { type: Set, required: true }`)
expect(code).toMatch(`literal: { type: String, required: true }`) expect(content).toMatch(`literal: { type: String, required: true }`)
expect(code).toMatch(`optional: { type: null, required: false }`) expect(content).toMatch(`optional: { type: null, required: false }`)
expect(code).toMatch(`recordRef: { type: Object, required: true }`) expect(content).toMatch(`recordRef: { type: Object, required: true }`)
expect(code).toMatch(`interface: { type: Object, required: true }`) expect(content).toMatch(`interface: { type: Object, required: true }`)
expect(code).toMatch(`alias: { type: Array, required: true }`) expect(content).toMatch(`alias: { type: Array, required: true }`)
expect(code).toMatch(`union: { type: [String, Number], required: true }`) expect(content).toMatch(
expect(code).toMatch( `union: { type: [String, Number], required: true }`
)
expect(content).toMatch(
`literalUnion: { type: [String, String], required: true }` `literalUnion: { type: [String, String], required: true }`
) )
expect(code).toMatch( expect(content).toMatch(
`literalUnionMixed: { type: [String, Number, Boolean], required: true }` `literalUnionMixed: { type: [String, Number, Boolean], required: true }`
) )
expect(code).toMatch(`intersection: { type: Object, required: true }`) expect(content).toMatch(`intersection: { type: Object, required: true }`)
}) })
test('extract emits', () => { test('extract emits', () => {
const { code } = compile(` const { content } = compile(`
<script setup="_, { emit: myEmit }" lang="ts"> <script setup="_, { emit: myEmit }" lang="ts">
declare function myEmit(e: 'foo' | 'bar'): void declare function myEmit(e: 'foo' | 'bar'): void
declare function myEmit(e: 'baz', id: number): void declare function myEmit(e: 'baz', id: number): void
</script> </script>
`) `)
assertCode(code) assertCode(content)
expect(code).toMatch(`declare function __emit__(e: 'foo' | 'bar'): void`) expect(content).toMatch(
expect(code).toMatch( `declare function __emit__(e: 'foo' | 'bar'): void`
)
expect(content).toMatch(
`declare function __emit__(e: 'baz', id: number): void` `declare function __emit__(e: 'baz', id: number): void`
) )
expect(code).toMatch( expect(content).toMatch(
`emits: ["foo", "bar", "baz"] as unknown as undefined` `emits: ["foo", "bar", "baz"] as unknown as undefined`
) )
}) })
}) })
describe('errors', () => { describe('errors', () => {
test('must have <script setup>', () => {
expect(() => compile(`<script>foo()</script>`)).toThrow(
`SFC has no <script setup>`
)
})
test('<script> and <script setup> must have same lang', () => { test('<script> and <script setup> must have same lang', () => {
expect(() => expect(() =>
compile(`<script>foo()</script><script setup lang="ts">bar()</script>`) compile(`<script>foo()</script><script setup lang="ts">bar()</script>`)
@ -342,7 +341,7 @@ describe('SFC compile <script setup>', () => {
} }
} }
} }
</script>`).code </script>`).content
) )
}) })
@ -358,7 +357,7 @@ describe('SFC compile <script setup>', () => {
} }
} }
} }
</script>`).code </script>`).content
) )
}) })
@ -373,7 +372,7 @@ describe('SFC compile <script setup>', () => {
} }
} }
} }
</script>`).code </script>`).content
) )
}) })

View File

@ -1,4 +1,4 @@
import MagicString, { SourceMap } from 'magic-string' import MagicString from 'magic-string'
import { SFCDescriptor, SFCScriptBlock } from './parse' import { SFCDescriptor, SFCScriptBlock } from './parse'
import { parse, ParserPlugin } from '@babel/parser' import { parse, ParserPlugin } from '@babel/parser'
import { babelParserDefautPlugins, generateCodeFrame } from '@vue/shared' import { babelParserDefautPlugins, generateCodeFrame } from '@vue/shared'
@ -17,15 +17,16 @@ import {
TSDeclareFunction TSDeclareFunction
} from '@babel/types' } from '@babel/types'
import { walk } from 'estree-walker' import { walk } from 'estree-walker'
import { RawSourceMap } from 'source-map'
export interface SFCScriptCompileOptions {
babelParserPlugins?: ParserPlugin[]
}
export interface BindingMetadata { export interface BindingMetadata {
[key: string]: 'data' | 'props' | 'setup' | 'ctx' [key: string]: 'data' | 'props' | 'setup' | 'ctx'
} }
export interface SFCScriptCompileOptions {
parserPlugins?: ParserPlugin[]
}
let hasWarned = false let hasWarned = false
/** /**
@ -33,10 +34,10 @@ let hasWarned = false
* It requires the whole SFC descriptor because we need to handle and merge * It requires the whole SFC descriptor because we need to handle and merge
* normal `<script>` + `<script setup>` if both are present. * normal `<script>` + `<script setup>` if both are present.
*/ */
export function compileScriptSetup( export function compileScript(
sfc: SFCDescriptor, sfc: SFCDescriptor,
options: SFCScriptCompileOptions = {} options: SFCScriptCompileOptions = {}
) { ): SFCScriptBlock {
if (__DEV__ && !__TEST__ && !hasWarned) { if (__DEV__ && !__TEST__ && !hasWarned) {
hasWarned = true hasWarned = true
console.log( console.log(
@ -47,7 +48,13 @@ export function compileScriptSetup(
const { script, scriptSetup, source, filename } = sfc const { script, scriptSetup, source, filename } = sfc
if (!scriptSetup) { if (!scriptSetup) {
throw new Error('SFC has no <script setup>.') if (!script) {
throw new Error(`SFC contains no <script> tags.`)
}
return {
...script,
bindings: analyzeScriptBindings(script)
}
} }
if (script && script.lang !== scriptSetup.lang) { if (script && script.lang !== scriptSetup.lang) {
@ -86,7 +93,7 @@ export function compileScriptSetup(
const isTS = scriptSetup.lang === 'ts' const isTS = scriptSetup.lang === 'ts'
const plugins: ParserPlugin[] = [ const plugins: ParserPlugin[] = [
...(options.parserPlugins || []), ...(options.babelParserPlugins || []),
...babelParserDefautPlugins, ...babelParserDefautPlugins,
...(isTS ? (['typescript'] as const) : []) ...(isTS ? (['typescript'] as const) : [])
] ]
@ -154,7 +161,8 @@ export function compileScriptSetup(
} }
// 2. check <script setup="xxx"> function signature // 2. check <script setup="xxx"> function signature
const hasExplicitSignature = typeof scriptSetup.setup === 'string' const setupValue = scriptSetup.attrs.setup
const hasExplicitSignature = typeof setupValue === 'string'
let propsVar: string | undefined let propsVar: string | undefined
let emitVar: string | undefined let emitVar: string | undefined
@ -179,8 +187,8 @@ export function compileScriptSetup(
// <script setup="xxx" lang="ts"> // <script setup="xxx" lang="ts">
// parse the signature to extract the props/emit variables the user wants // parse the signature to extract the props/emit variables the user wants
// we need them to find corresponding type declarations. // we need them to find corresponding type declarations.
const signatureAST = parse(`(${scriptSetup.setup})=>{}`, { plugins }) const signatureAST = parse(`(${setupValue})=>{}`, { plugins }).program
.program.body[0] .body[0]
const params = ((signatureAST as ExpressionStatement) const params = ((signatureAST as ExpressionStatement)
.expression as ArrowFunctionExpression).params .expression as ArrowFunctionExpression).params
if (params[0] && params[0].type === 'Identifier') { if (params[0] && params[0].type === 'Identifier') {
@ -464,7 +472,7 @@ export function compileScriptSetup(
}` }`
if (hasExplicitSignature) { if (hasExplicitSignature) {
// inject types to user signature // inject types to user signature
args = scriptSetup.setup as string args = setupValue as string
const ss = new MagicString(args) const ss = new MagicString(args)
if (propsASTNode) { if (propsASTNode) {
// compensate for () wraper offset // compensate for () wraper offset
@ -476,7 +484,7 @@ export function compileScriptSetup(
args = ss.toString() args = ss.toString()
} }
} else { } else {
args = hasExplicitSignature ? (scriptSetup.setup as string) : `` args = hasExplicitSignature ? (setupValue as string) : ``
} }
// 6. wrap setup code with function. // 6. wrap setup code with function.
@ -530,13 +538,14 @@ export function compileScriptSetup(
s.trim() s.trim()
return { return {
...scriptSetup,
bindings, bindings,
code: s.toString(), content: s.toString(),
map: s.generateMap({ map: (s.generateMap({
source: filename, source: filename,
hires: true, hires: true,
includeContent: true includeContent: true
}) as SourceMap }) as unknown) as RawSourceMap
} }
} }

View File

@ -2,7 +2,7 @@
export { parse } from './parse' export { parse } from './parse'
export { compileTemplate } from './compileTemplate' export { compileTemplate } from './compileTemplate'
export { compileStyle, compileStyleAsync } from './compileStyle' export { compileStyle, compileStyleAsync } from './compileStyle'
export { compileScriptSetup, analyzeScriptBindings } from './compileScript' export { compileScript, analyzeScriptBindings } from './compileScript'
// Types // Types
export { export {
@ -23,7 +23,7 @@ export {
SFCAsyncStyleCompileOptions, SFCAsyncStyleCompileOptions,
SFCStyleCompileResults SFCStyleCompileResults
} from './compileStyle' } from './compileStyle'
export { SFCScriptCompileOptions } from './compileScript' export { SFCScriptCompileOptions, BindingMetadata } from './compileScript'
export { export {
CompilerOptions, CompilerOptions,
CompilerError, CompilerError,

View File

@ -5,10 +5,12 @@ import {
CompilerError, CompilerError,
TextModes TextModes
} from '@vue/compiler-core' } from '@vue/compiler-core'
import * as CompilerDOM from '@vue/compiler-dom'
import { RawSourceMap, SourceMapGenerator } from 'source-map' import { RawSourceMap, SourceMapGenerator } from 'source-map'
import { generateCodeFrame } from '@vue/shared' import { generateCodeFrame } from '@vue/shared'
import { TemplateCompiler } from './compileTemplate' import { TemplateCompiler } from './compileTemplate'
import * as CompilerDOM from '@vue/compiler-dom' import { compileScript, BindingMetadata } from './compileScript'
import { ParserPlugin } from '@babel/parser'
export interface SFCParseOptions { export interface SFCParseOptions {
filename?: string filename?: string
@ -16,6 +18,7 @@ export interface SFCParseOptions {
sourceRoot?: string sourceRoot?: string
pad?: boolean | 'line' | 'space' pad?: boolean | 'line' | 'space'
compiler?: TemplateCompiler compiler?: TemplateCompiler
babelParserPlugins?: ParserPlugin[]
} }
export interface SFCBlock { export interface SFCBlock {
@ -35,7 +38,7 @@ export interface SFCTemplateBlock extends SFCBlock {
export interface SFCScriptBlock extends SFCBlock { export interface SFCScriptBlock extends SFCBlock {
type: 'script' type: 'script'
setup?: boolean | string bindings?: BindingMetadata
} }
export interface SFCStyleBlock extends SFCBlock { export interface SFCStyleBlock extends SFCBlock {
@ -50,6 +53,7 @@ export interface SFCDescriptor {
template: SFCTemplateBlock | null template: SFCTemplateBlock | null
script: SFCScriptBlock | null script: SFCScriptBlock | null
scriptSetup: SFCScriptBlock | null scriptSetup: SFCScriptBlock | null
scriptTransformed: SFCScriptBlock | null
styles: SFCStyleBlock[] styles: SFCStyleBlock[]
customBlocks: SFCBlock[] customBlocks: SFCBlock[]
} }
@ -75,7 +79,8 @@ export function parse(
filename = 'component.vue', filename = 'component.vue',
sourceRoot = '', sourceRoot = '',
pad = false, pad = false,
compiler = CompilerDOM compiler = CompilerDOM,
babelParserPlugins
}: SFCParseOptions = {} }: SFCParseOptions = {}
): SFCParseResult { ): SFCParseResult {
const sourceKey = const sourceKey =
@ -91,6 +96,7 @@ export function parse(
template: null, template: null,
script: null, script: null,
scriptSetup: null, scriptSetup: null,
scriptTransformed: null,
styles: [], styles: [],
customBlocks: [] customBlocks: []
} }
@ -146,15 +152,16 @@ export function parse(
break break
case 'script': case 'script':
const block = createBlock(node, source, pad) as SFCScriptBlock const block = createBlock(node, source, pad) as SFCScriptBlock
if (block.setup && !descriptor.scriptSetup) { const isSetup = !!block.attrs.setup
if (isSetup && !descriptor.scriptSetup) {
descriptor.scriptSetup = block descriptor.scriptSetup = block
break break
} }
if (!block.setup && !descriptor.script) { if (!isSetup && !descriptor.script) {
descriptor.script = block descriptor.script = block
break break
} }
warnDuplicateBlock(source, filename, node, !!block.setup) warnDuplicateBlock(source, filename, node, isSetup)
break break
case 'style': case 'style':
descriptor.styles.push(createBlock(node, source, pad) as SFCStyleBlock) descriptor.styles.push(createBlock(node, source, pad) as SFCStyleBlock)
@ -182,6 +189,16 @@ export function parse(
descriptor.styles.forEach(genMap) descriptor.styles.forEach(genMap)
} }
if (descriptor.script || descriptor.scriptSetup) {
try {
descriptor.scriptTransformed = compileScript(descriptor, {
babelParserPlugins
})
} catch (e) {
errors.push(e)
}
}
const result = { const result = {
descriptor, descriptor,
errors errors
@ -252,8 +269,6 @@ function createBlock(
} }
} else if (type === 'template' && p.name === 'functional') { } else if (type === 'template' && p.name === 'functional') {
;(block as SFCTemplateBlock).functional = true ;(block as SFCTemplateBlock).functional = true
} else if (type === 'script' && p.name === 'setup') {
;(block as SFCScriptBlock).setup = attrs.setup || true
} }
} }
}) })