提交 fe038fa6 编写于 作者: fxy060608's avatar fxy060608

wip(uvue): setup

上级 eea2bb42
...@@ -58,5 +58,17 @@ declare namespace NodeJS { ...@@ -58,5 +58,17 @@ declare namespace NodeJS {
UNI_APP_X_PAGE_COUNT: string UNI_APP_X_PAGE_COUNT: string
UNI_APP_X_TSC?: string UNI_APP_X_TSC?: string
UNI_APP_X_SINGLE_THREAD?: string UNI_APP_X_SINGLE_THREAD?: string
UNI_APP_X_SETUP?: string
} }
} }
declare module 'estree-walker' {
export function walk<T>(
root: T,
options: {
enter?: (node: T, parent: T | undefined) => any
leave?: (node: T, parent: T | undefined) => any
exit?: (node: T) => any
} & ThisType<{ skip: () => void }>
)
}
...@@ -28,12 +28,14 @@ ...@@ -28,12 +28,14 @@
"@jridgewell/trace-mapping": "^0.3.19", "@jridgewell/trace-mapping": "^0.3.19",
"@rollup/pluginutils": "^4.2.0", "@rollup/pluginutils": "^4.2.0",
"@vue/compiler-core": "3.3.11", "@vue/compiler-core": "3.3.11",
"@vue/compiler-dom": "3.3.11",
"@vue/compiler-sfc": "3.3.11", "@vue/compiler-sfc": "3.3.11",
"@vue/shared": "3.3.11", "@vue/shared": "3.3.11",
"debug": "^4.3.3", "debug": "^4.3.3",
"es-module-lexer": "^1.2.1", "es-module-lexer": "^1.2.1",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"fs-extra": "^10.0.0", "fs-extra": "^10.0.0",
"magic-string": "^0.30.0",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"source-map-js": "^1.0.2", "source-map-js": "^1.0.2",
"unplugin-auto-import": "^0.16.7" "unplugin-auto-import": "^0.16.7"
......
...@@ -4,7 +4,7 @@ import { ResolvedOptions, setSrcDescriptor } from '../descriptorCache' ...@@ -4,7 +4,7 @@ import { ResolvedOptions, setSrcDescriptor } from '../descriptorCache'
export function genStyle( export function genStyle(
_: SFCDescriptor, _: SFCDescriptor,
{ className }: { className: string; filename: string } { className }: { className: string }
) { ) {
return `/*${className}Styles*/` return `/*${className}Styles*/`
} }
......
...@@ -19,3 +19,5 @@ export function genTemplate( ...@@ -19,3 +19,5 @@ export function genTemplate(
} }
return compile(template.content, options) return compile(template.content, options)
} }
export const genTemplateCode = genTemplate
...@@ -19,8 +19,7 @@ export interface SFCParseResult { ...@@ -19,8 +19,7 @@ export interface SFCParseResult {
errors: Array<CompilerError | SyntaxError> errors: Array<CompilerError | SyntaxError>
} }
const cache = new Map<string, SFCDescriptor>() export const cache = new Map<string, SFCDescriptor>()
const prevCache = new Map<string, SFCDescriptor | undefined>()
declare module '@vue/compiler-sfc' { declare module '@vue/compiler-sfc' {
interface SFCDescriptor { interface SFCDescriptor {
...@@ -33,33 +32,25 @@ export function createDescriptor( ...@@ -33,33 +32,25 @@ export function createDescriptor(
source: string, source: string,
{ root, sourceMap, compiler }: ResolvedOptions { root, sourceMap, compiler }: ResolvedOptions
): SFCParseResult { ): SFCParseResult {
const { descriptor, errors } = compiler.parse(source, {
filename,
sourceMap,
})
// ensure the path is normalized in a way that is consistent inside // ensure the path is normalized in a way that is consistent inside
// project (relative to root) and on different systems. // project (relative to root) and on different systems.
const normalizedPath = normalizePath( const normalizedPath = normalizePath(
path.normalize(path.relative(root, filename)) path.normalize(path.relative(root, filename))
) )
// 传入normalizedPath是为了让sourcemap记录的是相对路径
const { descriptor, errors } = compiler.parse(source, {
filename: normalizedPath,
sourceMap,
})
// 重置为绝对路径
descriptor.filename = filename
descriptor.id = getHash(normalizedPath) descriptor.id = getHash(normalizedPath)
cache.set(filename, descriptor) cache.set(filename, descriptor)
return { descriptor, errors } return { descriptor, errors }
} }
export function getPrevDescriptor(filename: string): SFCDescriptor | undefined {
return prevCache.get(filename)
}
export function setPrevDescriptor(
filename: string,
entry: SFCDescriptor
): void {
prevCache.set(filename, entry)
}
export function getDescriptor( export function getDescriptor(
filename: string, filename: string,
options: ResolvedOptions, options: ResolvedOptions,
......
...@@ -58,13 +58,14 @@ import { genTemplate } from './code/template' ...@@ -58,13 +58,14 @@ import { genTemplate } from './code/template'
import { genJsStylesCode, genStyle, transformStyle } from './code/style' import { genJsStylesCode, genStyle, transformStyle } from './code/style'
import { genComponentPublicInstanceImported } from './compiler/utils' import { genComponentPublicInstanceImported } from './compiler/utils'
import { ImportItem } from './compiler/transform' import { ImportItem } from './compiler/transform'
import { transformMain } from './sfc/main'
export function uniAppUVuePlugin(opts: { export function uniAppUVuePlugin(opts: {
autoImportOptions?: AutoImportOptions autoImportOptions?: AutoImportOptions
}): Plugin { }): Plugin {
const options: ResolvedOptions = { const options: ResolvedOptions = {
root: process.env.UNI_INPUT_DIR, root: process.env.UNI_INPUT_DIR,
sourceMap: false, sourceMap: process.env.NODE_ENV === 'development',
// eslint-disable-next-line no-restricted-globals // eslint-disable-next-line no-restricted-globals
compiler: require('@vue/compiler-sfc'), compiler: require('@vue/compiler-sfc'),
targetLanguage: process.env.UNI_UTS_TARGET_LANGUAGE, targetLanguage: process.env.UNI_UTS_TARGET_LANGUAGE,
...@@ -118,6 +119,9 @@ export function uniAppUVuePlugin(opts: { ...@@ -118,6 +119,9 @@ export function uniAppUVuePlugin(opts: {
} }
if (!query.vue) { if (!query.vue) {
// main request // main request
if (process.env.UNI_APP_X_SETUP === 'true') {
return transformMain(code, filename, options, this)
}
const { errors, uts, js, sourceMap } = await transformVue( const { errors, uts, js, sourceMap } = await transformVue(
code, code,
filename, filename,
...@@ -147,7 +151,7 @@ export function uniAppUVuePlugin(opts: { ...@@ -147,7 +151,7 @@ export function uniAppUVuePlugin(opts: {
if (sourceMap) { if (sourceMap) {
this.emitFile({ this.emitFile({
type: 'asset', type: 'asset',
fileName: removeExt(fileName) + '.template.map', fileName: removeExt(fileName) + '.map',
source: JSON.stringify(sourceMap), source: JSON.stringify(sourceMap),
}) })
} }
...@@ -364,7 +368,7 @@ export async function transformVue( ...@@ -364,7 +368,7 @@ export async function transformVue(
genScript(descriptor, { filename: className }) + genScript(descriptor, { filename: className }) +
templateCode + templateCode +
'\n' + '\n' +
genStyle(descriptor, { filename: relativeFileName, className }) + genStyle(descriptor, { className }) +
'\n' '\n'
let jsCodes = [ let jsCodes = [
......
import { parse } from '@babel/parser'
import MagicString from 'magic-string'
import type { ParserPlugin } from '@babel/parser'
import type { Identifier, Statement } from '@babel/types'
export function rewriteDefault(
input: string,
as: string,
parserPlugins?: ParserPlugin[]
): string {
const ast = parse(input, {
sourceType: 'module',
plugins: parserPlugins,
}).program.body
const s = new MagicString(input)
rewriteDefaultAST(ast, s, as)
return s.toString()
}
/**
* Utility for rewriting `export default` in a script block into a variable
* declaration so that we can inject things into it
*/
export function rewriteDefaultAST(
ast: Statement[],
s: MagicString,
as: string
): void {
if (!hasDefaultExport(ast)) {
s.append(`\nconst ${as} = {}`)
return
}
// if the script somehow still contains `default export`, it probably has
// multi-line comments or template strings. fallback to a full parse.
ast.forEach((node) => {
if (node.type === 'ExportDefaultDeclaration') {
if (node.declaration.type === 'ClassDeclaration' && node.declaration.id) {
let start: number =
node.declaration.decorators && node.declaration.decorators.length > 0
? node.declaration.decorators[
node.declaration.decorators.length - 1
].end!
: node.start!
s.overwrite(start, node.declaration.id.start!, ` class `)
s.append(`\nconst ${as} = ${node.declaration.id.name}`)
} else {
s.overwrite(node.start!, node.declaration.start!, `const ${as} = `)
}
} else 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') {
s.prepend(
`import { default as __VUE_DEFAULT__ } from '${node.source.value}'\n`
)
const end = specifierEnd(s, specifier.local.end!, node.end!)
s.remove(specifier.start!, end)
s.append(`\nconst ${as} = __VUE_DEFAULT__`)
continue
} else {
s.prepend(
`import { ${s.slice(
specifier.local.start!,
specifier.local.end!
)} as __VUE_DEFAULT__ } from '${node.source.value}'\n`
)
const end = specifierEnd(s, specifier.exported.end!, node.end!)
s.remove(specifier.start!, end)
s.append(`\nconst ${as} = __VUE_DEFAULT__`)
continue
}
}
const end = specifierEnd(s, specifier.end!, node.end!)
s.remove(specifier.start!, end)
s.append(`\nconst ${as} = ${specifier.local.name}`)
}
}
}
})
}
export function hasDefaultExport(ast: Statement[]): boolean {
for (const stmt of ast) {
if (stmt.type === 'ExportDefaultDeclaration') {
return true
} else if (
stmt.type === 'ExportNamedDeclaration' &&
stmt.specifiers.some(
(spec) => (spec.exported as Identifier).name === 'default'
)
) {
return true
}
}
return false
}
function specifierEnd(s: MagicString, end: number, nodeEnd: number | null) {
// export { default , foo } ...
let hasCommas = false
let oldEnd = end
while (end < nodeEnd!) {
if (/\s/.test(s.slice(end, end + 1))) {
end++
} else if (s.slice(end, end + 1) === ',') {
end++
hasCommas = true
break
} else if (s.slice(end, end + 1) === '}') {
break
}
}
return hasCommas ? end : oldEnd
}
import {
ArrayExpression,
Node,
ObjectExpression,
Statement,
} from '@babel/types'
import { BindingMetadata, BindingTypes } from '@vue/compiler-dom'
import { resolveObjectKey } from './utils'
/**
* 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(ast: Statement[]): BindingMetadata {
for (const node of ast) {
if (
node.type === 'ExportDefaultDeclaration' &&
node.declaration.type === 'ObjectExpression'
) {
return analyzeBindingsFromOptions(node.declaration)
}
}
return {}
}
function analyzeBindingsFromOptions(node: ObjectExpression): BindingMetadata {
const bindings: BindingMetadata = {}
// #3270, #3275
// mark non-script-setup so we don't resolve components/directives from these
Object.defineProperty(bindings, '__isScriptSetup', {
enumerable: false,
value: false,
})
for (const property of node.properties) {
if (
property.type === 'ObjectProperty' &&
!property.computed &&
property.key.type === 'Identifier'
) {
// props
if (property.key.name === 'props') {
// props: ['foo']
// props: { foo: ... }
for (const key of getObjectOrArrayExpressionKeys(property.value)) {
bindings[key] = BindingTypes.PROPS
}
}
// inject
else if (property.key.name === 'inject') {
// inject: ['foo']
// inject: { foo: {} }
for (const key of getObjectOrArrayExpressionKeys(property.value)) {
bindings[key] = BindingTypes.OPTIONS
}
}
// computed & methods
else if (
property.value.type === 'ObjectExpression' &&
(property.key.name === 'computed' || property.key.name === 'methods')
) {
// methods: { foo() {} }
// computed: { foo() {} }
for (const key of getObjectExpressionKeys(property.value)) {
bindings[key] = BindingTypes.OPTIONS
}
}
}
// setup & data
else if (
property.type === 'ObjectMethod' &&
property.key.type === 'Identifier' &&
(property.key.name === 'setup' || property.key.name === 'data')
) {
for (const bodyItem of property.body.body) {
// setup() {
// return {
// foo: null
// }
// }
if (
bodyItem.type === 'ReturnStatement' &&
bodyItem.argument &&
bodyItem.argument.type === 'ObjectExpression'
) {
for (const key of getObjectExpressionKeys(bodyItem.argument)) {
bindings[key] =
property.key.name === 'setup'
? BindingTypes.SETUP_MAYBE_REF
: BindingTypes.DATA
}
}
}
}
}
return bindings
}
function getObjectExpressionKeys(node: ObjectExpression): string[] {
const keys = []
for (const prop of node.properties) {
if (prop.type === 'SpreadElement') continue
const key = resolveObjectKey(prop.key, prop.computed)
if (key) keys.push(String(key))
}
return keys
}
function getArrayExpressionKeys(node: ArrayExpression): string[] {
const keys = []
for (const element of node.elements) {
if (element && element.type === 'StringLiteral') {
keys.push(element.value)
}
}
return keys
}
export function getObjectOrArrayExpressionKeys(value: Node): string[] {
if (value.type === 'ArrayExpression') {
return getArrayExpressionKeys(value)
}
if (value.type === 'ObjectExpression') {
return getObjectExpressionKeys(value)
}
return []
}
import { CallExpression, Node, ObjectPattern, Program } from '@babel/types'
import { SFCDescriptor } from '@vue/compiler-sfc'
import { generateCodeFrame } from '@vue/shared'
import { parse as babelParse, ParserPlugin } from '@babel/parser'
import { ImportBinding, SFCScriptCompileOptions } from '../compileScript'
import { PropsDestructureBindings } from './defineProps'
import { ModelDecl } from './defineModel'
import type { BindingMetadata } from '@vue/compiler-core'
import MagicString from 'magic-string'
import { TypeScope } from './resolveType'
export class ScriptCompileContext {
scriptAst: Program | null
scriptSetupAst: Program | null
source = this.descriptor.source
filename = this.descriptor.filename
s = new MagicString(this.source)
startOffset = this.descriptor.scriptSetup?.loc.start.offset
endOffset = this.descriptor.scriptSetup?.loc.end.offset
// import / type analysis
scope?: TypeScope
globalScopes?: TypeScope[]
userImports: Record<string, ImportBinding> = Object.create(null)
// macros presence check
hasDefinePropsCall = false
hasDefineEmitCall = false
hasDefineExposeCall = false
hasDefaultExportName = false
hasDefaultExportRender = false
hasDefineOptionsCall = false
hasDefineSlotsCall = false
hasDefineModelCall = false
// defineProps
propsCall: CallExpression | undefined
propsDecl: Node | undefined
propsRuntimeDecl: Node | undefined
propsTypeDecl: Node | undefined
propsDestructureDecl: ObjectPattern | undefined
propsDestructuredBindings: PropsDestructureBindings = Object.create(null)
propsDestructureRestId: string | undefined
propsRuntimeDefaults: Node | undefined
// defineEmits
emitsRuntimeDecl: Node | undefined
emitsTypeDecl: Node | undefined
emitDecl: Node | undefined
// defineModel
modelDecls: Record<string, ModelDecl> = Object.create(null)
// defineOptions
optionsRuntimeDecl: Node | undefined
// codegen
bindingMetadata: BindingMetadata = {}
helperImports: Set<string> = new Set()
helper(key: string): string {
this.helperImports.add(key)
return `_${key}`
}
/**
* to be exposed on compiled script block for HMR cache busting
*/
deps?: Set<string>
/**
* cache for resolved fs
*/
fs?: NonNullable<SFCScriptCompileOptions['fs']>
constructor(
public descriptor: SFCDescriptor,
public options: Partial<SFCScriptCompileOptions>
) {
// resolve parser plugins
const plugins: ParserPlugin[] = resolveParserPlugins(
options.babelParserPlugins
)
function parse(input: string, offset: number): Program {
try {
return babelParse(input, {
plugins,
sourceType: 'module',
}).program
} catch (e: any) {
e.message = `[vue/compiler-sfc] ${e.message}\n\n${
descriptor.filename
}\n${generateCodeFrame(
descriptor.source,
e.pos + offset,
e.pos + offset + 1
)}`
throw e
}
}
this.scriptAst =
descriptor.script &&
parse(descriptor.script.content, descriptor.script.loc.start.offset)
this.scriptSetupAst =
descriptor.scriptSetup &&
parse(descriptor.scriptSetup!.content, this.startOffset!)
}
getString(node: Node, scriptSetup = true): string {
const block = scriptSetup
? this.descriptor.scriptSetup!
: this.descriptor.script!
return block.content.slice(node.start!, node.end!)
}
error(msg: string, node: Node, scope?: TypeScope): never {
const offset = scope ? scope.offset : this.startOffset!
throw new Error(
`[@vue/compiler-sfc] ${msg}\n\n${
(scope || this.descriptor).filename
}\n${generateCodeFrame(
(scope || this.descriptor).source,
node.start! + offset,
node.end! + offset
)}`
)
}
}
export function resolveParserPlugins(
userPlugins?: ParserPlugin[],
dts = false
) {
const plugins: ParserPlugin[] = []
if (userPlugins) {
// If don't match the case of adding jsx
// should remove the jsx from user options
userPlugins = userPlugins.filter((p) => p !== 'jsx')
}
plugins.push(['typescript', { dts }])
if (!userPlugins || !userPlugins.includes('decorators')) {
plugins.push('decorators-legacy')
}
if (userPlugins) {
plugins.push(...userPlugins)
}
return plugins
}
import {
ArrayPattern,
Identifier,
LVal,
Node,
ObjectPattern,
RestElement,
} from '@babel/types'
import { isCallOf } from './utils'
import { ScriptCompileContext } from './context'
import { resolveTypeElements, resolveUnionType } from './resolveType'
export const DEFINE_EMITS = 'defineEmits'
export function processDefineEmits(
ctx: ScriptCompileContext,
node: Node,
declId?: LVal
): boolean {
if (!isCallOf(node, DEFINE_EMITS)) {
return false
}
if (ctx.hasDefineEmitCall) {
ctx.error(`duplicate ${DEFINE_EMITS}() call`, node)
}
ctx.hasDefineEmitCall = true
ctx.emitsRuntimeDecl = node.arguments[0]
if (node.typeParameters) {
if (ctx.emitsRuntimeDecl) {
ctx.error(
`${DEFINE_EMITS}() cannot accept both type and non-type arguments ` +
`at the same time. Use one or the other.`,
node
)
}
ctx.emitsTypeDecl = node.typeParameters.params[0]
}
ctx.emitDecl = declId
return true
}
export function genRuntimeEmits(ctx: ScriptCompileContext): string | undefined {
let emitsDecl = ''
if (ctx.emitsRuntimeDecl) {
emitsDecl = ctx.getString(ctx.emitsRuntimeDecl).trim()
} else if (ctx.emitsTypeDecl) {
const typeDeclaredEmits = extractRuntimeEmits(ctx)
emitsDecl = typeDeclaredEmits.size
? `[${Array.from(typeDeclaredEmits)
.map((k) => JSON.stringify(k))
.join(', ')}]`
: ``
}
if (ctx.hasDefineModelCall) {
let modelEmitsDecl = `[${Object.keys(ctx.modelDecls)
.map((n) => JSON.stringify(`update:${n}`))
.join(', ')}]`
emitsDecl = emitsDecl
? `/*#__PURE__*/${ctx.helper(
'mergeModels'
)}(${emitsDecl}, ${modelEmitsDecl})`
: modelEmitsDecl
}
return emitsDecl
}
function extractRuntimeEmits(ctx: ScriptCompileContext): Set<string> {
const emits = new Set<string>()
const node = ctx.emitsTypeDecl!
if (node.type === 'TSFunctionType') {
extractEventNames(ctx, node.parameters[0], emits)
return emits
}
const { props, calls } = resolveTypeElements(ctx, node)
let hasProperty = false
for (const key in props) {
emits.add(key)
hasProperty = true
}
if (calls) {
if (hasProperty) {
ctx.error(
`defineEmits() type cannot mixed call signature and property syntax.`,
node
)
}
for (const call of calls) {
extractEventNames(ctx, call.parameters[0], emits)
}
}
return emits
}
function extractEventNames(
ctx: ScriptCompileContext,
eventName: ArrayPattern | Identifier | ObjectPattern | RestElement,
emits: Set<string>
) {
if (
eventName.type === 'Identifier' &&
eventName.typeAnnotation &&
eventName.typeAnnotation.type === 'TSTypeAnnotation'
) {
const types = resolveUnionType(ctx, eventName.typeAnnotation.typeAnnotation)
for (const type of types) {
if (type.type === 'TSLiteralType') {
if (
type.literal.type !== 'UnaryExpression' &&
type.literal.type !== 'TemplateLiteral'
) {
emits.add(String(type.literal.value))
}
}
}
}
}
import { Node } from '@babel/types'
import { isCallOf } from './utils'
import { ScriptCompileContext } from './context'
export const DEFINE_EXPOSE = 'defineExpose'
export function processDefineExpose(
ctx: ScriptCompileContext,
node: Node
): boolean {
if (isCallOf(node, DEFINE_EXPOSE)) {
if (ctx.hasDefineExposeCall) {
ctx.error(`duplicate ${DEFINE_EXPOSE}() call`, node)
}
ctx.hasDefineExposeCall = true
return true
}
return false
}
import { LVal, Node, ObjectProperty, TSType } from '@babel/types'
import { ScriptCompileContext } from './context'
import { inferRuntimeType } from './resolveType'
import {
UNKNOWN_TYPE,
concatStrings,
isCallOf,
toRuntimeTypeString,
unwrapTSNode,
} from './utils'
import { BindingTypes } from '@vue/compiler-dom'
import { warnOnce } from '../warn'
export const DEFINE_MODEL = 'defineModel'
export interface ModelDecl {
type: TSType | undefined
options: string | undefined
identifier: string | undefined
}
export function processDefineModel(
ctx: ScriptCompileContext,
node: Node,
declId?: LVal
): boolean {
if (!isCallOf(node, DEFINE_MODEL)) {
return false
}
if (!ctx.options.defineModel) {
warnOnce(
`defineModel() is an experimental feature and disabled by default.\n` +
`To enable it, follow the RFC at https://github.com/vuejs/rfcs/discussions/503.`
)
return false
}
warnOnce(
`This project is using defineModel(), which is an experimental ` +
`feature. It may receive breaking changes or be removed in the future, so ` +
`use at your own risk.\n` +
`To stay updated, follow the RFC at https://github.com/vuejs/rfcs/discussions/503.`
)
ctx.hasDefineModelCall = true
const type =
(node.typeParameters && node.typeParameters.params[0]) || undefined
let modelName: string
let options: Node | undefined
const arg0 = node.arguments[0] && unwrapTSNode(node.arguments[0])
if (arg0 && arg0.type === 'StringLiteral') {
modelName = arg0.value
options = node.arguments[1]
} else {
modelName = 'modelValue'
options = arg0
}
if (ctx.modelDecls[modelName]) {
ctx.error(`duplicate model name ${JSON.stringify(modelName)}`, node)
}
const optionsString = options && ctx.getString(options)
ctx.modelDecls[modelName] = {
type,
options: optionsString,
identifier:
declId && declId.type === 'Identifier' ? declId.name : undefined,
}
// register binding type
ctx.bindingMetadata[modelName] = BindingTypes.PROPS
let runtimeOptions = ''
if (options) {
if (options.type === 'ObjectExpression') {
const local = options.properties.find(
(p) =>
p.type === 'ObjectProperty' &&
((p.key.type === 'Identifier' && p.key.name === 'local') ||
(p.key.type === 'StringLiteral' && p.key.value === 'local'))
) as ObjectProperty
if (local) {
runtimeOptions = `{ ${ctx.getString(local)} }`
} else {
for (const p of options.properties) {
if (p.type === 'SpreadElement' || p.computed) {
runtimeOptions = optionsString!
break
}
}
}
} else {
runtimeOptions = optionsString!
}
}
ctx.s.overwrite(
ctx.startOffset! + node.start!,
ctx.startOffset! + node.end!,
`${ctx.helper('useModel')}(__props, ${JSON.stringify(modelName)}${
runtimeOptions ? `, ${runtimeOptions}` : ``
})`
)
return true
}
export function genModelProps(ctx: ScriptCompileContext) {
if (!ctx.hasDefineModelCall) return
const isProd = !!ctx.options.isProd
let modelPropsDecl = ''
for (const [name, { type, options }] of Object.entries(ctx.modelDecls)) {
let skipCheck = false
let runtimeTypes = type && inferRuntimeType(ctx, type)
if (runtimeTypes) {
const hasUnknownType = runtimeTypes.includes(UNKNOWN_TYPE)
runtimeTypes = runtimeTypes.filter((el) => {
if (el === UNKNOWN_TYPE) return false
return isProd
? el === 'Boolean' || (el === 'Function' && options)
: true
})
skipCheck = !isProd && hasUnknownType && runtimeTypes.length > 0
}
let runtimeType =
(runtimeTypes &&
runtimeTypes.length > 0 &&
toRuntimeTypeString(runtimeTypes)) ||
undefined
const codegenOptions = concatStrings([
runtimeType && `type: ${runtimeType}`,
skipCheck && 'skipCheck: true',
])
let decl: string
if (runtimeType && options) {
decl = `{ ${codegenOptions}, ...${options} }`
} else {
decl = options || (runtimeType ? `{ ${codegenOptions} }` : '{}')
}
modelPropsDecl += `\n ${JSON.stringify(name)}: ${decl},`
}
return `{${modelPropsDecl}\n }`
}
import { Node } from '@babel/types'
import { ScriptCompileContext } from './context'
import { isCallOf, unwrapTSNode } from './utils'
import { DEFINE_PROPS } from './defineProps'
import { DEFINE_EMITS } from './defineEmits'
import { DEFINE_EXPOSE } from './defineExpose'
import { DEFINE_SLOTS } from './defineSlots'
export const DEFINE_OPTIONS = 'defineOptions'
export function processDefineOptions(
ctx: ScriptCompileContext,
node: Node
): boolean {
if (!isCallOf(node, DEFINE_OPTIONS)) {
return false
}
if (ctx.hasDefineOptionsCall) {
ctx.error(`duplicate ${DEFINE_OPTIONS}() call`, node)
}
if (node.typeParameters) {
ctx.error(`${DEFINE_OPTIONS}() cannot accept type arguments`, node)
}
if (!node.arguments[0]) return true
ctx.hasDefineOptionsCall = true
ctx.optionsRuntimeDecl = unwrapTSNode(node.arguments[0])
let propsOption = undefined
let emitsOption = undefined
let exposeOption = undefined
let slotsOption = undefined
if (ctx.optionsRuntimeDecl.type === 'ObjectExpression') {
for (const prop of ctx.optionsRuntimeDecl.properties) {
if (
(prop.type === 'ObjectProperty' || prop.type === 'ObjectMethod') &&
prop.key.type === 'Identifier'
) {
if (prop.key.name === 'props') propsOption = prop
if (prop.key.name === 'emits') emitsOption = prop
if (prop.key.name === 'expose') exposeOption = prop
if (prop.key.name === 'slots') slotsOption = prop
}
}
}
if (propsOption) {
ctx.error(
`${DEFINE_OPTIONS}() cannot be used to declare props. Use ${DEFINE_PROPS}() instead.`,
propsOption
)
}
if (emitsOption) {
ctx.error(
`${DEFINE_OPTIONS}() cannot be used to declare emits. Use ${DEFINE_EMITS}() instead.`,
emitsOption
)
}
if (exposeOption) {
ctx.error(
`${DEFINE_OPTIONS}() cannot be used to declare expose. Use ${DEFINE_EXPOSE}() instead.`,
exposeOption
)
}
if (slotsOption) {
ctx.error(
`${DEFINE_OPTIONS}() cannot be used to declare slots. Use ${DEFINE_SLOTS}() instead.`,
slotsOption
)
}
return true
}
import {
Node,
LVal,
ObjectProperty,
ObjectMethod,
ObjectExpression,
Expression,
} from '@babel/types'
import { BindingTypes, isFunctionType } from '@vue/compiler-dom'
import { ScriptCompileContext } from './context'
import { inferRuntimeType, resolveTypeElements } from './resolveType'
import {
resolveObjectKey,
UNKNOWN_TYPE,
concatStrings,
isLiteralNode,
isCallOf,
unwrapTSNode,
toRuntimeTypeString,
getEscapedPropName,
} from './utils'
import { genModelProps } from './defineModel'
import { getObjectOrArrayExpressionKeys } from './analyzeScriptBindings'
import { processPropsDestructure } from './definePropsDestructure'
export const DEFINE_PROPS = 'defineProps'
export const WITH_DEFAULTS = 'withDefaults'
export interface PropTypeData {
key: string
type: string[]
required: boolean
skipCheck: boolean
}
export type PropsDestructureBindings = Record<
string, // public prop key
{
local: string // local identifier, may be different
default?: Expression
}
>
export function processDefineProps(
ctx: ScriptCompileContext,
node: Node,
declId?: LVal
) {
if (!isCallOf(node, DEFINE_PROPS)) {
return processWithDefaults(ctx, node, declId)
}
if (ctx.hasDefinePropsCall) {
ctx.error(`duplicate ${DEFINE_PROPS}() call`, node)
}
ctx.hasDefinePropsCall = true
ctx.propsRuntimeDecl = node.arguments[0]
// register bindings
if (ctx.propsRuntimeDecl) {
for (const key of getObjectOrArrayExpressionKeys(ctx.propsRuntimeDecl)) {
if (!(key in ctx.bindingMetadata)) {
ctx.bindingMetadata[key] = BindingTypes.PROPS
}
}
}
// call has type parameters - infer runtime types from it
if (node.typeParameters) {
if (ctx.propsRuntimeDecl) {
ctx.error(
`${DEFINE_PROPS}() cannot accept both type and non-type arguments ` +
`at the same time. Use one or the other.`,
node
)
}
ctx.propsTypeDecl = node.typeParameters.params[0]
}
// handle props destructure
if (declId && declId.type === 'ObjectPattern') {
processPropsDestructure(ctx, declId)
}
ctx.propsCall = node
ctx.propsDecl = declId
return true
}
function processWithDefaults(
ctx: ScriptCompileContext,
node: Node,
declId?: LVal
): boolean {
if (!isCallOf(node, WITH_DEFAULTS)) {
return false
}
if (!processDefineProps(ctx, node.arguments[0], declId)) {
ctx.error(
`${WITH_DEFAULTS}' first argument must be a ${DEFINE_PROPS} call.`,
node.arguments[0] || node
)
}
if (ctx.propsRuntimeDecl) {
ctx.error(
`${WITH_DEFAULTS} can only be used with type-based ` +
`${DEFINE_PROPS} declaration.`,
node
)
}
if (ctx.propsDestructureDecl) {
ctx.error(
`${WITH_DEFAULTS}() is unnecessary when using destructure with ${DEFINE_PROPS}().\n` +
`Prefer using destructure default values, e.g. const { foo = 1 } = defineProps(...).`,
node.callee
)
}
ctx.propsRuntimeDefaults = node.arguments[1]
if (!ctx.propsRuntimeDefaults) {
ctx.error(`The 2nd argument of ${WITH_DEFAULTS} is required.`, node)
}
ctx.propsCall = node
return true
}
export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined {
let propsDecls: undefined | string
if (ctx.propsRuntimeDecl) {
propsDecls = ctx.getString(ctx.propsRuntimeDecl).trim()
if (ctx.propsDestructureDecl) {
const defaults: string[] = []
for (const key in ctx.propsDestructuredBindings) {
const d = genDestructuredDefaultValue(ctx, key)
const finalKey = getEscapedPropName(key)
if (d)
defaults.push(
`${finalKey}: ${d.valueString}${
d.needSkipFactory ? `, __skip_${finalKey}: true` : ``
}`
)
}
if (defaults.length) {
propsDecls = `/*#__PURE__*/${ctx.helper(
`mergeDefaults`
)}(${propsDecls}, {\n ${defaults.join(',\n ')}\n})`
}
}
} else if (ctx.propsTypeDecl) {
propsDecls = genRuntimePropsFromTypes(ctx)
}
const modelsDecls = genModelProps(ctx)
if (propsDecls && modelsDecls) {
return `/*#__PURE__*/${ctx.helper(
'mergeModels'
)}(${propsDecls}, ${modelsDecls})`
} else {
return modelsDecls || propsDecls
}
}
function genRuntimePropsFromTypes(ctx: ScriptCompileContext) {
// this is only called if propsTypeDecl exists
const props = resolveRuntimePropsFromType(ctx, ctx.propsTypeDecl!)
if (!props.length) {
return
}
const propStrings: string[] = []
const hasStaticDefaults = hasStaticWithDefaults(ctx)
for (const prop of props) {
propStrings.push(genRuntimePropFromType(ctx, prop, hasStaticDefaults))
// register bindings
if (!(prop.key in ctx.bindingMetadata)) {
ctx.bindingMetadata[prop.key] = BindingTypes.PROPS
}
}
let propsDecls = `{
${propStrings.join(',\n ')}\n }`
if (ctx.propsRuntimeDefaults && !hasStaticDefaults) {
propsDecls = `/*#__PURE__*/${ctx.helper(
'mergeDefaults'
)}(${propsDecls}, ${ctx.getString(ctx.propsRuntimeDefaults)})`
}
return propsDecls
}
function resolveRuntimePropsFromType(
ctx: ScriptCompileContext,
node: Node
): PropTypeData[] {
const props: PropTypeData[] = []
const elements = resolveTypeElements(ctx, node)
for (const key in elements.props) {
const e = elements.props[key]
let type = inferRuntimeType(ctx, e)
let skipCheck = false
// skip check for result containing unknown types
if (type.includes(UNKNOWN_TYPE)) {
if (type.includes('Boolean') || type.includes('Function')) {
type = type.filter((t) => t !== UNKNOWN_TYPE)
skipCheck = true
} else {
type = ['null']
}
}
props.push({
key,
required: !e.optional,
type: type || [`null`],
skipCheck,
})
}
return props
}
function genRuntimePropFromType(
ctx: ScriptCompileContext,
{ key, required, type, skipCheck }: PropTypeData,
hasStaticDefaults: boolean
): string {
let defaultString: string | undefined
const destructured = genDestructuredDefaultValue(ctx, key, type)
if (destructured) {
defaultString = `default: ${destructured.valueString}${
destructured.needSkipFactory ? `, skipFactory: true` : ``
}`
} else if (hasStaticDefaults) {
const prop = (ctx.propsRuntimeDefaults as ObjectExpression).properties.find(
(node) => {
if (node.type === 'SpreadElement') return false
return resolveObjectKey(node.key, node.computed) === key
}
) as ObjectProperty | ObjectMethod
if (prop) {
if (prop.type === 'ObjectProperty') {
// prop has corresponding static default value
defaultString = `default: ${ctx.getString(prop.value)}`
} else {
defaultString = `${prop.async ? 'async ' : ''}${
prop.kind !== 'method' ? `${prop.kind} ` : ''
}default() ${ctx.getString(prop.body)}`
}
}
}
const finalKey = getEscapedPropName(key)
if (!ctx.options.isProd) {
return `${finalKey}: { ${concatStrings([
`type: ${toRuntimeTypeString(type)}`,
`required: ${required}`,
skipCheck && 'skipCheck: true',
defaultString,
])} }`
} else if (
type.some(
(el) =>
el === 'Boolean' ||
((!hasStaticDefaults || defaultString) && el === 'Function')
)
) {
// #4783 for boolean, should keep the type
// #7111 for function, if default value exists or it's not static, should keep it
// in production
return `${finalKey}: { ${concatStrings([
`type: ${toRuntimeTypeString(type)}`,
defaultString,
])} }`
} else {
// production: checks are useless
return `${finalKey}: ${defaultString ? `{ ${defaultString} }` : `{}`}`
}
}
/**
* check defaults. If the default object is an object literal with only
* static properties, we can directly generate more optimized default
* declarations. Otherwise we will have to fallback to runtime merging.
*/
function hasStaticWithDefaults(ctx: ScriptCompileContext) {
return !!(
ctx.propsRuntimeDefaults &&
ctx.propsRuntimeDefaults.type === 'ObjectExpression' &&
ctx.propsRuntimeDefaults.properties.every(
(node) =>
node.type !== 'SpreadElement' &&
(!node.computed || node.key.type.endsWith('Literal'))
)
)
}
function genDestructuredDefaultValue(
ctx: ScriptCompileContext,
key: string,
inferredType?: string[]
):
| {
valueString: string
needSkipFactory: boolean
}
| undefined {
const destructured = ctx.propsDestructuredBindings[key]
const defaultVal = destructured && destructured.default
if (defaultVal) {
const value = ctx.getString(defaultVal)
const unwrapped = unwrapTSNode(defaultVal)
if (inferredType && inferredType.length && !inferredType.includes('null')) {
const valueType = inferValueType(unwrapped)
if (valueType && !inferredType.includes(valueType)) {
ctx.error(
`Default value of prop "${key}" does not match declared type.`,
unwrapped
)
}
}
// If the default value is a function or is an identifier referencing
// external value, skip factory wrap. This is needed when using
// destructure w/ runtime declaration since we cannot safely infer
// whether the expected runtime prop type is `Function`.
const needSkipFactory =
!inferredType &&
(isFunctionType(unwrapped) || unwrapped.type === 'Identifier')
const needFactoryWrap =
!needSkipFactory &&
!isLiteralNode(unwrapped) &&
!inferredType?.includes('Function')
return {
valueString: needFactoryWrap ? `() => (${value})` : value,
needSkipFactory,
}
}
}
// non-comprehensive, best-effort type infernece for a runtime value
// this is used to catch default value / type declaration mismatches
// when using props destructure.
function inferValueType(node: Node): string | undefined {
switch (node.type) {
case 'StringLiteral':
return 'String'
case 'NumericLiteral':
return 'Number'
case 'BooleanLiteral':
return 'Boolean'
case 'ObjectExpression':
return 'Object'
case 'ArrayExpression':
return 'Array'
case 'FunctionExpression':
case 'ArrowFunctionExpression':
return 'Function'
}
}
import {
Node,
Identifier,
BlockStatement,
Program,
VariableDeclaration,
ObjectPattern,
Expression,
} from '@babel/types'
import { walk } from 'estree-walker'
import {
BindingTypes,
extractIdentifiers,
isFunctionType,
isInDestructureAssignment,
isReferencedIdentifier,
isStaticProperty,
walkFunctionParams,
} from '@vue/compiler-dom'
import { genPropsAccessExp } from '@vue/shared'
import { isCallOf, resolveObjectKey, unwrapTSNode } from './utils'
import { ScriptCompileContext } from './context'
import { DEFINE_PROPS } from './defineProps'
import { warnOnce } from '../warn'
export function processPropsDestructure(
ctx: ScriptCompileContext,
declId: ObjectPattern
) {
if (!ctx.options.propsDestructure && !ctx.options.reactivityTransform) {
return
}
warnOnce(
`This project is using reactive props destructure, which is an experimental ` +
`feature. It may receive breaking changes or be removed in the future, so ` +
`use at your own risk.\n` +
`To stay updated, follow the RFC at https://github.com/vuejs/rfcs/discussions/502.`
)
ctx.propsDestructureDecl = declId
const registerBinding = (
key: string,
local: string,
defaultValue?: Expression
) => {
ctx.propsDestructuredBindings[key] = { local, default: defaultValue }
if (local !== key) {
ctx.bindingMetadata[local] = BindingTypes.PROPS_ALIASED
;(ctx.bindingMetadata.__propsAliases ||
(ctx.bindingMetadata.__propsAliases = {}))[local] = key
}
}
for (const prop of declId.properties) {
if (prop.type === 'ObjectProperty') {
const propKey = resolveObjectKey(prop.key, prop.computed)
if (!propKey) {
ctx.error(
`${DEFINE_PROPS}() destructure cannot use computed key.`,
prop.key
)
}
if (prop.value.type === 'AssignmentPattern') {
// default value { foo = 123 }
const { left, right } = prop.value
if (left.type !== 'Identifier') {
ctx.error(
`${DEFINE_PROPS}() destructure does not support nested patterns.`,
left
)
}
registerBinding(propKey, left.name, right)
} else if (prop.value.type === 'Identifier') {
// simple destructure
registerBinding(propKey, prop.value.name)
} else {
ctx.error(
`${DEFINE_PROPS}() destructure does not support nested patterns.`,
prop.value
)
}
} else {
// rest spread
ctx.propsDestructureRestId = (prop.argument as Identifier).name
// register binding
ctx.bindingMetadata[ctx.propsDestructureRestId] =
BindingTypes.SETUP_REACTIVE_CONST
}
}
}
/**
* true -> prop binding
* false -> local binding
*/
type Scope = Record<string, boolean>
export function transformDestructuredProps(
ctx: ScriptCompileContext,
vueImportAliases: Record<string, string>
) {
if (!ctx.options.propsDestructure && !ctx.options.reactivityTransform) {
return
}
const rootScope: Scope = {}
const scopeStack: Scope[] = [rootScope]
let currentScope: Scope = rootScope
const excludedIds = new WeakSet<Identifier>()
const parentStack: Node[] = []
const propsLocalToPublicMap: Record<string, string> = Object.create(null)
for (const key in ctx.propsDestructuredBindings) {
const { local } = ctx.propsDestructuredBindings[key]
rootScope[local] = true
propsLocalToPublicMap[local] = key
}
function pushScope() {
scopeStack.push((currentScope = Object.create(currentScope)))
}
function popScope() {
scopeStack.pop()
currentScope = scopeStack[scopeStack.length - 1] || null
}
function registerLocalBinding(id: Identifier) {
excludedIds.add(id)
if (currentScope) {
currentScope[id.name] = false
} else {
ctx.error(
'registerBinding called without active scope, something is wrong.',
id
)
}
}
function walkScope(node: Program | BlockStatement, isRoot = false) {
for (const stmt of node.body) {
if (stmt.type === 'VariableDeclaration') {
walkVariableDeclaration(stmt, isRoot)
} else if (
stmt.type === 'FunctionDeclaration' ||
stmt.type === 'ClassDeclaration'
) {
if (stmt.declare || !stmt.id) continue
registerLocalBinding(stmt.id)
} else if (
(stmt.type === 'ForOfStatement' || stmt.type === 'ForInStatement') &&
stmt.left.type === 'VariableDeclaration'
) {
walkVariableDeclaration(stmt.left)
} else if (
stmt.type === 'ExportNamedDeclaration' &&
stmt.declaration &&
stmt.declaration.type === 'VariableDeclaration'
) {
walkVariableDeclaration(stmt.declaration, isRoot)
} else if (
stmt.type === 'LabeledStatement' &&
stmt.body.type === 'VariableDeclaration'
) {
walkVariableDeclaration(stmt.body, isRoot)
}
}
}
function walkVariableDeclaration(stmt: VariableDeclaration, isRoot = false) {
if (stmt.declare) {
return
}
for (const decl of stmt.declarations) {
const isDefineProps =
isRoot && decl.init && isCallOf(unwrapTSNode(decl.init), 'defineProps')
for (const id of extractIdentifiers(decl.id)) {
if (isDefineProps) {
// for defineProps destructure, only exclude them since they
// are already passed in as knownProps
excludedIds.add(id)
} else {
registerLocalBinding(id)
}
}
}
}
function rewriteId(id: Identifier, parent: Node, parentStack: Node[]) {
if (
(parent.type === 'AssignmentExpression' && id === parent.left) ||
parent.type === 'UpdateExpression'
) {
ctx.error(`Cannot assign to destructured props as they are readonly.`, id)
}
if (isStaticProperty(parent) && parent.shorthand) {
// let binding used in a property shorthand
// skip for destructure patterns
if (
!(parent as any).inPattern ||
isInDestructureAssignment(parent, parentStack)
) {
// { prop } -> { prop: __props.prop }
ctx.s.appendLeft(
id.end! + ctx.startOffset!,
`: ${genPropsAccessExp(propsLocalToPublicMap[id.name])}`
)
}
} else {
// x --> __props.x
ctx.s.overwrite(
id.start! + ctx.startOffset!,
id.end! + ctx.startOffset!,
genPropsAccessExp(propsLocalToPublicMap[id.name])
)
}
}
function checkUsage(node: Node, method: string, alias = method) {
if (isCallOf(node, alias)) {
const arg = unwrapTSNode(node.arguments[0])
if (arg.type === 'Identifier' && currentScope[arg.name]) {
ctx.error(
`"${arg.name}" is a destructured prop and should not be passed directly to ${method}(). ` +
`Pass a getter () => ${arg.name} instead.`,
arg
)
}
}
}
// check root scope first
const ast = ctx.scriptSetupAst!
walkScope(ast, true)
walk(ast, {
enter(node: Node, parent?: Node) {
parent && parentStack.push(parent)
// skip type nodes
if (
parent &&
parent.type.startsWith('TS') &&
parent.type !== 'TSAsExpression' &&
parent.type !== 'TSNonNullExpression' &&
parent.type !== 'TSTypeAssertion'
) {
return this.skip()
}
checkUsage(node, 'watch', vueImportAliases.watch)
checkUsage(node, 'toRef', vueImportAliases.toRef)
// function scopes
if (isFunctionType(node)) {
pushScope()
walkFunctionParams(node, registerLocalBinding)
if (node.body.type === 'BlockStatement') {
walkScope(node.body)
}
return
}
// catch param
if (node.type === 'CatchClause') {
pushScope()
if (node.param && node.param.type === 'Identifier') {
registerLocalBinding(node.param)
}
walkScope(node.body)
return
}
// non-function block scopes
if (node.type === 'BlockStatement' && !isFunctionType(parent!)) {
pushScope()
walkScope(node)
return
}
if (node.type === 'Identifier') {
if (
isReferencedIdentifier(node, parent!, parentStack) &&
!excludedIds.has(node)
) {
if (currentScope[node.name]) {
rewriteId(node, parent!, parentStack)
}
}
}
},
leave(node: Node, parent?: Node) {
parent && parentStack.pop()
if (
(node.type === 'BlockStatement' && !isFunctionType(parent!)) ||
isFunctionType(node)
) {
popScope()
}
},
})
}
import { LVal, Node } from '@babel/types'
import { isCallOf } from './utils'
import { ScriptCompileContext } from './context'
export const DEFINE_SLOTS = 'defineSlots'
export function processDefineSlots(
ctx: ScriptCompileContext,
node: Node,
declId?: LVal
): boolean {
if (!isCallOf(node, DEFINE_SLOTS)) {
return false
}
if (ctx.hasDefineSlotsCall) {
ctx.error(`duplicate ${DEFINE_SLOTS}() call`, node)
}
ctx.hasDefineSlotsCall = true
if (node.arguments.length > 0) {
ctx.error(`${DEFINE_SLOTS}() cannot accept arguments`, node)
}
if (declId) {
ctx.s.overwrite(
ctx.startOffset! + node.start!,
ctx.startOffset! + node.end!,
`${ctx.helper('useSlots')}()`
)
}
return true
}
import { analyzeScriptBindings } from './analyzeScriptBindings'
import { ScriptCompileContext } from './context'
import MagicString from 'magic-string'
import { rewriteDefaultAST } from '../rewriteDefault'
export const normalScriptDefaultVar = `__default__`
export function processNormalScript(
ctx: ScriptCompileContext,
_scopeId: string
) {
const script = ctx.descriptor.script!
// if (script.lang && !ctx.isJS && !ctx.isTS) {
// // do not process non js/ts script blocks
// return script
// }
try {
let content = script.content
let map = script.map
const scriptAst = ctx.scriptAst!
const bindings = analyzeScriptBindings(scriptAst.body)
const { cssVars } = ctx.descriptor
const { genDefaultAs } = ctx.options
if (cssVars.length || genDefaultAs) {
const defaultVar = genDefaultAs || normalScriptDefaultVar
const s = new MagicString(content)
rewriteDefaultAST(scriptAst.body, s, defaultVar)
content = s.toString()
if (!genDefaultAs) {
content += `\nexport default ${defaultVar}`
}
}
return {
...script,
content,
map,
bindings,
scriptAst: scriptAst.body,
}
} catch (e: any) {
// silently fallback if parse fails since user may be using custom
// babel syntax
return script
}
}
import { AwaitExpression } from '@babel/types'
import { ScriptCompileContext } from './context'
/**
* Support context-persistence between top-level await expressions:
*
* ```js
* const instance = getCurrentInstance()
* await foo()
* expect(getCurrentInstance()).toBe(instance)
* ```
*
* In the future we can potentially get rid of this when Async Context
* becomes generally available: https://github.com/tc39/proposal-async-context
*
* ```js
* // input
* await foo()
* // output
* ;(
* ([__temp,__restore] = withAsyncContext(() => foo())),
* await __temp,
* __restore()
* )
*
* // input
* const a = await foo()
* // output
* const a = (
* ([__temp, __restore] = withAsyncContext(() => foo())),
* __temp = await __temp,
* __restore(),
* __temp
* )
* ```
*/
export function processAwait(
ctx: ScriptCompileContext,
node: AwaitExpression,
needSemi: boolean,
isStatement: boolean
) {
const argumentStart =
node.argument.extra && node.argument.extra.parenthesized
? (node.argument.extra.parenStart as number)
: node.argument.start!
const startOffset = ctx.startOffset!
const argumentStr = ctx.descriptor.source.slice(
argumentStart + startOffset,
node.argument.end! + startOffset
)
const containsNestedAwait = /\bawait\b/.test(argumentStr)
ctx.s.overwrite(
node.start! + startOffset,
argumentStart + startOffset,
`${needSemi ? `;` : ``}(\n ([__temp,__restore] = ${ctx.helper(
`withAsyncContext`
)}(${containsNestedAwait ? `async ` : ``}() => `
)
ctx.s.appendLeft(
node.end! + startOffset,
`)),\n ${isStatement ? `` : `__temp = `}await __temp,\n __restore()${
isStatement ? `` : `,\n __temp`
}\n)`
)
}
import {
CallExpression,
Expression,
Identifier,
ImportDefaultSpecifier,
ImportNamespaceSpecifier,
ImportSpecifier,
Node,
StringLiteral,
} from '@babel/types'
import path from 'path'
import { TS_NODE_TYPES } from '@vue/compiler-dom'
export const UNKNOWN_TYPE = 'Unknown'
export function resolveObjectKey(node: Node, computed: boolean) {
switch (node.type) {
case 'StringLiteral':
case 'NumericLiteral':
return String(node.value)
case 'Identifier':
if (!computed) return node.name
}
return undefined
}
export function concatStrings(strs: Array<string | null | undefined | false>) {
return strs.filter((s): s is string => !!s).join(', ')
}
export function isLiteralNode(node: Node) {
return node.type.endsWith('Literal')
}
export function unwrapTSNode(node: Node): Node {
if (TS_NODE_TYPES.includes(node.type)) {
return unwrapTSNode((node as any).expression)
} else {
return node
}
}
export function isCallOf(
node: Node | null | undefined,
test: string | ((id: string) => boolean) | null | undefined
): node is CallExpression {
return !!(
node &&
test &&
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
(typeof test === 'string'
? node.callee.name === test
: test(node.callee.name))
)
}
export function toRuntimeTypeString(types: string[]) {
return types.length > 1 ? `[${types.join(', ')}]` : types[0]
}
export function getImportedName(
specifier: ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier
) {
if (specifier.type === 'ImportSpecifier')
return specifier.imported.type === 'Identifier'
? specifier.imported.name
: specifier.imported.value
else if (specifier.type === 'ImportNamespaceSpecifier') return '*'
return 'default'
}
export function getId(node: Identifier | StringLiteral): string
export function getId(node: Expression): string | null
export function getId(node: Expression) {
return node.type === 'Identifier'
? node.name
: node.type === 'StringLiteral'
? node.value
: null
}
const identity = (str: string) => str
const fileNameLowerCaseRegExp = /[^\u0130\u0131\u00DFa-z0-9\\/:\-_\. ]+/g
const toLowerCase = (str: string) => str.toLowerCase()
function toFileNameLowerCase(x: string) {
return fileNameLowerCaseRegExp.test(x)
? x.replace(fileNameLowerCaseRegExp, toLowerCase)
: x
}
/**
* We need `getCanonicalFileName` when creating ts module resolution cache,
* but TS does not expose it directly. This implementation is repllicated from
* the TS source code.
*/
export function createGetCanonicalFileName(useCaseSensitiveFileNames: boolean) {
return useCaseSensitiveFileNames ? identity : toFileNameLowerCase
}
// in the browser build, the polyfill doesn't expose posix, but defaults to
// posix behavior.
const normalize = (path.posix || path).normalize
const windowsSlashRE = /\\/g
export function normalizePath(p: string) {
return normalize(p.replace(windowsSlashRE, '/'))
}
export const joinPaths = (path.posix || path).join
/**
* key may contain symbols
* e.g. onUpdate:modelValue -> "onUpdate:modelValue"
*/
export const propNameEscapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~\-]/
export function getEscapedPropName(key: string) {
return propNameEscapeSymbolsRE.test(key) ? JSON.stringify(key) : key
}
export const cssVarNameEscapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g
export function getEscapedCssVarName(key: string, doubleEscape: boolean) {
return key.replace(cssVarNameEscapeSymbolsRE, (s) =>
doubleEscape ? `\\\\${s}` : `\\${s}`
)
}
declare module 'merge-source-map' {
export default function merge(oldMap: object, newMap: object): object
}
import { UrlWithStringQuery, parse as uriParse } from 'url'
import { isString } from '@vue/shared'
export function isRelativeUrl(url: string): boolean {
const firstChar = url.charAt(0)
return firstChar === '.' || firstChar === '~' || firstChar === '@'
}
const externalRE = /^(https?:)?\/\//
export function isExternalUrl(url: string): boolean {
return externalRE.test(url)
}
const dataUrlRE = /^\s*data:/i
export function isDataUrl(url: string): boolean {
return dataUrlRE.test(url)
}
/**
* Parses string url into URL object.
*/
export function parseUrl(url: string): UrlWithStringQuery {
const firstChar = url.charAt(0)
if (firstChar === '~') {
const secondChar = url.charAt(1)
url = url.slice(secondChar === '/' ? 2 : 1)
}
return parseUriParts(url)
}
/**
* vuejs/component-compiler-utils#22 Support uri fragment in transformed require
* @param urlString - an url as a string
*/
function parseUriParts(urlString: string): UrlWithStringQuery {
// A TypeError is thrown if urlString is not a string
// @see https://nodejs.org/api/url.html#url_url_parse_urlstring_parsequerystring_slashesdenotehost
return uriParse(isString(urlString) ? urlString : '', false, true)
}
import path from 'path'
import {
ConstantTypes,
createSimpleExpression,
ExpressionNode,
NodeTransform,
NodeTypes,
SimpleExpressionNode,
SourceLocation,
TransformContext,
} from '@vue/compiler-core'
import {
isRelativeUrl,
parseUrl,
isExternalUrl,
isDataUrl,
} from './templateUtils'
import { isArray } from '@vue/shared'
export interface AssetURLTagConfig {
[name: string]: string[]
}
export interface AssetURLOptions {
/**
* If base is provided, instead of transforming relative asset urls into
* imports, they will be directly rewritten to absolute urls.
*/
base?: string | null
/**
* If true, also processes absolute urls.
*/
includeAbsolute?: boolean
tags?: AssetURLTagConfig
}
export const defaultAssetUrlOptions: Required<AssetURLOptions> = {
base: null,
includeAbsolute: false,
tags: {
video: ['src', 'poster'],
source: ['src'],
img: ['src'],
image: ['xlink:href', 'href'],
use: ['xlink:href', 'href'],
},
}
export const normalizeOptions = (
options: AssetURLOptions | AssetURLTagConfig
): Required<AssetURLOptions> => {
if (Object.keys(options).some((key) => isArray((options as any)[key]))) {
// legacy option format which directly passes in tags config
return {
...defaultAssetUrlOptions,
tags: options as any,
}
}
return {
...defaultAssetUrlOptions,
...options,
}
}
export const createAssetUrlTransformWithOptions = (
options: Required<AssetURLOptions>
): NodeTransform => {
return (node, context) =>
(transformAssetUrl as Function)(node, context, options)
}
/**
* A `@vue/compiler-core` plugin that transforms relative asset urls into
* either imports or absolute urls.
*
* ``` js
* // Before
* createVNode('img', { src: './logo.png' })
*
* // After
* import _imports_0 from './logo.png'
* createVNode('img', { src: _imports_0 })
* ```
*/
export const transformAssetUrl: NodeTransform = (
node,
context,
options: AssetURLOptions = defaultAssetUrlOptions
) => {
if (node.type === NodeTypes.ELEMENT) {
if (!node.props.length) {
return
}
const tags = options.tags || defaultAssetUrlOptions.tags
const attrs = tags[node.tag]
const wildCardAttrs = tags['*']
if (!attrs && !wildCardAttrs) {
return
}
const assetAttrs = (attrs || []).concat(wildCardAttrs || [])
node.props.forEach((attr, index) => {
if (
attr.type !== NodeTypes.ATTRIBUTE ||
!assetAttrs.includes(attr.name) ||
!attr.value ||
isExternalUrl(attr.value.content) ||
isDataUrl(attr.value.content) ||
attr.value.content[0] === '#' ||
(!options.includeAbsolute && !isRelativeUrl(attr.value.content))
) {
return
}
const url = parseUrl(attr.value.content)
if (options.base && attr.value.content[0] === '.') {
// explicit base - directly rewrite relative urls into absolute url
// to avoid generating extra imports
// Allow for full hostnames provided in options.base
const base = parseUrl(options.base)
const protocol = base.protocol || ''
const host = base.host ? protocol + '//' + base.host : ''
const basePath = base.path || '/'
// when packaged in the browser, path will be using the posix-
// only version provided by rollup-plugin-node-builtins.
attr.value.content =
host +
(path.posix || path).join(basePath, url.path + (url.hash || ''))
return
}
// otherwise, transform the url into an import.
// this assumes a bundler will resolve the import into the correct
// absolute url (e.g. webpack file-loader)
const exp = getImportsExpressionExp(url.path, url.hash, attr.loc, context)
node.props[index] = {
type: NodeTypes.DIRECTIVE,
name: 'bind',
arg: createSimpleExpression(attr.name, true, attr.loc),
exp,
modifiers: [],
loc: attr.loc,
}
})
}
}
function getImportsExpressionExp(
path: string | null,
hash: string | null,
loc: SourceLocation,
context: TransformContext
): ExpressionNode {
if (path) {
let name: string
let exp: SimpleExpressionNode
const existingIndex = context.imports.findIndex((i) => i.path === path)
if (existingIndex > -1) {
name = `_imports_${existingIndex}`
exp = context.imports[existingIndex].exp as SimpleExpressionNode
} else {
name = `_imports_${context.imports.length}`
exp = createSimpleExpression(
name,
false,
loc,
ConstantTypes.CAN_STRINGIFY
)
// We need to ensure the path is not encoded (to %2F),
// so we decode it back in case it is encoded
context.imports.push({
exp,
path: decodeURIComponent(path),
})
}
if (!hash) {
return exp
}
const hashExp = `${name} + '${hash}'`
const finalExp = createSimpleExpression(
hashExp,
false,
loc,
ConstantTypes.CAN_STRINGIFY
)
if (!context.hoistStatic) {
return finalExp
}
const existingHoistIndex = context.hoists.findIndex((h) => {
return (
h &&
h.type === NodeTypes.SIMPLE_EXPRESSION &&
!h.isStatic &&
h.content === hashExp
)
})
if (existingHoistIndex > -1) {
return createSimpleExpression(
`_hoisted_${existingHoistIndex + 1}`,
false,
loc,
ConstantTypes.CAN_STRINGIFY
)
}
return context.hoist(finalExp)
} else {
return createSimpleExpression(`''`, false, loc, ConstantTypes.CAN_STRINGIFY)
}
}
...@@ -201,7 +201,7 @@ export function uniViteInjectPlugin( ...@@ -201,7 +201,7 @@ export function uniViteInjectPlugin(
return return
} }
if (isReference(node, parent)) { if (isReference(node, parent!)) {
const { name, keypath } = flatten(node) const { name, keypath } = flatten(node)
const handled = handleReference(node, name, keypath, parent) const handled = handleReference(node, name, keypath, parent)
if (handled) { if (handled) {
......
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册