wip: compileScriptSetup
This commit is contained in:
214
packages/compiler-sfc/src/compileScript.ts
Normal file
214
packages/compiler-sfc/src/compileScript.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import MagicString, { SourceMap } from 'magic-string'
|
||||
import { SFCDescriptor, SFCScriptBlock } from './parse'
|
||||
import { parse, ParserPlugin } from '@babel/parser'
|
||||
import { babelParserDefautPlugins } from '@vue/shared'
|
||||
import { ObjectPattern, ArrayPattern } from '@babel/types'
|
||||
|
||||
export interface BindingMetadata {
|
||||
[key: string]: 'data' | 'props' | 'setup' | 'ctx'
|
||||
}
|
||||
|
||||
export interface SFCScriptCompileOptions {
|
||||
parserPlugins?: ParserPlugin[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile `<script setup>`
|
||||
* It requires the whole SFC descriptor because we need to handle and merge
|
||||
* normal `<script>` + `<script setup>` if both are present.
|
||||
*/
|
||||
export function compileScriptSetup(
|
||||
sfc: SFCDescriptor,
|
||||
options: SFCScriptCompileOptions = {}
|
||||
) {
|
||||
const { script, scriptSetup, source, filename } = sfc
|
||||
if (!scriptSetup) {
|
||||
throw new Error('SFC has no <script setup>.')
|
||||
}
|
||||
|
||||
if (script && script.lang !== scriptSetup.lang) {
|
||||
throw new Error(
|
||||
`<script> and <script setup> must have the same language type.`
|
||||
)
|
||||
}
|
||||
|
||||
const bindings: BindingMetadata = {}
|
||||
const setupExports: string[] = []
|
||||
let exportAllIndex = 0
|
||||
|
||||
const s = new MagicString(source)
|
||||
const startOffset = scriptSetup.loc.start.offset
|
||||
const endOffset = scriptSetup.loc.end.offset
|
||||
|
||||
// parse and transform <script setup>
|
||||
const plugins: ParserPlugin[] = [
|
||||
...(options.parserPlugins || []),
|
||||
...(babelParserDefautPlugins as ParserPlugin[])
|
||||
]
|
||||
if (scriptSetup.lang === 'ts') {
|
||||
plugins.push('typescript')
|
||||
}
|
||||
|
||||
const ast = parse(scriptSetup.content, {
|
||||
plugins,
|
||||
sourceType: 'module'
|
||||
}).program.body
|
||||
|
||||
for (const node of ast) {
|
||||
const start = node.start! + startOffset
|
||||
let end = node.end! + startOffset
|
||||
// import or type declarations: move to top
|
||||
// locate the end of whitespace between this statement and the next
|
||||
while (end <= source.length) {
|
||||
if (!/\s/.test(source.charAt(end))) {
|
||||
break
|
||||
}
|
||||
end++
|
||||
}
|
||||
if (node.type === 'ImportDeclaration') {
|
||||
s.move(start, end, 0)
|
||||
}
|
||||
if (node.type === 'ExportNamedDeclaration') {
|
||||
// named exports
|
||||
if (node.declaration) {
|
||||
// variable/function/class declarations.
|
||||
// remove leading `export ` keyword
|
||||
s.remove(start, start + 7)
|
||||
if (node.declaration.type === 'VariableDeclaration') {
|
||||
// export const foo = ...
|
||||
// export declarations can only have one declaration at a time
|
||||
const id = node.declaration.declarations[0].id
|
||||
if (id.type === 'Identifier') {
|
||||
setupExports.push(id.name)
|
||||
} else if (id.type === 'ObjectPattern') {
|
||||
walkObjectPattern(id, setupExports)
|
||||
} else if (id.type === 'ArrayPattern') {
|
||||
walkArrayPattern(id, setupExports)
|
||||
}
|
||||
} else if (
|
||||
node.declaration.type === 'FunctionDeclaration' ||
|
||||
node.declaration.type === 'ClassDeclaration'
|
||||
) {
|
||||
// export function foo() {} / export class Foo {}
|
||||
// export declarations must be named.
|
||||
setupExports.push(node.declaration.id!.name)
|
||||
}
|
||||
}
|
||||
if (node.specifiers.length) {
|
||||
for (const { exported } of node.specifiers) {
|
||||
if (exported.name === 'default') {
|
||||
// TODO
|
||||
// check duplicated default export
|
||||
// walk export default to make sure it does not reference exported
|
||||
// variables
|
||||
throw new Error(
|
||||
'export default in <script setup> not supported yet'
|
||||
)
|
||||
} else {
|
||||
setupExports.push(exported.name)
|
||||
}
|
||||
}
|
||||
if (node.source) {
|
||||
// export { x } from './x'
|
||||
// change it to import and move to top
|
||||
s.overwrite(start, start + 6, 'import')
|
||||
s.move(start, end, 0)
|
||||
} else {
|
||||
// export { x }
|
||||
s.remove(start, end)
|
||||
}
|
||||
}
|
||||
} else if (node.type === 'ExportAllDeclaration') {
|
||||
// export * from './x'
|
||||
s.overwrite(
|
||||
start,
|
||||
node.source.start! + startOffset,
|
||||
`import * as __import_all_${exportAllIndex++}__ from `
|
||||
)
|
||||
s.move(start, end, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// remove non-script content
|
||||
if (script) {
|
||||
const s2 = script.loc.start.offset
|
||||
const e2 = script.loc.end.offset
|
||||
if (startOffset < s2) {
|
||||
// <script setup> before <script>
|
||||
s.remove(endOffset, s2)
|
||||
s.remove(e2, source.length)
|
||||
} else {
|
||||
// <script> before <script setup>
|
||||
s.remove(0, s2)
|
||||
s.remove(e2, startOffset)
|
||||
s.remove(endOffset, source.length)
|
||||
}
|
||||
} else {
|
||||
// only <script setup>
|
||||
s.remove(0, startOffset)
|
||||
s.remove(endOffset, source.length)
|
||||
}
|
||||
|
||||
// wrap setup code with function
|
||||
// determine the argument signature.
|
||||
const args =
|
||||
typeof scriptSetup.setup === 'string'
|
||||
? scriptSetup.setup
|
||||
: // TODO should we force explicit args signature?
|
||||
`$props, { attrs: $attrs, slots: $slots, emit: $emit }`
|
||||
// export the content of <script setup> as a named export, `setup`.
|
||||
// this allows `import { setup } from '*.vue'` for testing purposes.
|
||||
s.appendLeft(startOffset, `\nexport function setup(${args}) {\n`)
|
||||
|
||||
// generate return statement
|
||||
let returned = `{ ${setupExports.join(', ')} }`
|
||||
|
||||
// handle `export * from`. We need to call `toRefs` on the imported module
|
||||
// object before merging.
|
||||
if (exportAllIndex > 0) {
|
||||
s.prepend(`import { toRefs as __toRefs__ } from 'vue'\n`)
|
||||
for (let i = 0; i < exportAllIndex; i++) {
|
||||
returned += `,\n __toRefs__(__export_all_${i}__)`
|
||||
}
|
||||
returned = `Object.assign(\n ${returned}\n)`
|
||||
}
|
||||
|
||||
s.appendRight(
|
||||
endOffset,
|
||||
`\nreturn ${returned}\n}\n\nexport default { setup }\n`
|
||||
)
|
||||
|
||||
s.trim()
|
||||
|
||||
setupExports.forEach(key => {
|
||||
bindings[key] = 'setup'
|
||||
})
|
||||
|
||||
return {
|
||||
bindings,
|
||||
code: s.toString(),
|
||||
map: s.generateMap({
|
||||
source: filename,
|
||||
includeContent: true
|
||||
}) as SourceMap
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze bindings in normal `<script>`
|
||||
* Note that `compileScriptSetup` already analyzes bindings as part of its
|
||||
* compilation process so this should only be used on single `<script>` SFCs.
|
||||
*/
|
||||
export function analyzeScriptBindings(
|
||||
_script: SFCScriptBlock
|
||||
): BindingMetadata {
|
||||
return {}
|
||||
}
|
||||
|
||||
function walkObjectPattern(_node: ObjectPattern, _setupExports: string[]) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
function walkArrayPattern(_node: ArrayPattern, _setupExports: string[]) {
|
||||
// TODO
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
export { parse } from './parse'
|
||||
export { compileTemplate } from './compileTemplate'
|
||||
export { compileStyle, compileStyleAsync } from './compileStyle'
|
||||
export { compileScriptSetup, analyzeScriptBindings } from './compileScript'
|
||||
|
||||
// Types
|
||||
export {
|
||||
|
||||
@@ -35,7 +35,7 @@ export interface SFCTemplateBlock extends SFCBlock {
|
||||
|
||||
export interface SFCScriptBlock extends SFCBlock {
|
||||
type: 'script'
|
||||
setup?: boolean
|
||||
setup?: boolean | string
|
||||
}
|
||||
|
||||
export interface SFCStyleBlock extends SFCBlock {
|
||||
@@ -46,6 +46,7 @@ export interface SFCStyleBlock extends SFCBlock {
|
||||
|
||||
export interface SFCDescriptor {
|
||||
filename: string
|
||||
source: string
|
||||
template: SFCTemplateBlock | null
|
||||
script: SFCScriptBlock | null
|
||||
scriptSetup: SFCScriptBlock | null
|
||||
@@ -86,6 +87,7 @@ export function parse(
|
||||
|
||||
const descriptor: SFCDescriptor = {
|
||||
filename,
|
||||
source,
|
||||
template: null,
|
||||
script: null,
|
||||
scriptSetup: null,
|
||||
@@ -152,7 +154,7 @@ export function parse(
|
||||
descriptor.script = block
|
||||
break
|
||||
}
|
||||
warnDuplicateBlock(source, filename, node, block.setup)
|
||||
warnDuplicateBlock(source, filename, node, !!block.setup)
|
||||
break
|
||||
case 'style':
|
||||
descriptor.styles.push(createBlock(node, source, pad) as SFCStyleBlock)
|
||||
@@ -251,7 +253,7 @@ function createBlock(
|
||||
} else if (type === 'template' && p.name === 'functional') {
|
||||
;(block as SFCTemplateBlock).functional = true
|
||||
} else if (type === 'script' && p.name === 'setup') {
|
||||
;(block as SFCScriptBlock).setup = true
|
||||
;(block as SFCScriptBlock).setup = attrs.setup || true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user