vue3-core/packages/compiler-vapor/src/generate.ts

446 lines
11 KiB
TypeScript
Raw Normal View History

2023-12-01 22:12:19 +08:00
import {
type CodegenOptions,
type CodegenResult,
type Position,
2023-12-02 00:49:13 +08:00
type SourceLocation,
2023-12-01 22:12:19 +08:00
NewlineType,
advancePositionWithMutation,
locStub,
2023-12-01 23:30:21 +08:00
NodeTypes,
BindingTypes,
2023-12-02 00:49:13 +08:00
isSimpleIdentifier,
2023-12-01 22:12:19 +08:00
} from '@vue/compiler-dom'
2023-11-24 15:25:34 +08:00
import {
2023-12-01 23:30:21 +08:00
type IRDynamicChildren,
2023-11-24 15:25:34 +08:00
type RootIRNode,
IRNodeTypes,
OperationNode,
2023-12-01 05:18:20 +08:00
VaporHelper,
2023-12-01 23:30:21 +08:00
IRExpression,
2023-11-24 15:25:34 +08:00
} from './ir'
2023-12-01 22:12:19 +08:00
import { SourceMapGenerator } from 'source-map-js'
2023-12-01 23:30:21 +08:00
import { isString } from '@vue/shared'
2023-11-17 03:01:19 +08:00
2023-11-24 14:44:57 +08:00
// remove when stable
2023-12-01 22:12:19 +08:00
// @ts-expect-error
function checkNever(x: never): never {}
2023-12-01 23:30:21 +08:00
export interface CodegenContext extends Required<CodegenOptions> {
2023-12-01 22:12:19 +08:00
source: string
code: string
line: number
column: number
offset: number
indentLevel: number
map?: SourceMapGenerator
2023-12-02 00:07:24 +08:00
push(
code: string,
newlineIndex?: number,
2023-12-02 00:49:13 +08:00
loc?: SourceLocation,
name?: string,
2023-12-02 00:07:24 +08:00
): void
pushWithNewline(
code: string,
newlineIndex?: number,
2023-12-02 00:49:13 +08:00
loc?: SourceLocation,
name?: string,
2023-12-02 00:07:24 +08:00
): void
2023-12-01 22:12:19 +08:00
indent(): void
2023-12-01 22:45:08 +08:00
deindent(): void
2023-12-01 22:12:19 +08:00
newline(): void
2023-11-24 14:44:57 +08:00
2023-12-01 05:18:20 +08:00
helpers: Set<string>
vaporHelpers: Set<string>
helper(name: string): string
vaporHelper(name: string): string
}
2023-12-01 22:12:19 +08:00
function createCodegenContext(
ir: RootIRNode,
{
mode = 'function',
prefixIdentifiers = mode === 'module',
sourceMap = false,
filename = `template.vue.html`,
scopeId = null,
optimizeImports = false,
runtimeGlobalName = `Vue`,
runtimeModuleName = `vue`,
ssrRuntimeModuleName = 'vue/server-renderer',
ssr = false,
isTS = false,
inSSR = false,
2023-12-01 23:30:21 +08:00
inline = false,
bindingMetadata = {},
2023-12-01 22:12:19 +08:00
}: CodegenOptions,
) {
2023-12-01 05:18:20 +08:00
const { helpers, vaporHelpers } = ir
2023-12-01 22:12:19 +08:00
const context: CodegenContext = {
mode,
prefixIdentifiers,
sourceMap,
filename,
scopeId,
optimizeImports,
runtimeGlobalName,
runtimeModuleName,
ssrRuntimeModuleName,
ssr,
isTS,
inSSR,
2023-12-01 23:30:21 +08:00
bindingMetadata,
inline,
2023-12-01 22:12:19 +08:00
source: ir.source,
code: ``,
column: 1,
line: 1,
offset: 0,
indentLevel: 0,
2023-12-01 05:18:20 +08:00
helpers,
vaporHelpers,
helper(name: string) {
helpers.add(name)
2023-12-01 08:26:01 +08:00
return `_${name}`
2023-12-01 05:18:20 +08:00
},
vaporHelper(name: VaporHelper) {
vaporHelpers.add(name)
2023-12-01 08:26:01 +08:00
return `_${name}`
2023-12-01 05:18:20 +08:00
},
2023-12-02 00:49:13 +08:00
push(code, newlineIndex = NewlineType.None, loc, name) {
2023-12-01 22:12:19 +08:00
context.code += code
if (!__BROWSER__ && context.map) {
2023-12-02 00:49:13 +08:00
if (loc) addMapping(loc.start, name)
2023-12-01 22:12:19 +08:00
if (newlineIndex === NewlineType.Unknown) {
// multiple newlines, full iteration
advancePositionWithMutation(context, code)
} else {
// fast paths
context.offset += code.length
if (newlineIndex === NewlineType.None) {
// no newlines; fast path to avoid newline detection
if (__TEST__ && code.includes('\n')) {
throw new Error(
`CodegenContext.push() called newlineIndex: none, but contains` +
`newlines: ${code.replace(/\n/g, '\\n')}`,
)
}
context.column += code.length
} else {
// single newline at known index
if (newlineIndex === NewlineType.End) {
newlineIndex = code.length - 1
}
if (
__TEST__ &&
(code.charAt(newlineIndex) !== '\n' ||
code.slice(0, newlineIndex).includes('\n') ||
code.slice(newlineIndex + 1).includes('\n'))
) {
throw new Error(
`CodegenContext.push() called with newlineIndex: ${newlineIndex} ` +
`but does not conform: ${code.replace(/\n/g, '\\n')}`,
)
}
context.line++
context.column = code.length - newlineIndex
}
}
2023-12-02 00:49:13 +08:00
if (loc && loc !== locStub) {
addMapping(loc.end)
2023-12-01 22:12:19 +08:00
}
}
},
2023-12-01 22:45:08 +08:00
pushWithNewline(code, newlineIndex, node) {
context.newline()
context.push(code, newlineIndex, node)
},
2023-12-01 22:12:19 +08:00
indent() {
2023-12-01 22:45:08 +08:00
++context.indentLevel
2023-12-01 22:12:19 +08:00
},
2023-12-01 22:45:08 +08:00
deindent() {
--context.indentLevel
2023-12-01 22:12:19 +08:00
},
newline() {
newline(context.indentLevel)
},
}
function newline(n: number) {
context.push(`\n${` `.repeat(n)}`, NewlineType.Start)
}
function addMapping(loc: Position, name: string | null = null) {
// we use the private property to directly add the mapping
// because the addMapping() implementation in source-map-js has a bunch of
// unnecessary arg and validation checks that are pure overhead in our case.
const { _names, _mappings } = context.map!
if (name !== null && !_names.has(name)) _names.add(name)
_mappings.add({
originalLine: loc.line,
originalColumn: loc.column - 1, // source-map column is 0 based
generatedLine: context.line,
generatedColumn: context.column - 1,
source: filename,
// @ts-ignore it is possible to be null
name,
})
}
if (!__BROWSER__ && sourceMap) {
// lazy require source-map implementation, only in non-browser builds
context.map = new SourceMapGenerator()
context.map.setSourceContent(filename, context.source)
context.map._sources.add(filename)
2023-12-01 05:18:20 +08:00
}
2023-12-01 22:12:19 +08:00
return context
2023-12-01 05:18:20 +08:00
}
2023-11-17 03:01:19 +08:00
// IR -> JS codegen
export function generate(
2023-11-23 23:42:08 +08:00
ir: RootIRNode,
2023-11-24 11:15:33 +08:00
options: CodegenOptions = {},
2023-11-17 03:01:19 +08:00
): CodegenResult {
2023-12-01 05:18:20 +08:00
const ctx = createCodegenContext(ir, options)
2023-12-01 22:45:08 +08:00
const { push, pushWithNewline, indent, deindent, newline } = ctx
2023-12-01 05:18:20 +08:00
const { vaporHelper, helpers, vaporHelpers } = ctx
2023-12-01 22:12:19 +08:00
const functionName = 'render'
const isSetupInlined = !!options.inline
if (isSetupInlined) {
2023-12-01 22:45:08 +08:00
push(`(() => {`)
2023-12-01 22:12:19 +08:00
} else {
2023-12-02 00:35:30 +08:00
// placeholder for preamble
newline()
2023-12-01 23:30:21 +08:00
pushWithNewline(`export function ${functionName}(_ctx) {`)
2023-12-01 22:12:19 +08:00
}
2023-12-01 22:45:08 +08:00
indent()
2023-11-17 03:01:19 +08:00
2023-11-26 03:53:47 +08:00
ir.template.forEach((template, i) => {
if (template.type === IRNodeTypes.TEMPLATE_FACTORY) {
2023-12-01 22:12:19 +08:00
// TODO source map?
2023-12-01 22:45:08 +08:00
pushWithNewline(
2023-12-01 22:12:19 +08:00
`const t${i} = ${vaporHelper('template')}(${JSON.stringify(
template.template,
2023-12-01 22:45:08 +08:00
)})`,
2023-12-01 22:12:19 +08:00
)
2023-11-26 03:53:47 +08:00
} else {
// fragment
2023-12-01 22:45:08 +08:00
pushWithNewline(
`const t0 = ${vaporHelper('fragment')}()\n`,
NewlineType.End,
)
2023-11-26 03:53:47 +08:00
}
})
2023-11-17 03:01:19 +08:00
2023-11-26 02:13:59 +08:00
{
2023-12-01 22:45:08 +08:00
pushWithNewline(`const n${ir.dynamic.id} = t0()`)
2023-11-27 05:16:21 +08:00
const children = genChildren(ir.dynamic.children)
if (children) {
2023-12-01 22:45:08 +08:00
pushWithNewline(
`const ${children} = ${vaporHelper('children')}(n${ir.dynamic.id})`,
2023-12-01 22:12:19 +08:00
)
2023-11-26 03:08:35 +08:00
}
2023-11-23 23:42:08 +08:00
2023-11-24 20:29:05 +08:00
for (const operation of ir.operation) {
2023-12-01 22:45:08 +08:00
genOperation(operation, ctx)
2023-11-23 23:42:08 +08:00
}
2023-12-01 23:30:21 +08:00
for (const { operations } of ir.effect) {
2023-12-01 22:45:08 +08:00
pushWithNewline(`${vaporHelper('effect')}(() => {`)
indent()
2023-11-24 20:29:05 +08:00
for (const operation of operations) {
2023-12-01 22:45:08 +08:00
genOperation(operation, ctx)
2023-11-24 20:29:05 +08:00
}
2023-12-01 22:45:08 +08:00
deindent()
pushWithNewline('})')
2023-11-24 20:29:05 +08:00
}
// TODO multiple-template
2023-11-26 03:53:47 +08:00
// TODO return statement in IR
2023-12-01 22:45:08 +08:00
pushWithNewline(`return n${ir.dynamic.id}`)
2023-11-24 20:29:05 +08:00
}
2023-11-17 03:01:19 +08:00
2023-12-01 22:45:08 +08:00
deindent()
newline()
2023-12-01 22:12:19 +08:00
if (isSetupInlined) {
2023-12-01 22:45:08 +08:00
push('})()')
2023-12-01 22:12:19 +08:00
} else {
2023-12-01 22:45:08 +08:00
push('}')
2023-12-01 22:12:19 +08:00
}
2023-12-02 00:35:30 +08:00
let preamble = ''
if (vaporHelpers.size)
2023-12-02 00:35:30 +08:00
// TODO: extract import codegen
preamble = `import { ${[...vaporHelpers]
.map((h) => `${h} as _${h}`)
.join(', ')} } from 'vue/vapor';`
if (helpers.size)
2023-12-02 00:35:30 +08:00
preamble = `import { ${[...helpers]
.map((h) => `${h} as _${h}`)
.join(', ')} } from 'vue';`
if (!isSetupInlined) {
ctx.code = preamble + ctx.code
}
2023-11-17 03:01:19 +08:00
return {
2023-12-01 22:12:19 +08:00
code: ctx.code,
2023-11-23 23:42:08 +08:00
ast: ir as any,
2023-12-02 00:35:30 +08:00
preamble,
2023-12-01 22:12:19 +08:00
map: ctx.map ? ctx.map.toJSON() : undefined,
2023-11-23 23:42:08 +08:00
}
2023-12-01 05:18:20 +08:00
}
2023-11-24 15:25:34 +08:00
2023-12-01 23:30:21 +08:00
function genOperation(oper: OperationNode, context: CodegenContext) {
2023-12-02 00:07:24 +08:00
const { vaporHelper, push, pushWithNewline } = context
2023-12-01 05:18:20 +08:00
// TODO: cache old value
switch (oper.type) {
case IRNodeTypes.SET_PROP: {
2023-12-02 00:07:24 +08:00
pushWithNewline(`${vaporHelper('setAttr')}(n${oper.element}, `)
genExpression(oper.name, context)
push(`, undefined, `)
genExpression(oper.value, context)
push(')')
2023-12-01 22:45:08 +08:00
return
2023-12-01 05:18:20 +08:00
}
2023-11-24 15:25:34 +08:00
2023-12-01 05:18:20 +08:00
case IRNodeTypes.SET_TEXT: {
2023-12-02 00:07:24 +08:00
pushWithNewline(`${vaporHelper('setText')}(n${oper.element}, undefined, `)
genExpression(oper.value, context)
push(')')
2023-12-01 22:45:08 +08:00
return
2023-12-01 05:18:20 +08:00
}
2023-11-24 15:25:34 +08:00
2023-12-01 05:18:20 +08:00
case IRNodeTypes.SET_EVENT: {
2023-12-02 00:07:24 +08:00
pushWithNewline(`${vaporHelper('on')}(n${oper.element}, `)
genExpression(oper.name, context)
push(', ')
const hasModifiers = oper.modifiers.length
hasModifiers && push(`${vaporHelper('withModifiers')}(`)
genExpression(oper.value, context)
hasModifiers && push(`, ${genArrayExpression(oper.modifiers)})`)
push(')')
2023-12-01 22:45:08 +08:00
return
2023-12-01 05:18:20 +08:00
}
2023-11-24 15:25:34 +08:00
2023-12-01 05:18:20 +08:00
case IRNodeTypes.SET_HTML: {
2023-12-02 00:07:24 +08:00
pushWithNewline(`${vaporHelper('setHtml')}(n${oper.element}, undefined, `)
genExpression(oper.value, context)
push(')')
2023-12-01 22:45:08 +08:00
return
2023-12-01 05:18:20 +08:00
}
2023-11-24 15:25:34 +08:00
2023-12-01 05:18:20 +08:00
case IRNodeTypes.CREATE_TEXT_NODE: {
2023-12-02 00:07:24 +08:00
pushWithNewline(`const n${oper.id} = ${vaporHelper('createTextNode')}(`)
genExpression(oper.value, context)
push(')')
2023-12-01 22:45:08 +08:00
return
2023-11-24 15:25:34 +08:00
}
2023-12-01 05:18:20 +08:00
case IRNodeTypes.INSERT_NODE: {
const elements = ([] as number[]).concat(oper.element)
let element = elements.map((el) => `n${el}`).join(', ')
if (elements.length > 1) element = `[${element}]`
2023-12-01 22:45:08 +08:00
pushWithNewline(
`${vaporHelper('insert')}(${element}, n${
oper.parent
}${`, n${oper.anchor}`})`,
)
return
2023-12-01 05:18:20 +08:00
}
case IRNodeTypes.PREPEND_NODE: {
2023-12-01 22:45:08 +08:00
pushWithNewline(
`${vaporHelper('prepend')}(n${oper.parent}, ${oper.elements
.map((el) => `n${el}`)
.join(', ')})`,
)
return
2023-12-01 05:18:20 +08:00
}
case IRNodeTypes.APPEND_NODE: {
2023-12-01 22:45:08 +08:00
pushWithNewline(
`${vaporHelper('append')}(n${oper.parent}, ${oper.elements
.map((el) => `n${el}`)
.join(', ')})`,
)
return
2023-12-01 05:18:20 +08:00
}
default:
2023-12-01 22:12:19 +08:00
return checkNever(oper)
2023-11-24 15:25:34 +08:00
}
2023-11-23 23:42:08 +08:00
}
2023-12-01 23:30:21 +08:00
function genChildren(children: IRDynamicChildren) {
2023-11-27 05:16:21 +08:00
let code = ''
// TODO
let offset = 0
2023-11-23 23:42:08 +08:00
for (const [index, child] of Object.entries(children)) {
2023-11-27 05:16:21 +08:00
const childrenLength = Object.keys(child.children).length
if (child.ghost && child.placeholder === null && childrenLength === 0) {
offset--
2023-11-27 05:16:21 +08:00
continue
}
2023-11-27 05:16:21 +08:00
code += ` ${Number(index) + offset}: [`
const id = child.ghost ? child.placeholder : child.id
if (id !== null) code += `n${id}`
const childrenString = childrenLength && genChildren(child.children)
if (childrenString) code += `, ${childrenString}`
code += '],'
2023-11-17 03:01:19 +08:00
}
2023-11-27 05:16:21 +08:00
if (!code) return ''
return `{${code}}`
2023-11-17 03:01:19 +08:00
}
// TODO: other types (not only string)
function genArrayExpression(elements: string[]) {
2023-12-02 00:07:24 +08:00
return `[${elements.map((it) => JSON.stringify(it)).join(', ')}]`
}
2023-12-01 23:30:21 +08:00
2023-12-02 00:07:24 +08:00
function genExpression(
2023-12-01 23:30:21 +08:00
exp: IRExpression,
2023-12-02 00:07:24 +08:00
{
inline,
prefixIdentifiers,
bindingMetadata,
vaporHelper,
push,
}: CodegenContext,
2023-12-01 23:30:21 +08:00
) {
2023-12-02 00:07:24 +08:00
if (isString(exp)) return push(exp)
2023-12-01 23:30:21 +08:00
2023-12-02 00:07:24 +08:00
// TODO NodeTypes.COMPOUND_EXPRESSION
if (exp.type === NodeTypes.COMPOUND_EXPRESSION) return
2023-12-01 23:30:21 +08:00
2023-12-02 00:49:13 +08:00
let { content } = exp
let name: string | undefined
2023-12-01 23:30:21 +08:00
2023-12-02 00:07:24 +08:00
if (exp.isStatic) {
content = JSON.stringify(content)
} else {
switch (bindingMetadata[content]) {
case BindingTypes.SETUP_REF:
content += '.value'
break
case BindingTypes.SETUP_MAYBE_REF:
content = `${vaporHelper('unref')}(${content})`
break
}
if (prefixIdentifiers && !inline) {
2023-12-02 00:49:13 +08:00
if (isSimpleIdentifier(content)) name = content
2023-12-02 00:07:24 +08:00
content = `_ctx.${content}`
}
2023-12-01 23:30:21 +08:00
}
2023-12-02 00:49:13 +08:00
push(content, NewlineType.None, exp.loc, name)
2023-12-01 23:30:21 +08:00
}