-import { parse, compileScriptSetup, SFCScriptCompileOptions } from '../src'
+import { parse, SFCScriptCompileOptions } from '../src'
import { parse as babelParse } from '@babel/parser'
import { babelParserDefautPlugins } from '@vue/shared'
function compile(src: string, options?: SFCScriptCompileOptions) {
- const { descriptor } = parse(src)
- return compileScriptSetup(descriptor, options)
+ return parse(src, options).descriptor.scriptTransformed!
}
function assertCode(code: string) {
describe('SFC compile <script setup>', () => {
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', () => {
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>', () => {
- const code = compile(`
+ const { content } = compile(`
<script>
import { x } from './x'
</script>
import { x } from './x'
x()
</script>
- `).code
- assertCode(code)
- expect(code.indexOf(`import { x }`)).toEqual(
- code.lastIndexOf(`import { x }`)
+ `)
+ assertCode(content)
+ expect(content.indexOf(`import { x }`)).toEqual(
+ content.lastIndexOf(`import { x }`)
)
})
describe('exports', () => {
test('export const x = ...', () => {
- const { code, bindings } = compile(
+ const { content, bindings } = compile(
`<script setup>export const x = 1</script>`
)
- assertCode(code)
+ assertCode(content)
expect(bindings).toStrictEqual({
x: 'setup'
})
})
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 { d = 2, _: [e], ...f } = useBar()
</script>`)
- assertCode(code)
+ assertCode(content)
expect(bindings).toStrictEqual({
a: 'setup',
b: 'setup',
})
test('export function x() {}', () => {
- const { code, bindings } = compile(
+ const { content, bindings } = compile(
`<script setup>export function x(){}</script>`
)
- assertCode(code)
+ assertCode(content)
expect(bindings).toStrictEqual({
x: 'setup'
})
})
test('export class X() {}', () => {
- const { code, bindings } = compile(
+ const { content, bindings } = compile(
`<script setup>export class X {}</script>`
)
- assertCode(code)
+ assertCode(content)
expect(bindings).toStrictEqual({
X: 'setup'
})
})
test('export { x }', () => {
- const { code, bindings } = compile(
+ const { content, bindings } = compile(
`<script setup>
const x = 1
const y = 2
export { x, y }
</script>`
)
- assertCode(code)
+ assertCode(content)
expect(bindings).toStrictEqual({
x: 'setup',
y: 'setup'
})
test(`export { x } from './x'`, () => {
- const { code, bindings } = compile(
+ const { content, bindings } = compile(
`<script setup>
export { x, y } from './x'
</script>`
)
- assertCode(code)
+ assertCode(content)
expect(bindings).toStrictEqual({
x: 'setup',
y: 'setup'
})
test(`export default from './x'`, () => {
- const { code, bindings } = compile(
+ const { content, bindings } = compile(
`<script setup>
export default from './x'
</script>`,
{
- parserPlugins: ['exportDefaultFrom']
+ babelParserPlugins: ['exportDefaultFrom']
}
)
- assertCode(code)
+ assertCode(content)
expect(bindings).toStrictEqual({})
})
test(`export { x as default }`, () => {
- const { code, bindings } = compile(
+ const { content, bindings } = compile(
`<script setup>
import x from './x'
const y = 1
export { x as default, y }
</script>`
)
- assertCode(code)
+ assertCode(content)
expect(bindings).toStrictEqual({
y: 'setup'
})
})
test(`export { x as default } from './x'`, () => {
- const { code, bindings } = compile(
+ const { content, bindings } = compile(
`<script setup>
export { x as default, y } from './x'
</script>`
)
- assertCode(code)
+ assertCode(content)
expect(bindings).toStrictEqual({
y: 'setup'
})
})
test(`export * from './x'`, () => {
- const { code, bindings } = compile(
+ const { content, bindings } = compile(
`<script setup>
export * from './x'
export const y = 1
</script>`
)
- assertCode(code)
+ assertCode(content)
expect(bindings).toStrictEqual({
y: 'setup'
// in this case we cannot extract bindings from ./x so it falls back
})
test('export default in <script setup>', () => {
- const { code, bindings } = compile(
+ const { content, bindings } = compile(
`<script setup>
export default {
props: ['foo']
export const y = 1
</script>`
)
- assertCode(code)
+ assertCode(content)
expect(bindings).toStrictEqual({
y: 'setup'
})
describe('<script setup lang="ts">', () => {
test('hoist type declarations', () => {
- const { code, bindings } = compile(`
+ const { content, bindings } = compile(`
<script setup lang="ts">
export interface Foo {}
type Bar = {}
export const a = 1
</script>`)
- assertCode(code)
+ assertCode(content)
expect(bindings).toStrictEqual({ a: 'setup' })
})
test('extract props', () => {
- const { code } = compile(`
+ const { content } = compile(`
<script setup="myProps" lang="ts">
interface Test {}
intersection: Test & {}
}
</script>`)
- assertCode(code)
- expect(code).toMatch(`string: { type: String, required: true }`)
- expect(code).toMatch(`number: { type: Number, required: true }`)
- expect(code).toMatch(`boolean: { type: Boolean, required: true }`)
- expect(code).toMatch(`object: { type: Object, required: true }`)
- expect(code).toMatch(`objectLiteral: { type: Object, required: true }`)
- expect(code).toMatch(`fn: { type: Function, required: true }`)
- expect(code).toMatch(`functionRef: { type: Function, required: true }`)
- expect(code).toMatch(`objectRef: { type: Object, required: true }`)
- expect(code).toMatch(`array: { type: Array, required: true }`)
- expect(code).toMatch(`arrayRef: { type: Array, required: true }`)
- expect(code).toMatch(`tuple: { type: Array, required: true }`)
- expect(code).toMatch(`set: { type: Set, required: true }`)
- expect(code).toMatch(`literal: { type: String, required: true }`)
- expect(code).toMatch(`optional: { type: null, required: false }`)
- expect(code).toMatch(`recordRef: { type: Object, required: true }`)
- expect(code).toMatch(`interface: { type: Object, required: true }`)
- expect(code).toMatch(`alias: { type: Array, required: true }`)
- expect(code).toMatch(`union: { type: [String, Number], required: true }`)
- expect(code).toMatch(
+ assertCode(content)
+ expect(content).toMatch(`string: { type: String, required: true }`)
+ expect(content).toMatch(`number: { type: Number, required: true }`)
+ expect(content).toMatch(`boolean: { type: Boolean, required: true }`)
+ expect(content).toMatch(`object: { type: Object, required: true }`)
+ expect(content).toMatch(`objectLiteral: { type: Object, required: true }`)
+ expect(content).toMatch(`fn: { type: Function, required: true }`)
+ expect(content).toMatch(`functionRef: { type: Function, required: true }`)
+ expect(content).toMatch(`objectRef: { type: Object, required: true }`)
+ expect(content).toMatch(`array: { type: Array, required: true }`)
+ expect(content).toMatch(`arrayRef: { type: Array, required: true }`)
+ expect(content).toMatch(`tuple: { type: Array, required: true }`)
+ expect(content).toMatch(`set: { type: Set, required: true }`)
+ expect(content).toMatch(`literal: { type: String, required: true }`)
+ expect(content).toMatch(`optional: { type: null, required: false }`)
+ expect(content).toMatch(`recordRef: { type: Object, required: true }`)
+ expect(content).toMatch(`interface: { type: Object, required: true }`)
+ expect(content).toMatch(`alias: { type: Array, required: true }`)
+ expect(content).toMatch(
+ `union: { type: [String, Number], required: true }`
+ )
+ expect(content).toMatch(
`literalUnion: { type: [String, String], required: true }`
)
- expect(code).toMatch(
+ expect(content).toMatch(
`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', () => {
- const { code } = compile(`
+ const { content } = compile(`
<script setup="_, { emit: myEmit }" lang="ts">
declare function myEmit(e: 'foo' | 'bar'): void
declare function myEmit(e: 'baz', id: number): void
</script>
`)
- assertCode(code)
- expect(code).toMatch(`declare function __emit__(e: 'foo' | 'bar'): void`)
- expect(code).toMatch(
+ assertCode(content)
+ expect(content).toMatch(
+ `declare function __emit__(e: 'foo' | 'bar'): void`
+ )
+ expect(content).toMatch(
`declare function __emit__(e: 'baz', id: number): void`
)
- expect(code).toMatch(
+ expect(content).toMatch(
`emits: ["foo", "bar", "baz"] as unknown as undefined`
)
})
})
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', () => {
expect(() =>
compile(`<script>foo()</script><script setup lang="ts">bar()</script>`)
}
}
}
- </script>`).code
+ </script>`).content
)
})
}
}
}
- </script>`).code
+ </script>`).content
)
})
}
}
}
- </script>`).code
+ </script>`).content
)
})
-import MagicString, { SourceMap } from 'magic-string'
+import MagicString from 'magic-string'
import { SFCDescriptor, SFCScriptBlock } from './parse'
import { parse, ParserPlugin } from '@babel/parser'
import { babelParserDefautPlugins, generateCodeFrame } from '@vue/shared'
TSDeclareFunction
} from '@babel/types'
import { walk } from 'estree-walker'
+import { RawSourceMap } from 'source-map'
-export interface BindingMetadata {
- [key: string]: 'data' | 'props' | 'setup' | 'ctx'
+export interface SFCScriptCompileOptions {
+ babelParserPlugins?: ParserPlugin[]
}
-export interface SFCScriptCompileOptions {
- parserPlugins?: ParserPlugin[]
+export interface BindingMetadata {
+ [key: string]: 'data' | 'props' | 'setup' | 'ctx'
}
let hasWarned = false
* It requires the whole SFC descriptor because we need to handle and merge
* normal `<script>` + `<script setup>` if both are present.
*/
-export function compileScriptSetup(
+export function compileScript(
sfc: SFCDescriptor,
options: SFCScriptCompileOptions = {}
-) {
+): SFCScriptBlock {
if (__DEV__ && !__TEST__ && !hasWarned) {
hasWarned = true
console.log(
const { script, scriptSetup, source, filename } = sfc
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) {
const isTS = scriptSetup.lang === 'ts'
const plugins: ParserPlugin[] = [
- ...(options.parserPlugins || []),
+ ...(options.babelParserPlugins || []),
...babelParserDefautPlugins,
...(isTS ? (['typescript'] as const) : [])
]
}
// 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 emitVar: string | undefined
// <script setup="xxx" lang="ts">
// parse the signature to extract the props/emit variables the user wants
// we need them to find corresponding type declarations.
- const signatureAST = parse(`(${scriptSetup.setup})=>{}`, { plugins })
- .program.body[0]
+ const signatureAST = parse(`(${setupValue})=>{}`, { plugins }).program
+ .body[0]
const params = ((signatureAST as ExpressionStatement)
.expression as ArrowFunctionExpression).params
if (params[0] && params[0].type === 'Identifier') {
}`
if (hasExplicitSignature) {
// inject types to user signature
- args = scriptSetup.setup as string
+ args = setupValue as string
const ss = new MagicString(args)
if (propsASTNode) {
// compensate for () wraper offset
args = ss.toString()
}
} else {
- args = hasExplicitSignature ? (scriptSetup.setup as string) : ``
+ args = hasExplicitSignature ? (setupValue as string) : ``
}
// 6. wrap setup code with function.
s.trim()
return {
+ ...scriptSetup,
bindings,
- code: s.toString(),
- map: s.generateMap({
+ content: s.toString(),
+ map: (s.generateMap({
source: filename,
hires: true,
includeContent: true
- }) as SourceMap
+ }) as unknown) as RawSourceMap
}
}
CompilerError,
TextModes
} from '@vue/compiler-core'
+import * as CompilerDOM from '@vue/compiler-dom'
import { RawSourceMap, SourceMapGenerator } from 'source-map'
import { generateCodeFrame } from '@vue/shared'
import { TemplateCompiler } from './compileTemplate'
-import * as CompilerDOM from '@vue/compiler-dom'
+import { compileScript, BindingMetadata } from './compileScript'
+import { ParserPlugin } from '@babel/parser'
export interface SFCParseOptions {
filename?: string
sourceRoot?: string
pad?: boolean | 'line' | 'space'
compiler?: TemplateCompiler
+ babelParserPlugins?: ParserPlugin[]
}
export interface SFCBlock {
export interface SFCScriptBlock extends SFCBlock {
type: 'script'
- setup?: boolean | string
+ bindings?: BindingMetadata
}
export interface SFCStyleBlock extends SFCBlock {
template: SFCTemplateBlock | null
script: SFCScriptBlock | null
scriptSetup: SFCScriptBlock | null
+ scriptTransformed: SFCScriptBlock | null
styles: SFCStyleBlock[]
customBlocks: SFCBlock[]
}
filename = 'component.vue',
sourceRoot = '',
pad = false,
- compiler = CompilerDOM
+ compiler = CompilerDOM,
+ babelParserPlugins
}: SFCParseOptions = {}
): SFCParseResult {
const sourceKey =
template: null,
script: null,
scriptSetup: null,
+ scriptTransformed: null,
styles: [],
customBlocks: []
}
break
case 'script':
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
break
}
- if (!block.setup && !descriptor.script) {
+ if (!isSetup && !descriptor.script) {
descriptor.script = block
break
}
- warnDuplicateBlock(source, filename, node, !!block.setup)
+ warnDuplicateBlock(source, filename, node, isSetup)
break
case 'style':
descriptor.styles.push(createBlock(node, source, pad) as SFCStyleBlock)
descriptor.styles.forEach(genMap)
}
+ if (descriptor.script || descriptor.scriptSetup) {
+ try {
+ descriptor.scriptTransformed = compileScript(descriptor, {
+ babelParserPlugins
+ })
+ } catch (e) {
+ errors.push(e)
+ }
+ }
+
const result = {
descriptor,
errors
}
} else if (type === 'template' && p.name === 'functional') {
;(block as SFCTemplateBlock).functional = true
- } else if (type === 'script' && p.name === 'setup') {
- ;(block as SFCScriptBlock).setup = attrs.setup || true
}
}
})