diff --git a/docs/introduction/getting-started.md b/docs/introduction/getting-started.md index 563f77e..b2cb1be 100644 --- a/docs/introduction/getting-started.md +++ b/docs/introduction/getting-started.md @@ -25,7 +25,7 @@ We assume you are already familiar with the basic usages of Vue before you conti pnpm add vue-jsx-vapor # runtime -pnpm add https://pkg.pr.new/vue@715b798 +pnpm add https://pkg.pr.new/vue@51677cd ``` The Vue Vapor runtime is not release, so we use [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new) to install. diff --git a/packages/babel/package.json b/packages/babel/package.json index cd42597..b8cb77c 100644 --- a/packages/babel/package.json +++ b/packages/babel/package.json @@ -46,7 +46,7 @@ "@babel/traverse": "catalog:", "@babel/types": "catalog:", "@vue-jsx-vapor/compiler": "workspace:*", - "source-map-js": "^1.2.1" + "source-map-js": "catalog:" }, "devDependencies": { "@types/babel__core": "catalog:", diff --git a/packages/babel/src/index.ts b/packages/babel/src/index.ts index a398a84..dc2f92f 100644 --- a/packages/babel/src/index.ts +++ b/packages/babel/src/index.ts @@ -14,8 +14,7 @@ export type Options = { filename: string importSet: Set delegateEventSet: Set - preambleMap: Map - preambleIndex: number + templates: string[] file: BabelFile roots: { node: JSXElement | JSXFragment @@ -43,8 +42,7 @@ export default (): { enter: (path, state) => { state.importSet = new Set() state.delegateEventSet = new Set() - state.preambleMap = new Map() - state.preambleIndex = 0 + state.templates = [] state.roots = [] const collectRoot: VisitNodeFunction< Node, @@ -78,20 +76,24 @@ export default (): { }) }, exit: (path, state) => { - const { delegateEventSet, importSet, preambleMap } = state + const { delegateEventSet, importSet, templates } = state const statements: string[] = [] if (delegateEventSet.size) { statements.unshift( - `_delegateEvents(${Array.from(delegateEventSet).join(', ')});`, + `_delegateEvents("${Array.from(delegateEventSet).join('", "')}");`, ) } - if (preambleMap.size) { - let preambleResult = '' - for (const [value, key] of preambleMap) { - preambleResult += `const ${key} = ${value}\n` - } + if (templates.length) { + let preambleResult = 'const ' + const definedTemplates: Record = {} + templates.forEach((template, index) => { + preambleResult += `t${index} = ${ + definedTemplates[template] || template + }${templates.length - 1 === index ? ';' : ','}\n` + definedTemplates[template] = `t${index}` + }) statements.unshift(preambleResult) } diff --git a/packages/babel/src/transform.ts b/packages/babel/src/transform.ts index 3b691c9..2428e31 100644 --- a/packages/babel/src/transform.ts +++ b/packages/babel/src/transform.ts @@ -19,37 +19,18 @@ export const transformJSX: VisitNodeFunction< if (!root || !root.inVaporComponent) return const isTS = state.filename?.endsWith('tsx') - let { code, helpers, preamble, map } = compile(root.node, { + const { code, map, helpers, templates, delegates } = compile(root.node, { isTS, filename: state.filename, sourceMap: !!state.file.opts.sourceMaps, source: ' '.repeat(root.node.start || 0) + root.source, + templates: state.templates.slice(), ...state.opts.compile, }) helpers.forEach((helper) => state.importSet.add(helper)) - - preamble = preamble.replaceAll( - /(?<=const )t(?=(\d))/g, - `_t${state.preambleIndex}`, - ) - code = code.replaceAll(/(?<== )t(?=\d)/g, `_t${state.preambleIndex}`) - state.preambleIndex++ - - for (const [, key, value] of preamble.matchAll( - /const (_t\d+) = (_template\(.*\))/g, - )) { - const result = state.preambleMap.get(value) - if (result) { - code = code.replaceAll(key, result) - } else { - state.preambleMap.set(value, key) - } - } - - for (const [, events] of preamble.matchAll(/_delegateEvents\((.*)\)/g)) { - events.split(', ').forEach((event) => state.delegateEventSet.add(event)) - } + delegates.forEach((delegate) => state.delegateEventSet.add(delegate)) + state.templates.push(...templates.slice(state.templates.length)) const ast = parse(`(() => {${code}})()`, { sourceFilename: state.filename, diff --git a/packages/babel/test/__snapshots__/interop.spec.ts.snap b/packages/babel/test/__snapshots__/interop.spec.ts.snap new file mode 100644 index 0000000..ec9a927 --- /dev/null +++ b/packages/babel/test/__snapshots__/interop.spec.ts.snap @@ -0,0 +1,26 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`transform > transform multiple components 1`] = ` +"import { template as _template } from 'vue'; +const t0 = _template("
", true), + t1 = t0, + t2 = t1; +const A = defineComponent(() => { + defineVaporComponent(() => (() => { + const n0 = t0(); + return n0; + })()); + return () =>
; +}); +const B = defineVaporComponent(() => { + const C = defineComponent(() =>
); + const D = (() => { + const n0 = t1(); + return n0; + })(); + return (() => { + const n0 = t2(); + return n0; + })(); +});" +`; diff --git a/packages/babel/test/__snapshots__/transform.spec.ts.snap b/packages/babel/test/__snapshots__/transform.spec.ts.snap new file mode 100644 index 0000000..9f592e0 --- /dev/null +++ b/packages/babel/test/__snapshots__/transform.spec.ts.snap @@ -0,0 +1,29 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`transform > transform multiple components 1`] = ` +"import { child as _child, delegateEvents as _delegateEvents, template as _template, createIf as _createIf } from 'vue'; +import { setNodes as _setNodes } from 'vue-jsx-vapor/runtime'; +const t0 = _template("
", true), + t1 = _template("
Hello
"), + t2 = _template("
World
"); +_delegateEvents("click", "dblclick"); +const a = (() => { + const n0 = t0(); + const x0 = _child(n0); + _setNodes(x0, () => Hello); + n0.$evtclick = e => onClick(e); + return n0; +})(); +const b = (() => { + const n0 = _createIf(() => foo, () => { + const n2 = t1(); + n2.$evtclick = e => onClick(e); + return n2; + }, () => { + const n4 = t2(); + n4.$evtdblclick = e => onDblclick(e); + return n4; + }); + return n0; +})();" +`; diff --git a/packages/babel/test/interop.spec.ts b/packages/babel/test/interop.spec.ts index 24a8631..2f285d2 100644 --- a/packages/babel/test/interop.spec.ts +++ b/packages/babel/test/interop.spec.ts @@ -19,27 +19,6 @@ describe('transform', () => { plugins: [[jsx, { interop: true }]], }, )! - expect(code).toMatchInlineSnapshot(` - "import { template as _template } from 'vue'; - const _t00 = _template("
", true); - const A = defineComponent(() => { - defineVaporComponent(() => (() => { - const n0 = _t00(); - return n0; - })()); - return () =>
; - }); - const B = defineVaporComponent(() => { - const C = defineComponent(() =>
); - const D = (() => { - const n0 = _t00(); - return n0; - })(); - return (() => { - const n0 = _t00(); - return n0; - })(); - });" - `) + expect(code).matchSnapshot() }) }) diff --git a/packages/babel/test/transform.spec.ts b/packages/babel/test/transform.spec.ts index fdf536e..4bee9b5 100644 --- a/packages/babel/test/transform.spec.ts +++ b/packages/babel/test/transform.spec.ts @@ -12,32 +12,6 @@ describe('transform', () => { plugins: [[jsx]], }, )! - expect(code).toMatchInlineSnapshot(` - "import { child as _child, delegateEvents as _delegateEvents, template as _template, createIf as _createIf } from 'vue'; - import { setNodes as _setNodes } from 'vue-jsx-vapor/runtime'; - const _t00 = _template("
", true); - const _t10 = _template("
Hello
"); - const _t11 = _template("
World
"); - _delegateEvents("click", "dblclick"); - const a = (() => { - const n0 = _t00(); - const x0 = _child(n0); - _setNodes(x0, () => Hello); - n0.$evtclick = e => onClick(e); - return n0; - })(); - const b = (() => { - const n0 = _createIf(() => foo, () => { - const n2 = _t10(); - n2.$evtclick = e => onClick(e); - return n2; - }, () => { - const n4 = _t11(); - n4.$evtdblclick = e => onDblclick(e); - return n4; - }); - return n0; - })();" - `) + expect(code).matchSnapshot() }) }) diff --git a/packages/compiler/package.json b/packages/compiler/package.json index dde3481..9f6e3f6 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -43,7 +43,8 @@ "@babel/parser": "catalog:", "@babel/types": "catalog:", "@vue/compiler-dom": "catalog:", - "@vue/compiler-vapor": "catalog:", - "@vue/shared": "catalog:" + "@vue/shared": "catalog:", + "ast-kit": "^2.1.1", + "source-map-js": "catalog:" } } diff --git a/packages/compiler/src/compile.ts b/packages/compiler/src/compile.ts index b4f8e9b..6e4aba8 100644 --- a/packages/compiler/src/compile.ts +++ b/packages/compiler/src/compile.ts @@ -1,17 +1,7 @@ import { parse } from '@babel/parser' -import { - generate, - type VaporCodegenResult as BaseVaporCodegenResult, -} from '@vue/compiler-vapor' import { extend, isString } from '@vue/shared' -import { customGenOperation } from './generate' - -import { - IRNodeTypes, - type HackOptions, - type RootIRNode, - type RootNode, -} from './ir' +import { generate, type VaporCodegenResult } from './generate' +import { IRNodeTypes, type HackOptions, type RootNode } from './ir' import { transform, type DirectiveTransform, @@ -35,22 +25,12 @@ import { transformVText } from './transforms/vText' import type { ExpressionStatement, JSXElement, JSXFragment } from '@babel/types' import type { CompilerOptions as BaseCompilerOptions } from '@vue/compiler-dom' -export { generate } - -export interface VaporCodegenResult - extends Omit { - ast: RootIRNode - customHelpers: Set -} - // code/AST -> IR (transform) -> JS (generate) export function compile( source: JSXElement | JSXFragment | string, options: CompilerOptions = {}, ): VaporCodegenResult { const resolvedOptions = extend({}, options, { - inline: true, - prefixIdentifiers: false, expressionPlugins: options.expressionPlugins || ['jsx'], }) if (!resolvedOptions.source && isString(source)) { @@ -101,14 +81,12 @@ export function compile( }), ) - return generate(ir as any, { - ...resolvedOptions, - customGenOperation, - }) as unknown as VaporCodegenResult + return generate(ir, resolvedOptions) } export type CompilerOptions = HackOptions & { source?: string + templates?: string[] } export type TransformPreset = [ NodeTransform[], diff --git a/packages/compiler/src/generate.ts b/packages/compiler/src/generate.ts index e2251f5..23f500d 100644 --- a/packages/compiler/src/generate.ts +++ b/packages/compiler/src/generate.ts @@ -1,67 +1,138 @@ +import { extend, remove } from '@vue/shared' +import { genBlockContent } from './generators/block' +import { genTemplates } from './generators/template' +import { setTemplateRefIdent } from './generators/templateRef' import { - genCall, - genExpression, + buildCodeFragment, + codeFragmentToString, + INDENT_END, + INDENT_START, NEWLINE, - type CodeFragment, - type CodegenContext, -} from '@vue/compiler-vapor' -import { - IRNodeTypes, - type CreateNodesIRNode, - type OperationNode, - type SetNodesIRNode, -} from './ir' -import type { SimpleExpressionNode } from '@vue/compiler-dom' - -export const customGenOperation = ( - oper: OperationNode, - context: CodegenContext, -) => { - if (oper.type === IRNodeTypes.CREATE_NODES) { - return genCreateNodes(oper, context) - } else if (oper.type === IRNodeTypes.SET_NODES) { - return genSetNodes(oper, context) - } +} from './generators/utils' +import type { BlockIRNode, RootIRNode } from './ir' +import type { + CodegenOptions as BaseCodegenOptions, + BaseCodegenResult, + SimpleExpressionNode, +} from '@vue/compiler-dom' + +export type CodegenOptions = Omit< + BaseCodegenOptions, + 'optimizeImports' | 'inline' | 'bindingMetadata' | 'prefixIdentifiers' +> & { + templates?: string[] } -export function genSetNodes( - oper: SetNodesIRNode, - context: CodegenContext, -): CodeFragment[] { - const { helper } = context - const { element, values, generated } = oper - return [ - NEWLINE, - ...genCall( - helper('setNodes'), - `${generated ? 'x' : 'n'}${element}`, - combineValues(values, context), - ), - ] +export class CodegenContext { + options: Required + + helpers: Set = new Set([]) + + helper = (name: string) => { + this.helpers.add(name) + return `_${name}` + } + + delegates: Set = new Set() + + identifiers: Record = + Object.create(null) + + seenInlineHandlerNames: Record = Object.create(null) + + block: BlockIRNode + withId( + fn: () => T, + map: Record, + ): T { + const { identifiers } = this + const ids = Object.keys(map) + + for (const id of ids) { + identifiers[id] ||= [] + identifiers[id].unshift(map[id] || id) + } + + const ret = fn() + ids.forEach((id) => remove(identifiers[id], map[id] || id)) + + return ret + } + + enterBlock(block: BlockIRNode) { + const parent = this.block + this.block = block + return (): BlockIRNode => (this.block = parent) + } + + scopeLevel: number = 0 + enterScope(): [level: number, exit: () => number] { + return [this.scopeLevel++, () => this.scopeLevel--] as const + } + + constructor( + public ir: RootIRNode, + options: CodegenOptions, + ) { + const defaultOptions: Required = { + mode: 'module', + sourceMap: false, + filename: `template.vue.html`, + scopeId: null, + runtimeGlobalName: `Vue`, + runtimeModuleName: `vue`, + ssrRuntimeModuleName: 'vue/server-renderer', + ssr: false, + isTS: false, + inSSR: false, + templates: [], + expressionPlugins: [], + } + this.options = extend(defaultOptions, options) + this.block = ir.block + } } -export function genCreateNodes( - oper: CreateNodesIRNode, - context: CodegenContext, -): CodeFragment[] { - const { helper } = context - const { id, values } = oper - return [ - NEWLINE, - `const n${id} = `, - ...genCall(helper('createNodes'), values && combineValues(values, context)), - ] +export interface VaporCodegenResult + extends Omit { + ast: RootIRNode + helpers: Set + templates: string[] + delegates: Set } -function combineValues( - values: SimpleExpressionNode[], - context: CodegenContext, -): CodeFragment[] { - return values.flatMap((value, i) => { - const exp = genExpression(value, context) - if (i > 0) { - exp.unshift(', ') - } - return exp - }) +// IR -> JS codegen +export function generate( + ir: RootIRNode, + options: CodegenOptions = {}, +): VaporCodegenResult { + const [frag, push] = buildCodeFragment() + const context = new CodegenContext(ir, options) + const { helpers } = context + + push(INDENT_START) + if (ir.hasTemplateRef) { + push( + NEWLINE, + `const ${setTemplateRefIdent} = ${context.helper('createTemplateRefSetter')}()`, + ) + } + push(...genBlockContent(ir.block, context, true)) + push(INDENT_END, NEWLINE) + + if (context.delegates.size) { + context.helper('delegateEvents') + } + const templates = genTemplates(ir.templates, ir.rootTemplateIndex, context) + + const [code, map] = codeFragmentToString(frag, context) + + return { + code, + ast: ir, + map: map && map.toJSON(), + helpers, + templates, + delegates: context.delegates, + } } diff --git a/packages/compiler/src/generators/block.ts b/packages/compiler/src/generators/block.ts new file mode 100644 index 0000000..ab86abd --- /dev/null +++ b/packages/compiler/src/generators/block.ts @@ -0,0 +1,96 @@ +import { toValidAssetId } from '@vue/compiler-dom' +import type { CodegenContext } from '../generate' +import type { BlockIRNode } from '../ir' +import { genEffects, genOperations } from './operation' +import { genChildren, genSelf } from './template' +import { + buildCodeFragment, + DELIMITERS_ARRAY, + genCall, + genMulti, + INDENT_END, + INDENT_START, + NEWLINE, + type CodeFragment, +} from './utils' + +export function genBlock( + oper: BlockIRNode, + context: CodegenContext, + args: CodeFragment[] = [], + root?: boolean, + customReturns?: (returns: CodeFragment[]) => CodeFragment[], +): CodeFragment[] { + return [ + '(', + ...args, + ') => {', + INDENT_START, + ...genBlockContent(oper, context, root, customReturns), + INDENT_END, + NEWLINE, + '}', + ] +} + +export function genBlockContent( + block: BlockIRNode, + context: CodegenContext, + root?: boolean, + customReturns?: (returns: CodeFragment[]) => CodeFragment[], +): CodeFragment[] { + const [frag, push] = buildCodeFragment() + const { dynamic, effect, operation, returns } = block + const resetBlock = context.enterBlock(block) + + if (root) { + for (let name of context.ir.component) { + const id = toValidAssetId(name, 'component') + const maybeSelfReference = name.endsWith('__self') + if (maybeSelfReference) name = name.slice(0, -6) + push( + NEWLINE, + `const ${id} = `, + ...genCall( + context.helper('resolveComponent'), + JSON.stringify(name), + // pass additional `maybeSelfReference` flag + maybeSelfReference ? 'true' : undefined, + ), + ) + } + genResolveAssets('directive', 'resolveDirective') + } + + for (const child of dynamic.children) { + push(...genSelf(child, context)) + } + for (const child of dynamic.children) { + push(...genChildren(child, context, push, `n${child.id!}`)) + } + + push(...genOperations(operation, context)) + push(...genEffects(effect, context)) + + push(NEWLINE, `return `) + + const returnNodes = returns.map((n) => `n${n}`) + const returnsCode: CodeFragment[] = + returnNodes.length > 1 + ? genMulti(DELIMITERS_ARRAY, ...returnNodes) + : [returnNodes[0] || 'null'] + push(...(customReturns ? customReturns(returnsCode) : returnsCode)) + + resetBlock() + return frag + + function genResolveAssets(kind: 'component' | 'directive', helper: string) { + for (const name of context.ir[kind]) { + push( + NEWLINE, + `const ${toValidAssetId(name, kind)} = `, + ...genCall(context.helper(helper), JSON.stringify(name)), + ) + } + } +} diff --git a/packages/compiler/src/generators/component.ts b/packages/compiler/src/generators/component.ts new file mode 100644 index 0000000..2144b74 --- /dev/null +++ b/packages/compiler/src/generators/component.ts @@ -0,0 +1,429 @@ +import { + createSimpleExpression, + isMemberExpression, + toValidAssetId, + type SimpleExpressionNode, +} from '@vue/compiler-dom' +import { camelize, extend, isArray } from '@vue/shared' +import { walkIdentifiers } from 'ast-kit' +import { + IRDynamicPropsKind, + IRSlotType, + type CreateComponentIRNode, + type IRProp, + type IRProps, + type IRPropsStatic, + type IRSlotDynamic, + type IRSlotDynamicBasic, + type IRSlotDynamicConditional, + type IRSlotDynamicLoop, + type IRSlots, + type IRSlotsStatic, + type SlotBlockIRNode, +} from '../ir' +import type { CodegenContext } from '../generate' +import { genBlock } from './block' +import { genDirectiveModifiers, genDirectivesForElement } from './directive' +import { genEventHandler } from './event' +import { genExpression } from './expression' +import { genPropKey, genPropValue } from './prop' +import { + DELIMITERS_ARRAY_NEWLINE, + DELIMITERS_OBJECT, + DELIMITERS_OBJECT_NEWLINE, + genCall, + genMulti, + INDENT_END, + INDENT_START, + NEWLINE, + type CodeFragment, +} from './utils' +import { genModelHandler } from './vModel' + +export function genCreateComponent( + operation: CreateComponentIRNode, + context: CodegenContext, +): CodeFragment[] { + const { helper } = context + + const tag = genTag() + const { root, props, slots, once } = operation + const rawSlots = genRawSlots(slots, context) + const [ids, handlers] = processInlineHandlers(props, context) + const rawProps = context.withId(() => genRawProps(props, context), ids) + + const inlineHandlers: CodeFragment[] = handlers.reduce( + (acc, { name, value }) => { + const handler = genEventHandler(context, value, undefined, false) + return [...acc, `const ${name} = `, ...handler, NEWLINE] + }, + [], + ) + + return [ + NEWLINE, + ...inlineHandlers, + `const n${operation.id} = `, + ...genCall( + operation.dynamic && !operation.dynamic.isStatic + ? helper('createDynamicComponent') + : operation.asset + ? helper('createComponentWithFallback') + : helper('createComponent'), + tag, + rawProps, + rawSlots, + root ? 'true' : false, + once && 'true', + ), + ...genDirectivesForElement(operation.id, context), + ] + + function genTag() { + if (operation.dynamic) { + if (operation.dynamic.isStatic) { + return genCall( + helper('resolveDynamicComponent'), + genExpression(operation.dynamic, context), + ) + } else { + return ['() => (', ...genExpression(operation.dynamic, context), ')'] + } + } else if (operation.asset) { + return toValidAssetId(operation.tag, 'component') + } else { + return genExpression( + extend(createSimpleExpression(operation.tag, false), { ast: null }), + context, + ) + } + } +} + +function getUniqueHandlerName(context: CodegenContext, name: string): string { + const { seenInlineHandlerNames } = context + const count = seenInlineHandlerNames[name] || 0 + seenInlineHandlerNames[name] = count + 1 + return count === 0 ? name : `${name}${count}` +} + +type InlineHandler = { + name: string + value: SimpleExpressionNode +} + +function processInlineHandlers( + props: IRProps[], + context: CodegenContext, +): [Record, InlineHandler[]] { + const ids: Record = Object.create(null) + const handlers: InlineHandler[] = [] + const staticProps = props[0] + if (isArray(staticProps)) { + for (const prop of staticProps) { + if (!prop.handler) continue + prop.values.forEach((value, i) => { + const isMemberExp = isMemberExpression(value, context.options) + // cache inline handlers (fn expression or inline statement) + if (!isMemberExp) { + const name = getUniqueHandlerName(context, `_on_${prop.key.content}`) + handlers.push({ name, value }) + ids[name] = null + // replace the original prop value with the handler name + prop.values[i] = extend({ ast: null }, createSimpleExpression(name)) + } + }) + } + } + return [ids, handlers] +} + +export function genRawProps( + props: IRProps[], + context: CodegenContext, +): CodeFragment[] | undefined { + const staticProps = props[0] + if (isArray(staticProps)) { + if (!staticProps.length && props.length === 1) { + return + } + return genStaticProps( + staticProps, + context, + genDynamicProps(props.slice(1), context), + ) + } else if (props.length) { + // all dynamic + return genStaticProps([], context, genDynamicProps(props, context)) + } +} + +function genStaticProps( + props: IRPropsStatic, + context: CodegenContext, + dynamicProps?: CodeFragment[], +): CodeFragment[] { + const args = props.map((prop) => genProp(prop, context, true)) + if (dynamicProps) { + args.push([`$: `, ...dynamicProps]) + } + return genMulti( + args.length > 1 ? DELIMITERS_OBJECT_NEWLINE : DELIMITERS_OBJECT, + ...args, + ) +} + +function genDynamicProps( + props: IRProps[], + context: CodegenContext, +): CodeFragment[] | undefined { + const { helper } = context + const frags: CodeFragment[][] = [] + for (const p of props) { + let expr: CodeFragment[] + if (isArray(p)) { + if (p.length) { + frags.push(genStaticProps(p, context)) + } + continue + } else if (p.kind === IRDynamicPropsKind.ATTRIBUTE) + expr = genMulti(DELIMITERS_OBJECT, genProp(p, context)) + else { + expr = genExpression(p.value, context) + if (p.handler) expr = genCall(helper('toHandlers'), expr) + } + frags.push(['() => (', ...expr, ')']) + } + if (frags.length) { + return genMulti(DELIMITERS_ARRAY_NEWLINE, ...frags) + } +} + +function genProp(prop: IRProp, context: CodegenContext, isStatic?: boolean) { + const values = genPropValue(prop.values, context) + return [ + ...genPropKey(prop, context), + ': ', + ...(prop.handler + ? genEventHandler( + context, + prop.values[0], + prop.handlerModifiers, + true /* wrap handlers passed to components */, + ) + : isStatic + ? ['() => (', ...values, ')'] + : values), + ...(prop.model + ? [...genModelEvent(prop, context), ...genModelModifiers(prop, context)] + : []), + ] +} + +function genModelEvent(prop: IRProp, context: CodegenContext): CodeFragment[] { + const name = prop.key.isStatic + ? [JSON.stringify(`onUpdate:${camelize(prop.key.content)}`)] + : ['["onUpdate:" + ', ...genExpression(prop.key, context), ']'] + const handler = genModelHandler(prop.values[0], context) + + return [',', NEWLINE, ...name, ': () => ', ...handler] +} + +function genModelModifiers( + prop: IRProp, + context: CodegenContext, +): CodeFragment[] { + const { key, modelModifiers } = prop + if (!modelModifiers || !modelModifiers.length) return [] + + const modifiersKey = key.isStatic + ? [`${key.content}Modifiers`] + : ['[', ...genExpression(key, context), ' + "Modifiers"]'] + + const modifiersVal = genDirectiveModifiers(modelModifiers) + return [',', NEWLINE, ...modifiersKey, `: () => ({ ${modifiersVal} })`] +} + +function genRawSlots(slots: IRSlots[], context: CodegenContext) { + if (!slots.length) return + const staticSlots = slots[0] + if (staticSlots.slotType === IRSlotType.STATIC) { + // single static slot + return genStaticSlots( + staticSlots, + context, + slots.length > 1 ? slots.slice(1) : undefined, + ) + } else { + return genStaticSlots( + { slotType: IRSlotType.STATIC, slots: {} }, + context, + slots, + ) + } +} + +function genStaticSlots( + { slots }: IRSlotsStatic, + context: CodegenContext, + dynamicSlots?: IRSlots[], +) { + const args = Object.keys(slots).map((name) => [ + `${JSON.stringify(name)}: `, + ...genSlotBlockWithProps(slots[name], context), + ]) + if (dynamicSlots) { + args.push([`$: `, ...genDynamicSlots(dynamicSlots, context)]) + } + return genMulti(DELIMITERS_OBJECT_NEWLINE, ...args) +} + +function genDynamicSlots( + slots: IRSlots[], + context: CodegenContext, +): CodeFragment[] { + return genMulti( + DELIMITERS_ARRAY_NEWLINE, + ...slots.map((slot) => + slot.slotType === IRSlotType.STATIC + ? genStaticSlots(slot, context) + : slot.slotType === IRSlotType.EXPRESSION + ? slot.slots.content + : genDynamicSlot(slot, context, true), + ), + ) +} + +function genDynamicSlot( + slot: IRSlotDynamic, + context: CodegenContext, + withFunction = false, +): CodeFragment[] { + let frag: CodeFragment[] + switch (slot.slotType) { + case IRSlotType.DYNAMIC: + frag = genBasicDynamicSlot(slot, context) + break + case IRSlotType.LOOP: + frag = genLoopSlot(slot, context) + break + case IRSlotType.CONDITIONAL: + frag = genConditionalSlot(slot, context) + break + } + return withFunction ? ['() => (', ...frag, ')'] : frag +} + +function genBasicDynamicSlot( + slot: IRSlotDynamicBasic, + context: CodegenContext, +): CodeFragment[] { + const { name, fn } = slot + return genMulti( + DELIMITERS_OBJECT_NEWLINE, + ['name: ', ...genExpression(name, context)], + ['fn: ', ...genSlotBlockWithProps(fn, context)], + ) +} + +function genLoopSlot( + slot: IRSlotDynamicLoop, + context: CodegenContext, +): CodeFragment[] { + const { name, fn, loop } = slot + const { value, key, index, source } = loop + const rawValue = value && value.content + const rawKey = key && key.content + const rawIndex = index && index.content + + const idMap: Record = {} + if (rawValue) idMap[rawValue] = rawValue + if (rawKey) idMap[rawKey] = rawKey + if (rawIndex) idMap[rawIndex] = rawIndex + const slotExpr = genMulti( + DELIMITERS_OBJECT_NEWLINE, + ['name: ', ...context.withId(() => genExpression(name, context), idMap)], + [ + 'fn: ', + ...context.withId(() => genSlotBlockWithProps(fn, context), idMap), + ], + ) + return [ + ...genCall( + context.helper('createForSlots'), + genExpression(source, context), + [ + ...genMulti( + ['(', ')', ', '], + rawValue ? rawValue : rawKey || rawIndex ? '_' : undefined, + rawKey ? rawKey : rawIndex ? '__' : undefined, + rawIndex, + ), + ' => (', + ...slotExpr, + ')', + ], + ), + ] +} + +function genConditionalSlot( + slot: IRSlotDynamicConditional, + context: CodegenContext, +): CodeFragment[] { + const { condition, positive, negative } = slot + return [ + ...genExpression(condition, context), + INDENT_START, + NEWLINE, + '? ', + ...genDynamicSlot(positive, context), + NEWLINE, + ': ', + ...(negative ? [...genDynamicSlot(negative, context)] : ['void 0']), + INDENT_END, + ] +} + +function genSlotBlockWithProps(oper: SlotBlockIRNode, context: CodegenContext) { + let isDestructureAssignment = false + let rawProps: string | undefined + let propsName: string | undefined + let exitScope: (() => void) | undefined + let depth: number | undefined + const { props } = oper + const idsOfProps = new Set() + + if (props) { + rawProps = props.content + if ((isDestructureAssignment = !!props.ast)) { + ;[depth, exitScope] = context.enterScope() + propsName = `_slotProps${depth}` + walkIdentifiers( + props.ast, + (id, _, __, ___, isLocal) => { + if (isLocal) idsOfProps.add(id.name) + }, + true, + ) + } else { + idsOfProps.add((propsName = rawProps)) + } + } + + const idMap: Record = {} + + idsOfProps.forEach( + (id) => + (idMap[id] = isDestructureAssignment + ? `${propsName}[${JSON.stringify(id)}]` + : null), + ) + const blockFn = context.withId( + () => genBlock(oper, context, [propsName]), + idMap, + ) + exitScope && exitScope() + + return blockFn +} diff --git a/packages/compiler/src/generators/directive.ts b/packages/compiler/src/generators/directive.ts new file mode 100644 index 0000000..bd752da --- /dev/null +++ b/packages/compiler/src/generators/directive.ts @@ -0,0 +1,111 @@ +import { + createSimpleExpression, + isSimpleIdentifier, + toValidAssetId, +} from '@vue/compiler-dom' +import { extend } from '@vue/shared' +import { IRNodeTypes, type DirectiveIRNode, type OperationNode } from '../ir' +import type { CodegenContext } from '../generate' +import { genExpression } from './expression' +import { + DELIMITERS_ARRAY, + genCall, + genMulti, + NEWLINE, + type CodeFragment, + type CodeFragmentDelimiters, +} from './utils' +import { genVModel } from './vModel' +import { genVShow } from './vShow' + +export function genBuiltinDirective( + oper: DirectiveIRNode, + context: CodegenContext, +): CodeFragment[] { + switch (oper.name) { + case 'show': + return genVShow(oper, context) + case 'model': + return genVModel(oper, context) + default: + return [] + } +} + +/** + * user directives via `withVaporDirectives` + * TODO the compiler side is implemented but no runtime support yet + * it was removed due to perf issues + */ +export function genDirectivesForElement( + id: number, + context: CodegenContext, +): CodeFragment[] { + const dirs = filterCustomDirectives(id, context.block.operation) + return dirs.length ? genCustomDirectives(dirs, context) : [] +} + +function genCustomDirectives( + opers: DirectiveIRNode[], + context: CodegenContext, +): CodeFragment[] { + const { helper } = context + + const element = `n${opers[0].element}` + const directiveItems = opers.map(genDirectiveItem) + const directives = genMulti(DELIMITERS_ARRAY, ...directiveItems) + + return [ + NEWLINE, + ...genCall(helper('withVaporDirectives'), element, directives), + ] + + function genDirectiveItem({ + dir, + name, + asset, + }: DirectiveIRNode): CodeFragment[] { + const directiveVar = asset + ? toValidAssetId(name, 'directive') + : genExpression( + extend(createSimpleExpression(name, false), { ast: null }), + context, + ) + const value = dir.exp && ['() => ', ...genExpression(dir.exp, context)] + const argument = dir.arg && genExpression(dir.arg, context) + const modifiers = !!dir.modifiers.length && [ + '{ ', + genDirectiveModifiers(dir.modifiers.map((m) => m.content)), + ' }', + ] + + return genMulti( + DELIMITERS_ARRAY.concat('void 0') as CodeFragmentDelimiters, + directiveVar, + value, + argument, + modifiers, + ) + } +} + +export function genDirectiveModifiers(modifiers: string[]): string { + return modifiers + .map( + (value) => + `${isSimpleIdentifier(value) ? value : JSON.stringify(value)}: true`, + ) + .join(', ') +} + +function filterCustomDirectives( + id: number, + operations: OperationNode[], +): DirectiveIRNode[] { + return operations.filter( + (oper): oper is DirectiveIRNode => + oper.type === IRNodeTypes.DIRECTIVE && + oper.element === id && + !oper.builtin, + ) +} diff --git a/packages/compiler/src/generators/dom.ts b/packages/compiler/src/generators/dom.ts new file mode 100644 index 0000000..571dba5 --- /dev/null +++ b/packages/compiler/src/generators/dom.ts @@ -0,0 +1,34 @@ +import type { CodegenContext } from '../generate' +import type { InsertNodeIRNode, PrependNodeIRNode } from '../ir' +import { genCall, NEWLINE, type CodeFragment } from './utils' + +export function genInsertNode( + { parent, elements, anchor }: InsertNodeIRNode, + { helper }: CodegenContext, +): CodeFragment[] { + let element = elements.map((el) => `n${el}`).join(', ') + if (elements.length > 1) element = `[${element}]` + return [ + NEWLINE, + ...genCall( + helper('insert'), + element, + `n${parent}`, + anchor === undefined ? undefined : `n${anchor}`, + ), + ] +} + +export function genPrependNode( + oper: PrependNodeIRNode, + { helper }: CodegenContext, +): CodeFragment[] { + return [ + NEWLINE, + ...genCall( + helper('prepend'), + `n${oper.parent}`, + ...oper.elements.map((el) => `n${el}`), + ), + ] +} diff --git a/packages/compiler/src/generators/event.ts b/packages/compiler/src/generators/event.ts new file mode 100644 index 0000000..8d9a40f --- /dev/null +++ b/packages/compiler/src/generators/event.ts @@ -0,0 +1,180 @@ +import { + isFnExpression, + isMemberExpression, + type SimpleExpressionNode, +} from '@vue/compiler-dom' +import { + IRNodeTypes, + type OperationNode, + type SetDynamicEventsIRNode, + type SetEventIRNode, +} from '../ir' +import type { CodegenContext } from '../generate' +import { genExpression } from './expression' +import { + DELIMITERS_OBJECT_NEWLINE, + genCall, + genMulti, + NEWLINE, + type CodeFragment, +} from './utils' + +export function genSetEvent( + oper: SetEventIRNode, + context: CodegenContext, +): CodeFragment[] { + const { helper } = context + const { element, key, keyOverride, value, modifiers, delegate, effect } = oper + + const name = genName() + const handler = genEventHandler(context, value, modifiers) + const eventOptions = genEventOptions() + + if (delegate) { + // key is static + context.delegates.add(key.content) + // if this is the only delegated event of this name on this element, + // we can generate optimized handler attachment code + // e.g. n1.$evtclick = () => {} + if (!context.block.operation.some(isSameDelegateEvent)) { + return [NEWLINE, `n${element}.$evt${key.content} = `, ...handler] + } + } + + return [ + NEWLINE, + ...genCall( + helper(delegate ? 'delegate' : 'on'), + `n${element}`, + name, + handler, + eventOptions, + ), + ] + + function genName(): CodeFragment[] { + const expr = genExpression(key, context) + if (keyOverride) { + // TODO unit test + const find = JSON.stringify(keyOverride[0]) + const replacement = JSON.stringify(keyOverride[1]) + const wrapped: CodeFragment[] = ['(', ...expr, ')'] + return [...wrapped, ` === ${find} ? ${replacement} : `, ...wrapped] + } else { + return genExpression(key, context) + } + } + + function genEventOptions(): CodeFragment[] | undefined { + const { options } = modifiers + if (!options.length && !effect) return + + return genMulti( + DELIMITERS_OBJECT_NEWLINE, + effect && ['effect: true'], + ...options.map((option): CodeFragment[] => [`${option}: true`]), + ) + } + + function isSameDelegateEvent(op: OperationNode) { + if ( + op.type === IRNodeTypes.SET_EVENT && + op !== oper && + op.delegate && + op.element === oper.element && + op.key.content === key.content + ) { + return true + } + } +} + +export function genSetDynamicEvents( + oper: SetDynamicEventsIRNode, + context: CodegenContext, +): CodeFragment[] { + const { helper } = context + return [ + NEWLINE, + ...genCall( + helper('setDynamicEvents'), + `n${oper.element}`, + genExpression(oper.event, context), + ), + ] +} + +export function genEventHandler( + context: CodegenContext, + value: SimpleExpressionNode | undefined, + modifiers: { + nonKeys: string[] + keys: string[] + } = { nonKeys: [], keys: [] }, + // passed as component prop - need additional wrap + extraWrap: boolean = false, +): CodeFragment[] { + let handlerExp: CodeFragment[] = [`() => {}`] + if (value && value.content.trim()) { + // Determine how the handler should be wrapped so it always reference the + // latest value when invoked. + if (isMemberExpression(value, context.options)) { + // e.g. @click="foo.bar" + handlerExp = genExpression(value, context) + if (!extraWrap) { + // non constant, wrap with invocation as `e => foo.bar(e)` + // when passing as component handler, access is always dynamic so we + // can skip this + handlerExp = [`e => `, ...handlerExp, `(e)`] + } + } else if (isFnExpression(value, context.options)) { + // Fn expression: @click="e => foo(e)" + // no need to wrap in this case + handlerExp = genExpression(value, context) + } else { + // inline statement + // @click="foo($event)" ---> $event => foo($event) + const referencesEvent = value.content.includes('$event') + const hasMultipleStatements = value.content.includes(`;`) + const expr = referencesEvent + ? context.withId(() => genExpression(value, context), { + $event: null, + }) + : genExpression(value, context) + handlerExp = [ + referencesEvent ? '$event => ' : '() => ', + hasMultipleStatements ? '{' : '(', + ...expr, + hasMultipleStatements ? '}' : ')', + ] + } + } + + const { keys, nonKeys } = modifiers + if (nonKeys.length) + handlerExp = genWithModifiers(context, handlerExp, nonKeys) + if (keys.length) handlerExp = genWithKeys(context, handlerExp, keys) + + if (extraWrap) handlerExp.unshift(`() => `) + return handlerExp +} + +function genWithModifiers( + context: CodegenContext, + handler: CodeFragment[], + nonKeys: string[], +): CodeFragment[] { + return genCall( + context.helper('withModifiers'), + handler, + JSON.stringify(nonKeys), + ) +} + +function genWithKeys( + context: CodegenContext, + handler: CodeFragment[], + keys: string[], +): CodeFragment[] { + return genCall(context.helper('withKeys'), handler, JSON.stringify(keys)) +} diff --git a/packages/compiler/src/generators/expression.ts b/packages/compiler/src/generators/expression.ts new file mode 100644 index 0000000..50fc67e --- /dev/null +++ b/packages/compiler/src/generators/expression.ts @@ -0,0 +1,151 @@ +import { + advancePositionWithClone, + isStaticProperty, + NewlineType, + TS_NODE_TYPES, + type SimpleExpressionNode, + type SourceLocation, +} from '@vue/compiler-dom' +import { isString } from '@vue/shared' +import { walkIdentifiers } from 'ast-kit' +import { isConstantExpression } from '../utils' +import type { CodegenContext } from '../generate' +import { buildCodeFragment, type CodeFragment } from './utils' +import type { Identifier, Node } from '@babel/types' + +export function genExpression( + node: SimpleExpressionNode, + context: CodegenContext, + assignment?: string, +): CodeFragment[] { + const { content, ast, isStatic, loc } = node + + if (isStatic) { + return [[JSON.stringify(content), NewlineType.None, loc]] + } + + if ( + !node.content.trim() || + // there was a parsing error + ast === false || + isConstantExpression(node) + ) { + return [[content, NewlineType.None, loc], assignment && ` = ${assignment}`] + } + + // the expression is a simple identifier + if (ast === null) { + return genIdentifier(content, context, loc, assignment) + } + + const ids: Identifier[] = [] + const parentStackMap = new Map() + const parentStack: Node[] = [] + walkIdentifiers( + ast!, + (id) => { + ids.push(id) + parentStackMap.set(id, parentStack.slice()) + }, + false, + parentStack, + ) + + let hasMemberExpression = false + if (ids.length) { + const [frag, push] = buildCodeFragment() + const isTSNode = ast && TS_NODE_TYPES.includes(ast.type) + const offset = + (ast?.start ? ast.start - 1 : 0) - ((ast as any)._offset || 0) + ids + .sort((a, b) => a.start! - b.start!) + .forEach((id, i) => { + // range is offset by -1 due to the wrapping parens when parsed + const start = id.start! - 1 - offset + const end = id.end! - 1 - offset + const last = ids[i - 1] + + if (!isTSNode || i !== 0) { + const leadingText = content.slice( + last ? last.end! - 1 - offset : 0, + start, + ) + if (leadingText.length) push([leadingText, NewlineType.Unknown]) + } + + const source = content.slice(start, end) + const parentStack = parentStackMap.get(id)! + const parent = parentStack.at(-1) + + hasMemberExpression ||= + !!parent && + (parent.type === 'MemberExpression' || + parent.type === 'OptionalMemberExpression') + + push( + ...genIdentifier( + source, + context, + { + start: advancePositionWithClone(node.loc.start, source, start), + end: advancePositionWithClone(node.loc.start, source, end), + source, + }, + hasMemberExpression ? undefined : assignment, + parent, + ), + ) + + if (i === ids.length - 1 && end < content.length && !isTSNode) { + push([content.slice(end), NewlineType.Unknown]) + } + }) + + if (assignment && hasMemberExpression) { + push(` = ${assignment}`) + } + return frag + } else { + return [[content, NewlineType.Unknown, loc]] + } +} + +function genIdentifier( + raw: string, + context: CodegenContext, + loc?: SourceLocation, + assignment?: string, + parent?: Node, +): CodeFragment[] { + const { identifiers } = context + const name: string | undefined = raw + + const idMap = identifiers[raw] + if (idMap && idMap.length) { + const replacement = idMap[0] + if (isString(replacement)) { + if (parent && parent.type === 'ObjectProperty' && parent.shorthand) { + return [[`${name}: ${replacement}`, NewlineType.None, loc]] + } else { + return [[replacement, NewlineType.None, loc]] + } + } else { + // replacement is an expression - process it again + return genExpression(replacement, context, assignment) + } + } + + let prefix: string | undefined + if (isStaticProperty(parent) && parent.shorthand) { + // property shorthand like { foo }, we need to add the key since + // we rewrite the value + prefix = `${raw}: ` + } + + raw = withAssignment(raw) + return [prefix, [raw, NewlineType.None, loc, name]] + + function withAssignment(s: string) { + return assignment ? `${s} = ${assignment}` : s + } +} diff --git a/packages/compiler/src/generators/for.ts b/packages/compiler/src/generators/for.ts new file mode 100644 index 0000000..d5bb945 --- /dev/null +++ b/packages/compiler/src/generators/for.ts @@ -0,0 +1,253 @@ +import { parseExpression } from '@babel/parser' +import { + createSimpleExpression, + walkIdentifiers, + type SimpleExpressionNode, +} from '@vue/compiler-dom' +import type { CodegenContext } from '../generate' +import type { ForIRNode } from '../ir' +import { genBlock } from './block' +import { genExpression } from './expression' +import { genCall, genMulti, NEWLINE, type CodeFragment } from './utils' +import type { Identifier } from '@babel/types' + +/** + * Flags to optimize vapor `createFor` runtime behavior, shared between the + * compiler and the runtime + */ +export enum VaporVForFlags { + /** + * v-for is the only child of a parent container, so it can take the fast + * path with textContent = '' when the whole list is emptied + */ + FAST_REMOVE = 1, + /** + * v-for used on component - we can skip creating child scopes for each block + * because the component itself already has a scope. + */ + IS_COMPONENT = 1 << 1, + /** + * v-for inside v-ince + */ + ONCE = 1 << 2, +} + +export function genFor( + oper: ForIRNode, + context: CodegenContext, +): CodeFragment[] { + const { helper } = context + const { + source, + value, + key, + index, + render, + keyProp, + once, + id, + component, + onlyChild, + } = oper + + let rawValue: string | null = null + const rawKey = key && key.content + const rawIndex = index && index.content + + const sourceExpr = ['() => (', ...genExpression(source, context), ')'] + const idToPathMap = parseValueDestructure() + + const [depth, exitScope] = context.enterScope() + const idMap: Record = {} + + const itemVar = `_for_item${depth}` + idMap[itemVar] = null + + idToPathMap.forEach((pathInfo, id) => { + let path = `${itemVar}.value${pathInfo ? pathInfo.path : ''}` + if (pathInfo) { + if (pathInfo.helper) { + idMap[pathInfo.helper] = null + path = `${pathInfo.helper}(${path}, ${pathInfo.helperArgs})` + } + if (pathInfo.dynamic) { + const node = (idMap[id] = createSimpleExpression(path)) + const plugins = context.options.expressionPlugins + node.ast = parseExpression(`(${path})`, { + plugins: plugins ? [...plugins, 'typescript'] : ['typescript'], + }) + } else { + idMap[id] = path + } + } else { + idMap[id] = path + } + }) + + const args = [itemVar] + if (rawKey) { + const keyVar = `_for_key${depth}` + args.push(`, ${keyVar}`) + idMap[rawKey] = `${keyVar}.value` + idMap[keyVar] = null + } + if (rawIndex) { + const indexVar = `_for_index${depth}` + args.push(`, ${indexVar}`) + idMap[rawIndex] = `${indexVar}.value` + idMap[indexVar] = null + } + + const blockFn = context.withId(() => genBlock(render, context, args), idMap) + exitScope() + + let flags = 0 + if (onlyChild) { + flags |= VaporVForFlags.FAST_REMOVE + } + if (component) { + flags |= VaporVForFlags.IS_COMPONENT + } + if (once) { + flags |= VaporVForFlags.ONCE + } + + return [ + NEWLINE, + `const n${id} = `, + ...genCall( + helper('createFor'), + sourceExpr, + blockFn, + genCallback(keyProp), + flags ? String(flags) : undefined, + // todo: hydrationNode + ), + ] + + // construct a id -> accessor path map. + // e.g. `{ x: { y: [z] }}` -> `Map{ 'z' => '.x.y[0]' }` + function parseValueDestructure() { + const map = new Map< + string, + { + path: string + dynamic: boolean + helper?: string + helperArgs?: string + } | null + >() + if (value) { + rawValue = value && value.content + if (value.ast) { + walkIdentifiers( + value.ast, + (id, _, parentStack, ___, isLocal) => { + if (isLocal) { + let path = '' + let isDynamic = false + let helper + let helperArgs + for (let i = 0; i < parentStack.length; i++) { + const parent = parentStack[i] + const child = parentStack[i + 1] || id + + if ( + parent.type === 'ObjectProperty' && + parent.value === child + ) { + if (parent.key.type === 'StringLiteral') { + path += `[${JSON.stringify(parent.key.value)}]` + } else if (parent.computed) { + isDynamic = true + path += `[${value.content.slice( + parent.key.start! - 1, + parent.key.end! - 1, + )}]` + } else { + // non-computed, can only be identifier + path += `.${(parent.key as Identifier).name}` + } + } else if (parent.type === 'ArrayPattern') { + const index = parent.elements.indexOf(child as any) + if (child.type === 'RestElement') { + path += `.slice(${index})` + } else { + path += `[${index}]` + } + } else if ( + parent.type === 'ObjectPattern' && + child.type === 'RestElement' + ) { + helper = context.helper('getRestElement') + helperArgs = `[${parent.properties + .filter((p) => p.type === 'ObjectProperty') + .map((p) => { + if (p.key.type === 'StringLiteral') { + return JSON.stringify(p.key.value) + } else if (p.computed) { + isDynamic = true + return value.content.slice( + p.key.start! - 1, + p.key.end! - 1, + ) + } else { + return JSON.stringify((p.key as Identifier).name) + } + }) + .join(', ')}]` + } + + // default value + if ( + child.type === 'AssignmentPattern' && + (parent.type === 'ObjectProperty' || + parent.type === 'ArrayPattern') + ) { + isDynamic = true + helper = context.helper('getDefaultValue') + helperArgs = value.content.slice( + child.right.start! - 1, + child.right.end! - 1, + ) + } + } + map.set(id.name, { path, dynamic: isDynamic, helper, helperArgs }) + } + }, + true, + ) + } else { + map.set(rawValue, null) + } + } + return map + } + + function genCallback(expr: SimpleExpressionNode | undefined) { + if (!expr) return false + const res = context.withId( + () => genExpression(expr, context), + genSimpleIdMap(), + ) + return [ + ...genMulti( + ['(', ')', ', '], + rawValue ? rawValue : rawKey || rawIndex ? '_' : undefined, + rawKey ? rawKey : rawIndex ? '__' : undefined, + rawIndex, + ), + ' => (', + ...res, + ')', + ] + } + + function genSimpleIdMap() { + const idMap: Record = {} + if (rawKey) idMap[rawKey] = null + if (rawIndex) idMap[rawIndex] = null + idToPathMap.forEach((_, id) => (idMap[id] = null)) + return idMap + } +} diff --git a/packages/compiler/src/generators/html.ts b/packages/compiler/src/generators/html.ts new file mode 100644 index 0000000..1befd42 --- /dev/null +++ b/packages/compiler/src/generators/html.ts @@ -0,0 +1,16 @@ +import type { CodegenContext } from '../generate' +import type { SetHtmlIRNode } from '../ir' +import { genExpression } from './expression' +import { genCall, NEWLINE, type CodeFragment } from './utils' + +export function genSetHtml( + oper: SetHtmlIRNode, + context: CodegenContext, +): CodeFragment[] { + const { helper } = context + const { value, element } = oper + return [ + NEWLINE, + ...genCall(helper('setHtml'), `n${element}`, genExpression(value, context)), + ] +} diff --git a/packages/compiler/src/generators/if.ts b/packages/compiler/src/generators/if.ts new file mode 100644 index 0000000..c59a752 --- /dev/null +++ b/packages/compiler/src/generators/if.ts @@ -0,0 +1,45 @@ +import { IRNodeTypes, type IfIRNode } from '../ir' +import type { CodegenContext } from '../generate' +import { genBlock } from './block' +import { genExpression } from './expression' +import { buildCodeFragment, genCall, NEWLINE, type CodeFragment } from './utils' + +export function genIf( + oper: IfIRNode, + context: CodegenContext, + isNested = false, +): CodeFragment[] { + const { helper } = context + const { condition, positive, negative, once } = oper + const [frag, push] = buildCodeFragment() + + const conditionExpr: CodeFragment[] = [ + '() => (', + ...genExpression(condition, context), + ')', + ] + + const positiveArg = genBlock(positive, context) + let negativeArg: false | CodeFragment[] = false + + if (negative) { + if (negative.type === IRNodeTypes.BLOCK) { + negativeArg = genBlock(negative, context) + } else { + negativeArg = ['() => ', ...genIf(negative!, context, true)] + } + } + + if (!isNested) push(NEWLINE, `const n${oper.id} = `) + push( + ...genCall( + helper('createIf'), + conditionExpr, + positiveArg, + negativeArg, + once && 'true', + ), + ) + + return frag +} diff --git a/packages/compiler/src/generators/operation.ts b/packages/compiler/src/generators/operation.ts new file mode 100644 index 0000000..8125050 --- /dev/null +++ b/packages/compiler/src/generators/operation.ts @@ -0,0 +1,170 @@ +import { + IRNodeTypes, + isBlockOperation, + type InsertionStateTypes, + type IREffect, + type OperationNode, +} from '../ir' +import type { CodegenContext } from '../generate' +import { genCreateComponent } from './component' +import { genBuiltinDirective } from './directive' +import { genInsertNode, genPrependNode } from './dom' +import { genSetDynamicEvents, genSetEvent } from './event' +import { genFor } from './for' +import { genSetHtml } from './html' +import { genIf } from './if' +import { genDynamicProps, genSetProp } from './prop' +import { genDeclareOldRef, genSetTemplateRef } from './templateRef' +import { + genCreateNodes, + genGetTextChild, + genSetNodes, + genSetText, +} from './text' +import { + buildCodeFragment, + genCall, + INDENT_END, + INDENT_START, + NEWLINE, + type CodeFragment, +} from './utils' + +export function genOperations( + opers: OperationNode[], + context: CodegenContext, +): CodeFragment[] { + const [frag, push] = buildCodeFragment() + for (const operation of opers) { + push(...genOperationWithInsertionState(operation, context)) + } + return frag +} + +export function genOperationWithInsertionState( + oper: OperationNode, + context: CodegenContext, +): CodeFragment[] { + const [frag, push] = buildCodeFragment() + if (isBlockOperation(oper) && oper.parent) { + push(...genInsertionState(oper, context)) + } + push(...genOperation(oper, context)) + return frag +} + +export function genOperation( + oper: OperationNode, + context: CodegenContext, +): CodeFragment[] { + switch (oper.type) { + case IRNodeTypes.SET_PROP: + return genSetProp(oper, context) + case IRNodeTypes.SET_DYNAMIC_PROPS: + return genDynamicProps(oper, context) + case IRNodeTypes.SET_TEXT: + return genSetText(oper, context) + case IRNodeTypes.SET_EVENT: + return genSetEvent(oper, context) + case IRNodeTypes.SET_DYNAMIC_EVENTS: + return genSetDynamicEvents(oper, context) + case IRNodeTypes.SET_HTML: + return genSetHtml(oper, context) + case IRNodeTypes.SET_TEMPLATE_REF: + return genSetTemplateRef(oper, context) + case IRNodeTypes.INSERT_NODE: + return genInsertNode(oper, context) + case IRNodeTypes.PREPEND_NODE: + return genPrependNode(oper, context) + case IRNodeTypes.IF: + return genIf(oper, context) + case IRNodeTypes.FOR: + return genFor(oper, context) + case IRNodeTypes.CREATE_COMPONENT_NODE: + return genCreateComponent(oper, context) + case IRNodeTypes.DECLARE_OLD_REF: + return genDeclareOldRef(oper) + case IRNodeTypes.SLOT_OUTLET_NODE: + return [] + case IRNodeTypes.DIRECTIVE: + return genBuiltinDirective(oper, context) + case IRNodeTypes.GET_TEXT_CHILD: + return genGetTextChild(oper, context) + case IRNodeTypes.SET_NODES: + return genSetNodes(oper, context) + case IRNodeTypes.CREATE_NODES: + return genCreateNodes(oper, context) + default: { + const exhaustiveCheck = oper + throw new Error( + `Unhandled operation type in genOperation: ${exhaustiveCheck}`, + ) + } + } +} + +export function genEffects( + effects: IREffect[], + context: CodegenContext, +): CodeFragment[] { + const { helper } = context + const [frag, push, unshift] = buildCodeFragment() + let operationsCount = 0 + for (const [i, effect] of effects.entries()) { + operationsCount += effect.operations.length + const frags = genEffect(effect, context) + i > 0 && push(NEWLINE) + if (frag.at(-1) === ')' && frags[0] === '(') { + push(';') + } + push(...frags) + } + + const newLineCount = frag.filter((frag) => frag === NEWLINE).length + if (newLineCount > 1 || operationsCount > 1) { + unshift(`{`, INDENT_START, NEWLINE) + push(INDENT_END, NEWLINE, '}') + } + + if (effects.length) { + unshift(NEWLINE, `${helper('renderEffect')}(() => `) + push(`)`) + } + + return frag +} + +export function genEffect( + { operations }: IREffect, + context: CodegenContext, +): CodeFragment[] { + const [frag, push] = buildCodeFragment() + const operationsExps = genOperations(operations, context) + const newlineCount = operationsExps.filter((frag) => frag === NEWLINE).length + + if (newlineCount > 1) { + push(...operationsExps) + } else { + push(...operationsExps.filter((frag) => frag !== NEWLINE)) + } + + return frag +} + +function genInsertionState( + operation: InsertionStateTypes, + context: CodegenContext, +): CodeFragment[] { + return [ + NEWLINE, + ...genCall( + context.helper('setInsertionState'), + `n${operation.parent}`, + operation.anchor == null + ? undefined + : operation.anchor === -1 // -1 indicates prepend + ? `0` // runtime anchor value for prepend + : `n${operation.anchor}`, + ), + ] +} diff --git a/packages/compiler/src/generators/prop.ts b/packages/compiler/src/generators/prop.ts new file mode 100644 index 0000000..1faf01e --- /dev/null +++ b/packages/compiler/src/generators/prop.ts @@ -0,0 +1,225 @@ +import { + isSimpleIdentifier, + NewlineType, + type SimpleExpressionNode, +} from '@vue/compiler-dom' +import { + canSetValueDirectly, + capitalize, + isSVGTag, + shouldSetAsAttr, + toHandlerKey, +} from '@vue/shared' +import { + IRDynamicPropsKind, + type IRProp, + type SetDynamicPropsIRNode, + type SetPropIRNode, +} from '../ir' +import type { CodegenContext } from '../generate' +import { genExpression } from './expression' +import { + DELIMITERS_ARRAY, + DELIMITERS_OBJECT, + genCall, + genMulti, + NEWLINE, + type CodeFragment, +} from './utils' + +export type HelperConfig = { + name: string + needKey?: boolean + acceptRoot?: boolean +} + +// this should be kept in sync with runtime-vapor/src/dom/prop.ts +const helpers = { + setText: { name: 'setText' }, + setHtml: { name: 'setHtml' }, + setClass: { name: 'setClass' }, + setStyle: { name: 'setStyle' }, + setValue: { name: 'setValue' }, + setAttr: { name: 'setAttr', needKey: true }, + setProp: { name: 'setProp', needKey: true }, + setDOMProp: { name: 'setDOMProp', needKey: true }, + setDynamicProps: { name: 'setDynamicProps' }, +} as const satisfies Partial> + +// only the static key prop will reach here +export function genSetProp( + oper: SetPropIRNode, + context: CodegenContext, +): CodeFragment[] { + const { helper } = context + const { + prop: { key, values, modifier }, + tag, + } = oper + const resolvedHelper = getRuntimeHelper(tag, key.content, modifier) + const propValue = genPropValue(values, context) + return [ + NEWLINE, + ...genCall( + [helper(resolvedHelper.name), null], + `n${oper.element}`, + resolvedHelper.needKey ? genExpression(key, context) : false, + propValue, + ), + ] +} + +// dynamic key props and v-bind="{}" will reach here +export function genDynamicProps( + oper: SetDynamicPropsIRNode, + context: CodegenContext, +): CodeFragment[] { + const { helper } = context + const values = oper.props.map((props) => + Array.isArray(props) + ? genLiteralObjectProps(props, context) // static and dynamic arg props + : props.kind === IRDynamicPropsKind.ATTRIBUTE + ? genLiteralObjectProps([props], context) // dynamic arg props + : genExpression(props.value, context), + ) // v-bind="" + return [ + NEWLINE, + ...genCall( + helper('setDynamicProps'), + `n${oper.element}`, + genMulti(DELIMITERS_ARRAY, ...values), + oper.root && 'true', + ), + ] +} + +function genLiteralObjectProps( + props: IRProp[], + context: CodegenContext, +): CodeFragment[] { + return genMulti( + DELIMITERS_OBJECT, + ...props.map((prop) => [ + ...genPropKey(prop, context), + `: `, + ...genPropValue(prop.values, context), + ]), + ) +} + +export function genPropKey( + { key: node, modifier, runtimeCamelize, handler, handlerModifiers }: IRProp, + context: CodegenContext, +): CodeFragment[] { + const { helper } = context + + const handlerModifierPostfix = + handlerModifiers && handlerModifiers.options + ? handlerModifiers.options.map(capitalize).join('') + : '' + // static arg was transformed by v-bind transformer + if (node.isStatic) { + // only quote keys if necessary + const keyName = + (handler ? toHandlerKey(node.content) : node.content) + + handlerModifierPostfix + return [ + [ + isSimpleIdentifier(keyName) ? keyName : JSON.stringify(keyName), + NewlineType.None, + node.loc, + ], + ] + } + + let key = genExpression(node, context) + if (runtimeCamelize) { + key = genCall(helper('camelize'), key) + } + if (handler) { + key = genCall(helper('toHandlerKey'), key) + } + return [ + '[', + modifier && `${JSON.stringify(modifier)} + `, + ...key, + handlerModifierPostfix + ? ` + ${JSON.stringify(handlerModifierPostfix)}` + : undefined, + ']', + ] +} + +export function genPropValue( + values: SimpleExpressionNode[], + context: CodegenContext, +): CodeFragment[] { + if (values.length === 1) { + return genExpression(values[0], context) + } + return genMulti( + DELIMITERS_ARRAY, + ...values.map((expr) => genExpression(expr, context)), + ) +} + +function getRuntimeHelper( + tag: string, + key: string, + modifier: '.' | '^' | undefined, +): HelperConfig { + const tagName = tag.toUpperCase() + if (modifier) { + if (modifier === '.') { + return getSpecialHelper(key, tagName) || helpers.setDOMProp + } else { + return helpers.setAttr + } + } + + // 1. special handling for value / style / class / textContent / innerHTML + const helper = getSpecialHelper(key, tagName) + if (helper) { + return helper + } + + // 2. Aria DOM properties shared between all Elements in + // https://developer.mozilla.org/en-US/docs/Web/API/Element + if (/aria[A-Z]/.test(key)) { + return helpers.setDOMProp + } + + // 3. SVG: always attribute + if (isSVGTag(tag)) { + // TODO pass svg flag + return helpers.setAttr + } + + // 4. respect shouldSetAsAttr used in vdom and setDynamicProp for consistency + // also fast path for presence of hyphen (covers data-* and aria-*) + if (shouldSetAsAttr(tagName, key) || key.includes('-')) { + return helpers.setAttr + } + + // 5. Fallback to setDOMProp, which has a runtime `key in el` check to + // ensure behavior consistency with vdom + return helpers.setProp +} + +function getSpecialHelper( + keyName: string, + tagName: string, +): HelperConfig | undefined { + // special case for 'value' property + if (keyName === 'value' && canSetValueDirectly(tagName)) { + return helpers.setValue + } else if (keyName === 'class') { + return helpers.setClass + } else if (keyName === 'style') { + return helpers.setStyle + } else if (keyName === 'innerHTML') { + return helpers.setHtml + } else if (keyName === 'textContent') { + return helpers.setText + } +} diff --git a/packages/compiler/src/generators/template.ts b/packages/compiler/src/generators/template.ts new file mode 100644 index 0000000..5c36e7b --- /dev/null +++ b/packages/compiler/src/generators/template.ts @@ -0,0 +1,115 @@ +import { DynamicFlag, type IRDynamicInfo } from '../ir' +import type { CodegenContext } from '../generate' +import { genDirectivesForElement } from './directive' +import { genOperationWithInsertionState } from './operation' +import { buildCodeFragment, genCall, NEWLINE, type CodeFragment } from './utils' + +export function genTemplates( + templates: string[], + rootIndex: number | undefined, + { helper }: CodegenContext, +): string[] { + return templates.map((template, i) => + template.startsWith('_template') + ? template + : `${helper('template')}(${JSON.stringify( + template, + )}${i === rootIndex ? ', true' : ''})`, + ) +} + +export function genSelf( + dynamic: IRDynamicInfo, + context: CodegenContext, +): CodeFragment[] { + const [frag, push] = buildCodeFragment() + const { id, template, operation } = dynamic + + if (id !== undefined && template !== undefined) { + push(NEWLINE, `const n${id} = t${template}()`) + push(...genDirectivesForElement(id, context)) + } + + if (operation) { + push(...genOperationWithInsertionState(operation, context)) + } + + return frag +} + +export function genChildren( + dynamic: IRDynamicInfo, + context: CodegenContext, + pushBlock: (...items: CodeFragment[]) => number, + from: string = `n${dynamic.id}`, +): CodeFragment[] { + const { helper } = context + const [frag, push] = buildCodeFragment() + const { children } = dynamic + + let offset = 0 + let prev: [variable: string, elementIndex: number] | undefined + const childrenToGen: [IRDynamicInfo, string][] = [] + + for (const [index, child] of children.entries()) { + if (child.flags & DynamicFlag.NON_TEMPLATE) { + offset-- + } + + const id = + child.flags & DynamicFlag.REFERENCED + ? child.flags & DynamicFlag.INSERT + ? child.anchor + : child.id + : undefined + + if (id === undefined && !child.hasDynamicChild) { + push(...genSelf(child, context)) + continue + } + + const elementIndex = Number(index) + offset + // p for "placeholder" variables that are meant for possible reuse by + // other access paths + const variable = id === undefined ? `p${context.block.tempId++}` : `n${id}` + pushBlock(NEWLINE, `const ${variable} = `) + + if (prev) { + if (elementIndex - prev[1] === 1) { + pushBlock(...genCall(helper('next'), prev[0])) + } else { + pushBlock(...genCall(helper('nthChild'), from, String(elementIndex))) + } + } else if (elementIndex === 0) { + pushBlock(...genCall(helper('child'), from)) + } else { + // check if there's a node that we can reuse from + let init = genCall(helper('child'), from) + if (elementIndex === 1) { + init = genCall(helper('next'), init) + } else if (elementIndex > 1) { + init = genCall(helper('nthChild'), from, String(elementIndex)) + } + pushBlock(...init) + } + + if (id === child.anchor) { + push(...genSelf(child, context)) + } + + if (id !== undefined) { + push(...genDirectivesForElement(id, context)) + } + + prev = [variable, elementIndex] + childrenToGen.push([child, variable]) + } + + if (childrenToGen.length) { + for (const [child, from] of childrenToGen) { + push(...genChildren(child, context, pushBlock, from)) + } + } + + return frag +} diff --git a/packages/compiler/src/generators/templateRef.ts b/packages/compiler/src/generators/templateRef.ts new file mode 100644 index 0000000..f3f511c --- /dev/null +++ b/packages/compiler/src/generators/templateRef.ts @@ -0,0 +1,26 @@ +import type { CodegenContext } from '../generate' +import type { DeclareOldRefIRNode, SetTemplateRefIRNode } from '../ir' +import { genCall, genExpression, NEWLINE, type CodeFragment } from './utils' + +export const setTemplateRefIdent = `_setTemplateRef` + +export function genSetTemplateRef( + oper: SetTemplateRefIRNode, + context: CodegenContext, +): CodeFragment[] { + return [ + NEWLINE, + oper.effect && `r${oper.element} = `, + ...genCall( + setTemplateRefIdent, // will be generated in root scope + `n${oper.element}`, + genExpression(oper.value, context), + oper.effect ? `r${oper.element}` : oper.refFor ? 'void 0' : undefined, + oper.refFor && 'true', + ), + ] +} + +export function genDeclareOldRef(oper: DeclareOldRefIRNode): CodeFragment[] { + return [NEWLINE, `let r${oper.id}`] +} diff --git a/packages/compiler/src/generators/text.ts b/packages/compiler/src/generators/text.ts new file mode 100644 index 0000000..c7bdf80 --- /dev/null +++ b/packages/compiler/src/generators/text.ts @@ -0,0 +1,81 @@ +import { getLiteralExpressionValue } from '../utils' +import type { CodegenContext } from '../generate' +import type { + CreateNodesIRNode, + GetTextChildIRNode, + SetNodesIRNode, + SetTextIRNode, +} from '../ir' +import { genExpression } from './expression' +import { genCall, NEWLINE, type CodeFragment } from './utils' +import type { SimpleExpressionNode } from '@vue/compiler-dom' + +export function genSetText( + oper: SetTextIRNode, + context: CodegenContext, +): CodeFragment[] { + const { helper } = context + const { element, values, generated } = oper + const texts = combineValues(values, context, true) + return [ + NEWLINE, + ...genCall(helper('setText'), `${generated ? 'x' : 'n'}${element}`, texts), + ] +} + +export function genGetTextChild( + oper: GetTextChildIRNode, + context: CodegenContext, +): CodeFragment[] { + return [ + NEWLINE, + `const x${oper.parent} = ${context.helper('child')}(n${oper.parent})`, + ] +} + +export function genSetNodes( + oper: SetNodesIRNode, + context: CodegenContext, +): CodeFragment[] { + const { helper } = context + const { element, values, generated } = oper + return [ + NEWLINE, + ...genCall( + helper('setNodes'), + `${generated ? 'x' : 'n'}${element}`, + combineValues(values, context), + ), + ] +} + +export function genCreateNodes( + oper: CreateNodesIRNode, + context: CodegenContext, +): CodeFragment[] { + const { helper } = context + const { id, values } = oper + return [ + NEWLINE, + `const n${id} = `, + ...genCall(helper('createNodes'), values && combineValues(values, context)), + ] +} + +function combineValues( + values: SimpleExpressionNode[], + context: CodegenContext, + setText?: boolean, +): CodeFragment[] { + return values.flatMap((value, i) => { + let exp = genExpression(value, context) + if (setText && getLiteralExpressionValue(value) == null) { + // dynamic, wrap with toDisplayString + exp = genCall(context.helper('toDisplayString'), exp) + } + if (i > 0) { + exp.unshift(setText ? ' + ' : ', ') + } + return exp + }) +} diff --git a/packages/compiler/src/generators/utils.ts b/packages/compiler/src/generators/utils.ts new file mode 100644 index 0000000..030b9a8 --- /dev/null +++ b/packages/compiler/src/generators/utils.ts @@ -0,0 +1,180 @@ +import { + advancePositionWithMutation, + locStub, + NewlineType, + type CodegenSourceMapGenerator, + type Position, + type SourceLocation, +} from '@vue/compiler-dom' +import { isArray, isString } from '@vue/shared' +import { SourceMapGenerator } from 'source-map-js' +import type { CodegenContext } from '../generate' + +export { genExpression } from './expression' + +export const NEWLINE: unique symbol = Symbol(`newline`) +export const INDENT_START: unique symbol = Symbol(`indent start`) +export const INDENT_END: unique symbol = Symbol(`indent end`) + +type FalsyValue = false | null | undefined +export type CodeFragment = + | typeof NEWLINE + | typeof INDENT_START + | typeof INDENT_END + | string + | [code: string, newlineIndex?: number, loc?: SourceLocation, name?: string] + | FalsyValue +export type CodeFragments = Exclude | CodeFragment[] + +export function buildCodeFragment( + ...frag: CodeFragment[] +): [ + CodeFragment[], + (...items: CodeFragment[]) => number, + (...items: CodeFragment[]) => number, +] { + const push = frag.push.bind(frag) + const unshift = frag.unshift.bind(frag) + return [frag, push, unshift] +} + +export type CodeFragmentDelimiters = [ + left: CodeFragments, + right: CodeFragments, + delimiter: CodeFragments, + placeholder?: CodeFragments, +] + +export function genMulti( + [left, right, seg, placeholder]: CodeFragmentDelimiters, + ...frags: CodeFragments[] +): CodeFragment[] { + if (placeholder) { + while (frags.length > 0 && !frags.at(-1)) { + frags.pop() + } + frags = frags.map((frag) => frag || placeholder) + } else { + frags = frags.filter(Boolean) + } + + const frag: CodeFragment[] = [] + push(left) + for (const [i, fn] of ( + frags as Array> + ).entries()) { + push(fn) + if (i < frags.length - 1) push(seg) + } + push(right) + return frag + + function push(fn: CodeFragments) { + if (!isArray(fn)) fn = [fn] + frag.push(...fn) + } +} +export const DELIMITERS_ARRAY: CodeFragmentDelimiters = ['[', ']', ', '] +export const DELIMITERS_ARRAY_NEWLINE: CodeFragmentDelimiters = [ + ['[', INDENT_START, NEWLINE], + [INDENT_END, NEWLINE, ']'], + [', ', NEWLINE], +] +export const DELIMITERS_OBJECT: CodeFragmentDelimiters = ['{ ', ' }', ', '] +export const DELIMITERS_OBJECT_NEWLINE: CodeFragmentDelimiters = [ + ['{', INDENT_START, NEWLINE], + [INDENT_END, NEWLINE, '}'], + [', ', NEWLINE], +] + +export function genCall( + name: string | [name: string, placeholder?: CodeFragments], + ...frags: CodeFragments[] +): CodeFragment[] { + const hasPlaceholder = isArray(name) + const fnName = hasPlaceholder ? name[0] : name + const placeholder = hasPlaceholder ? name[1] : 'null' + return [fnName, ...genMulti(['(', ')', ', ', placeholder], ...frags)] +} + +export function codeFragmentToString( + code: CodeFragment[], + context: CodegenContext, +): [code: string, map: CodegenSourceMapGenerator | undefined] { + const { + options: { filename, sourceMap }, + } = context + + let map: CodegenSourceMapGenerator | undefined + if (sourceMap) { + // lazy require source-map implementation, only in non-browser builds + map = new SourceMapGenerator() as unknown as CodegenSourceMapGenerator + map.setSourceContent(filename, context.ir.source) + map._sources.add(filename) + } + + let codegen = '' + const pos = { line: 1, column: 1, offset: 0 } + let indentLevel = 0 + + for (let frag of code) { + if (!frag) continue + + if (frag === NEWLINE) { + frag = [`\n${` `.repeat(indentLevel)}`, NewlineType.Start] + } else if (frag === INDENT_START) { + indentLevel++ + continue + } else if (frag === INDENT_END) { + indentLevel-- + continue + } + + if (isString(frag)) frag = [frag] + + let [code, newlineIndex = NewlineType.None, loc, name] = frag + codegen += code + + if (map) { + if (loc) addMapping(loc.start, name) + if (newlineIndex === NewlineType.Unknown) { + // multiple newlines, full iteration + advancePositionWithMutation(pos, code) + } else { + // fast paths + pos.offset += code.length + if (newlineIndex === NewlineType.None) { + pos.column += code.length + } else { + // single newline at known index + if (newlineIndex === NewlineType.End) { + newlineIndex = code.length - 1 + } + pos.line++ + pos.column = code.length - newlineIndex + } + } + if (loc && loc !== locStub) { + addMapping(loc.end) + } + } + } + + return [codegen, map] + + 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 } = 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: pos.line, + generatedColumn: pos.column - 1, + source: filename, + name, + }) + } +} diff --git a/packages/compiler/src/generators/vModel.ts b/packages/compiler/src/generators/vModel.ts new file mode 100644 index 0000000..5bee594 --- /dev/null +++ b/packages/compiler/src/generators/vModel.ts @@ -0,0 +1,52 @@ +import type { CodegenContext } from '../generate' +import type { DirectiveIRNode } from '../ir' +import { genExpression } from './expression' +import { genCall, NEWLINE, type CodeFragment } from './utils' +import type { SimpleExpressionNode } from '@vue/compiler-dom' + +const helperMap = { + text: 'applyTextModel', + radio: 'applyRadioModel', + checkbox: 'applyCheckboxModel', + select: 'applySelectModel', + dynamic: 'applyDynamicModel', +} as const + +// This is only for built-in v-model on native elements. +export function genVModel( + oper: DirectiveIRNode, + context: CodegenContext, +): CodeFragment[] { + const { + modelType, + element, + dir: { exp, modifiers }, + } = oper + + return [ + NEWLINE, + ...genCall( + context.helper(helperMap[modelType!]), + `n${element}`, + // getter + [`() => (`, ...genExpression(exp!, context), `)`], + // setter + genModelHandler(exp!, context), + // modifiers + modifiers.length + ? `{ ${modifiers.map((e) => `${e.content}: true`).join(',')} }` + : undefined, + ), + ] +} + +export function genModelHandler( + exp: SimpleExpressionNode, + context: CodegenContext, +): CodeFragment[] { + return [ + `${context.options.isTS ? `(_value: any)` : `_value`} => (`, + ...genExpression(exp, context, '_value'), + ')', + ] +} diff --git a/packages/compiler/src/generators/vShow.ts b/packages/compiler/src/generators/vShow.ts new file mode 100644 index 0000000..db77f82 --- /dev/null +++ b/packages/compiler/src/generators/vShow.ts @@ -0,0 +1,18 @@ +import type { CodegenContext } from '../generate' +import type { DirectiveIRNode } from '../ir' +import { genExpression } from './expression' +import { genCall, NEWLINE, type CodeFragment } from './utils' + +export function genVShow( + oper: DirectiveIRNode, + context: CodegenContext, +): CodeFragment[] { + return [ + NEWLINE, + ...genCall(context.helper('applyVShow'), `n${oper.element}`, [ + `() => (`, + ...genExpression(oper.dir.exp!, context), + `)`, + ]), + ] +} diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 79e0c24..c872008 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -1,10 +1,11 @@ +export { compile, type CompilerOptions, type TransformPreset } from './compile' +export * from './transform' export { - compile, + CodegenContext, generate, - type CompilerOptions, - type TransformPreset, -} from './compile' -export * from './transform' + type CodegenOptions, + type VaporCodegenResult, +} from './generate' export * from './ir' diff --git a/packages/compiler/src/ir/component.ts b/packages/compiler/src/ir/component.ts index 70046ee..d968108 100644 --- a/packages/compiler/src/ir/component.ts +++ b/packages/compiler/src/ir/component.ts @@ -1,7 +1,6 @@ import type { DirectiveTransformResult } from '../transform' -import type { BlockIRNode } from './index' +import type { BlockIRNode, IRFor } from './index' import type { SimpleExpressionNode } from '@vue/compiler-dom' -import type { IRFor } from '@vue/compiler-vapor' // props export interface IRProp extends Omit { diff --git a/packages/compiler/src/ir/index.ts b/packages/compiler/src/ir/index.ts index b46f8d1..4fee654 100644 --- a/packages/compiler/src/ir/index.ts +++ b/packages/compiler/src/ir/index.ts @@ -64,7 +64,7 @@ export interface RootIRNode { type: IRNodeTypes.ROOT node: RootNode source: string - template: string[] + templates: string[] rootTemplateIndex?: number component: Set directive: Set diff --git a/packages/compiler/src/transform.ts b/packages/compiler/src/transform.ts index e794d8b..2909893 100644 --- a/packages/compiler/src/transform.ts +++ b/packages/compiler/src/transform.ts @@ -7,7 +7,7 @@ import { type CompilerCompatOptions, type SimpleExpressionNode, } from '@vue/compiler-dom' -import { EMPTY_OBJ, extend, isArray, isString, NOOP } from '@vue/shared' +import { extend, isArray, isString, NOOP } from '@vue/shared' import { DynamicFlag, IRNodeTypes, @@ -54,15 +54,17 @@ export type StructuralDirectiveTransform = ( context: TransformContext, ) => void | (() => void) -export type TransformOptions = HackOptions +export type TransformOptions = HackOptions & { + templates?: string[] +} const defaultOptions = { filename: '', - prefixIdentifiers: false, hoistStatic: false, hmr: false, cacheHandlers: false, nodeTransforms: [], directiveTransforms: {}, + templates: [], transformHoist: null, isBuiltInComponent: NOOP, isCustomElement: NOOP, @@ -72,8 +74,6 @@ const defaultOptions = { ssr: false, inSSR: false, ssrCssVars: ``, - bindingMetadata: EMPTY_OBJ, - inline: false, isTS: false, onError: defaultOnError, onWarn: defaultOnWarn, @@ -88,7 +88,14 @@ export class TransformContext< block: BlockIRNode options: Required< - Omit + Omit< + TransformOptions, + | 'filename' + | 'inline' + | 'bindingMetadata' + | 'prefixIdentifiers' + | keyof CompilerCompatOptions + > > template: string = '' @@ -147,10 +154,10 @@ export class TransformContext< } pushTemplate(content: string) { - const existing = this.ir.template.indexOf(content) + const existing = this.ir.templates.indexOf(content) if (existing !== -1) return existing - this.ir.template.push(content) - return this.ir.template.length - 1 + this.ir.templates.push(content) + return this.ir.templates.length - 1 } registerTemplate() { @@ -210,7 +217,7 @@ export function transform( type: IRNodeTypes.ROOT, node, source: node.source, - template: [], + templates: options.templates || [], component: new Set(), directive: new Set(), block: newBlock(node), diff --git a/packages/compiler/src/transforms/transformElement.ts b/packages/compiler/src/transforms/transformElement.ts index 3ea9e9b..227feda 100644 --- a/packages/compiler/src/transforms/transformElement.ts +++ b/packages/compiler/src/transforms/transformElement.ts @@ -181,7 +181,7 @@ function transformNativeElement( } if (singleRoot) { - context.ir.rootTemplateIndex = context.ir.template.length + context.ir.rootTemplateIndex = context.ir.templates.length } if ( diff --git a/packages/compiler/src/transforms/vIf.ts b/packages/compiler/src/transforms/vIf.ts index 5934b21..1b3bd51 100644 --- a/packages/compiler/src/transforms/vIf.ts +++ b/packages/compiler/src/transforms/vIf.ts @@ -49,9 +49,7 @@ export function processIf( id, condition: dir.exp!, positive: branch, - once: - context.inVOnce || - isConstantNode(attribute.value!, context.options.bindingMetadata), + once: context.inVOnce || isConstantNode(attribute.value!, {}), } } } else { diff --git a/packages/compiler/src/utils.ts b/packages/compiler/src/utils.ts index d2978ae..2b976a3 100644 --- a/packages/compiler/src/utils.ts +++ b/packages/compiler/src/utils.ts @@ -127,7 +127,6 @@ export function resolveSimpleExpressionNode( return exp } -const resolvedExpressions = new WeakSet() export function resolveExpression( node: Node | undefined | null, context: TransformContext, @@ -154,24 +153,17 @@ export function resolveExpression( ? node.name : context.ir.source.slice(node.start!, node.end!) const location = node.loc - const isResolved = resolvedExpressions.has(node) if (source && !isStatic && effect && !isConstant(node)) { source = `() => (${source})` - if (location && node && !isResolved) { - location.start.column -= 7 - node.start! -= 7 - } + // add offset flag to avoid re-parsing + ;(node as any)._offset = 7 } return resolveSimpleExpression( source, isStatic, location, - isStatic - ? undefined - : parseExpression(`(${source})`, { - plugins: context.options.expressionPlugins, - }), + isStatic ? undefined : node, ) } diff --git a/packages/compiler/test/compile.spec.ts b/packages/compiler/test/compile.spec.ts index ba7b9a8..2e0fd01 100644 --- a/packages/compiler/test/compile.spec.ts +++ b/packages/compiler/test/compile.spec.ts @@ -25,9 +25,7 @@ describe('compile', () => { describe('expression parsing', () => { test('interpolation', () => { - const { code } = compile(`<>{ a + b }`, { - inline: true, - }) + const { code } = compile(`<>{ a + b }`) expect(code).toMatchSnapshot() expect(code).contains('a + b') }) diff --git a/packages/compiler/test/transforms/__snapshots__/expression.spec.ts.snap b/packages/compiler/test/transforms/__snapshots__/expression.spec.ts.snap index 79389c6..a1778b8 100644 --- a/packages/compiler/test/transforms/__snapshots__/expression.spec.ts.snap +++ b/packages/compiler/test/transforms/__snapshots__/expression.spec.ts.snap @@ -1,18 +1,14 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`compiler: expression > conditional expression 1`] = ` -"import { child as _child, setNodes as _setNodes, createNodes as _createNodes, createIf as _createIf, template as _template } from 'vue'; -const t0 = _template(" ") -const t1 = _template("
fail
") - -export function render(_ctx) { - const n0 = _createIf(() => (_ctx.ok), () => { +" + const n0 = _createIf(() => (ok), () => { const n2 = t0() const x2 = _child(n2) - _setNodes(x2, () => (_ctx.msg)) + _setNodes(x2, () => (msg)) return n2 }, () => { - const n4 = _createIf(() => (_ctx.fail), () => { + const n4 = _createIf(() => (fail), () => { const n6 = t1() return n6 }, () => { @@ -22,22 +18,18 @@ export function render(_ctx) { return n4 }) return n0 -}" +" `; exports[`compiler: expression > conditional expression 2`] = ` -"import { child as _child, setNodes as _setNodes, createNodes as _createNodes, createIf as _createIf, template as _template } from 'vue'; -const t0 = _template(" ") -const t1 = _template("
fail
") - -export function render(_ctx) { - const n0 = _createIf(() => (_ctx.ok), () => { +" + const n0 = _createIf(() => (ok), () => { const n2 = t0() const x2 = _child(n2) - _setNodes(x2, () => (_ctx.msg)) + _setNodes(x2, () => (msg)) return n2 }, () => { - const n4 = _createIf(() => (_ctx.fail), () => { + const n4 = _createIf(() => (fail), () => { const n6 = t1() return n6 }, () => { @@ -47,7 +39,7 @@ export function render(_ctx) { return n4 }) return n0 -}" +" `; exports[`compiler: expression > conditional expression with v-once 1`] = ` diff --git a/packages/compiler/test/transforms/__snapshots__/vOn.spec.ts.snap b/packages/compiler/test/transforms/__snapshots__/vOn.spec.ts.snap index dc76af4..19c1c45 100644 --- a/packages/compiler/test/transforms/__snapshots__/vOn.spec.ts.snap +++ b/packages/compiler/test/transforms/__snapshots__/vOn.spec.ts.snap @@ -57,15 +57,11 @@ exports[`v-on > event modifier 1`] = ` `; exports[`v-on > should delegate event 1`] = ` -"import { delegateEvents as _delegateEvents, template as _template } from 'vue'; -const t0 = _template("
", true) -_delegateEvents("click") - -export function render(_ctx) { +" const n0 = t0() - n0.$evtclick = e => _ctx.test(e) + n0.$evtclick = e => test(e) return n0 -}" +" `; exports[`v-on > should not wrap keys guard if no key modifier is present 1`] = ` @@ -76,7 +72,7 @@ exports[`v-on > should not wrap keys guard if no key modifier is present 1`] = ` " `; -exports[`v-on > should support multiple modifiers and event options w/ prefixIdentifiers: true 1`] = ` +exports[`v-on > should support multiple modifiers and event options 1`] = ` " const n0 = t0() _on(n0, "click", _withModifiers(e => test(e), ["stop","prevent"]), { @@ -88,40 +84,28 @@ exports[`v-on > should support multiple modifiers and event options w/ prefixIde `; exports[`v-on > should transform click.middle 1`] = ` -"import { withModifiers as _withModifiers, delegateEvents as _delegateEvents, template as _template } from 'vue'; -const t0 = _template("
", true) -_delegateEvents("mouseup") - -export function render(_ctx) { +" const n0 = t0() - n0.$evtmouseup = _withModifiers(e => _ctx.test(e), ["middle"]) + n0.$evtmouseup = _withModifiers(e => test(e), ["middle"]) return n0 -}" +" `; exports[`v-on > should transform click.right 1`] = ` -"import { withModifiers as _withModifiers, delegateEvents as _delegateEvents, template as _template } from 'vue'; -const t0 = _template("
", true) -_delegateEvents("contextmenu") - -export function render(_ctx) { +" const n0 = t0() - n0.$evtcontextmenu = _withModifiers(e => _ctx.test(e), ["right"]) + n0.$evtcontextmenu = _withModifiers(e => test(e), ["right"]) return n0 -}" +" `; exports[`v-on > should use delegate helper when have multiple events of same name 1`] = ` -"import { delegate as _delegate, withModifiers as _withModifiers, delegateEvents as _delegateEvents, template as _template } from 'vue'; -const t0 = _template("
", true) -_delegateEvents("click") - -export function render(_ctx) { +" const n0 = t0() - _delegate(n0, "click", e => _ctx.test(e)) - _delegate(n0, "click", _withModifiers(e => _ctx.test(e), ["stop"])) + _delegate(n0, "click", e => test(e)) + _delegate(n0, "click", _withModifiers(e => test(e), ["stop"])) return n0 -}" +" `; exports[`v-on > should wrap keys guard for keyboard events or dynamic events 1`] = ` diff --git a/packages/compiler/test/transforms/__snapshots__/vShow.spec.ts.snap b/packages/compiler/test/transforms/__snapshots__/vShow.spec.ts.snap index 0fa9297..603d358 100644 --- a/packages/compiler/test/transforms/__snapshots__/vShow.spec.ts.snap +++ b/packages/compiler/test/transforms/__snapshots__/vShow.spec.ts.snap @@ -3,7 +3,7 @@ exports[`compiler: v-show transform > simple expression 1`] = ` " const n0 = t0() - _applyVShow(n0, () => ("foo")) + _applyVShow(n0, () => (foo)) return n0 " `; diff --git a/packages/compiler/test/transforms/_utils.ts b/packages/compiler/test/transforms/_utils.ts index 9441c91..10a564a 100644 --- a/packages/compiler/test/transforms/_utils.ts +++ b/packages/compiler/test/transforms/_utils.ts @@ -1,6 +1,5 @@ import { parse } from '@babel/parser' import { generate, transform, type CompilerOptions } from '../../src' -import { customGenOperation } from '../../src/generate' import { IRNodeTypes, type RootNode } from '../../src/ir' import type { JSXElement, JSXFragment } from '@babel/types' @@ -30,18 +29,15 @@ export function makeCompile(options: CompilerOptions = {}) { } const ir = transform(ast, { expressionPlugins: ['typescript', 'jsx'], - inline: true, - prefixIdentifiers: false, ...options, ...overrideOptions, }) as any - const { code, helpers, preamble } = generate(ir, { - inline: true, - prefixIdentifiers: false, - ...options, - ...overrideOptions, - customGenOperation, - }) - return { ast, ir, code, helpers, preamble } + return { + ir, + ...generate(ir, { + ...options, + ...overrideOptions, + }), + } } } diff --git a/packages/compiler/test/transforms/expression.spec.ts b/packages/compiler/test/transforms/expression.spec.ts index 7ab8c30..3fb3365 100644 --- a/packages/compiler/test/transforms/expression.spec.ts +++ b/packages/compiler/test/transforms/expression.spec.ts @@ -23,14 +23,13 @@ describe('compiler: expression', () => { test('conditional expression', () => { const { code, helpers, ir } = compileWithVIf( `<>{ok? {msg} : fail ?
fail
: null }`, - { inline: false }, ) expect(code).toMatchSnapshot() expect(helpers).contains('createIf') - expect(ir.template).toEqual([' ', '
fail
']) + expect(ir.templates).toEqual([' ', '
fail
']) const op = ir.block.dynamic.children[0].operation expect(op).toMatchObject({ type: IRNodeTypes.IF, @@ -64,7 +63,7 @@ describe('compiler: expression', () => { expect(helpers).contains('createIf') - expect(ir.template).toEqual(['
']) + expect(ir.templates).toEqual(['
']) const op = ir.block.dynamic.children[0].operation expect(op).toMatchObject({ type: IRNodeTypes.IF, @@ -97,7 +96,7 @@ describe('compiler: expression', () => { expect(code).toMatchSnapshot() expect(helpers).contains('createIf') - expect(ir.template).toEqual([ + expect(ir.templates).toEqual([ ' ', '
fail
', '
', diff --git a/packages/compiler/test/transforms/transformChildren.spec.ts b/packages/compiler/test/transforms/transformChildren.spec.ts index d1f4475..737ba5a 100644 --- a/packages/compiler/test/transforms/transformChildren.spec.ts +++ b/packages/compiler/test/transforms/transformChildren.spec.ts @@ -1,8 +1,9 @@ import { NodeTypes } from '@vue/compiler-dom' -import { IRDynamicPropsKind, IRNodeTypes } from '@vue/compiler-vapor' import { describe, expect, test } from 'vitest' import { + IRDynamicPropsKind, + IRNodeTypes, transformChildren, transformElement, transformText, @@ -36,32 +37,22 @@ describe('compiler: children transform', () => { test('comments', () => { const { code } = compileWithElementTransform( '<>{/*foo*/}
{/*bar*/}
', - { - inline: false, - }, ) expect(code).toMatchInlineSnapshot(` - "import { template as _template } from 'vue'; - const t0 = _template("
") - - export function render(_ctx) { + " const n1 = t0() return n1 - }" + " `) }) test('fragment', () => { - const { code } = compileWithElementTransform('<>{foo}', { - inline: false, - }) + const { code } = compileWithElementTransform('<>{foo}') expect(code).toMatchInlineSnapshot(` - "import { createNodes as _createNodes } from 'vue'; - - export function render(_ctx) { - const n0 = _createNodes(() => (_ctx.foo)) + " + const n0 = _createNodes(() => (foo)) return n0 - }" + " `) }) diff --git a/packages/compiler/test/transforms/transformElement.spec.ts b/packages/compiler/test/transforms/transformElement.spec.ts index 50b69d1..2fe3812 100644 --- a/packages/compiler/test/transforms/transformElement.spec.ts +++ b/packages/compiler/test/transforms/transformElement.spec.ts @@ -37,9 +37,7 @@ describe('compiler: element transform', () => { }) test('resolve namespaced component from setup bindings (inline const)', () => { - const { code, helpers } = compileWithElementTransform(``, { - inline: true, - }) + const { code, helpers } = compileWithElementTransform(``) expect(code).toMatchInlineSnapshot(` " const n0 = _createComponent(Foo.Example, null, null, true) diff --git a/packages/compiler/test/transforms/transformTemplateRef.spec.ts b/packages/compiler/test/transforms/transformTemplateRef.spec.ts index c3c4094..f916a84 100644 --- a/packages/compiler/test/transforms/transformTemplateRef.spec.ts +++ b/packages/compiler/test/transforms/transformTemplateRef.spec.ts @@ -31,7 +31,7 @@ describe('compiler: template ref transform', () => { id: 0, flags: DynamicFlag.REFERENCED, }) - expect(ir.template).toEqual(['
']) + expect(ir.templates).toEqual(['
']) expect(ir.block.operation).lengthOf(1) expect(ir.block.operation[0]).toMatchObject({ type: IRNodeTypes.SET_TEMPLATE_REF, @@ -57,7 +57,7 @@ describe('compiler: template ref transform', () => { id: 0, flags: DynamicFlag.REFERENCED, }) - expect(ir.template).toEqual(['
']) + expect(ir.templates).toEqual(['
']) expect(ir.block.operation).toMatchObject([ { type: IRNodeTypes.DECLARE_OLD_REF, diff --git a/packages/compiler/test/transforms/vBind.spec.ts b/packages/compiler/test/transforms/vBind.spec.ts index bc519a9..4db2820 100644 --- a/packages/compiler/test/transforms/vBind.spec.ts +++ b/packages/compiler/test/transforms/vBind.spec.ts @@ -31,7 +31,7 @@ describe('compiler v-bind', () => { id: 0, flags: DynamicFlag.REFERENCED, }) - expect(ir.template).toEqual(['
']) + expect(ir.templates).toEqual(['
']) expect(ir.block.effect).lengthOf(1) expect(ir.block.effect[0].expressions).lengthOf(1) expect(ir.block.effect[0].operations).lengthOf(1) @@ -248,7 +248,7 @@ describe('compiler v-bind', () => { end: { line: 1, column: 19 }, }, }) - expect(ir.template).toEqual(['
']) + expect(ir.templates).toEqual(['
']) expect(code).matchSnapshot() expect(code).contains(JSON.stringify('
')) diff --git a/packages/compiler/test/transforms/vFor.spec.ts b/packages/compiler/test/transforms/vFor.spec.ts index 0cafac3..6e1e833 100644 --- a/packages/compiler/test/transforms/vFor.spec.ts +++ b/packages/compiler/test/transforms/vFor.spec.ts @@ -34,7 +34,7 @@ describe('compiler: v-for', () => { expect(code).toMatchSnapshot() expect(helpers).contains('createFor') - expect(ir.template).toEqual(['
']) + expect(ir.templates).toEqual(['
']) const op = ir.block.dynamic.children[0].operation expect(op).toMatchObject({ type: IRNodeTypes.FOR, @@ -84,7 +84,7 @@ describe('compiler: v-for', () => { `_createFor(() => (_for_item0.value), (_for_item1) => {`, ) expect(code).contains(`_for_item1.value+_for_item0.value`) - expect(ir.template).toEqual([' ', '
']) + expect(ir.templates).toEqual([' ', '
']) const op = ir.block.dynamic.children[0].operation expect(op).toMatchObject({ type: IRNodeTypes.FOR, diff --git a/packages/compiler/test/transforms/vHtml.spec.ts b/packages/compiler/test/transforms/vHtml.spec.ts index 17178b3..8a0b87a 100644 --- a/packages/compiler/test/transforms/vHtml.spec.ts +++ b/packages/compiler/test/transforms/vHtml.spec.ts @@ -52,7 +52,7 @@ describe('v-html', () => { test('should raise error and ignore children when v-html is present', () => { const onError = vi.fn() - const { ir, helpers, preamble } = compileWithVHtml( + const { ir, helpers, templates } = compileWithVHtml( `
hello
`, { onError, @@ -62,7 +62,7 @@ describe('v-html', () => { expect(helpers).contains('setHtml') // children should have been removed - expect(ir.template).toEqual(['
']) + expect(ir.templates).toEqual(['
']) expect(ir.block.operation).toMatchObject([]) expect(ir.block.effect).toMatchObject([ @@ -93,7 +93,7 @@ describe('v-html', () => { ]) // children should have been removed - expect(preamble).contains('template("
", true)') + expect(templates).includes('_template("
", true)') }) test('should raise error if has no expression', () => { diff --git a/packages/compiler/test/transforms/vIf.spec.ts b/packages/compiler/test/transforms/vIf.spec.ts index 3a2dce4..c0bb605 100644 --- a/packages/compiler/test/transforms/vIf.spec.ts +++ b/packages/compiler/test/transforms/vIf.spec.ts @@ -30,7 +30,7 @@ describe('compiler: v-if', () => { expect(helpers).contains('createIf') expect(code).toMatchSnapshot() - expect(ir.template).toEqual(['
']) + expect(ir.templates).toEqual(['
']) const op = ir.block.dynamic.children[0].operation expect(op).toMatchObject({ type: IRNodeTypes.IF, @@ -61,7 +61,7 @@ describe('compiler: v-if', () => { ) expect(code).matchSnapshot() - expect(ir.template).toEqual(['
', 'hello', '

']) + expect(ir.templates).toEqual(['
', 'hello', '

']) const op = ir.block.dynamic.children[0].operation expect((op as IfIRNode).positive.effect).toMatchObject([ { @@ -95,7 +95,7 @@ describe('compiler: v-if', () => { `<>
hello
hello
`, ) expect(code).matchSnapshot() - expect(ir.template).toEqual(['
hello
']) + expect(ir.templates).toEqual(['
hello
']) expect(ir.block.returns).toEqual([0, 3]) }) @@ -106,7 +106,7 @@ describe('compiler: v-if', () => { `<>

`, ) expect(code).matchSnapshot() - expect(ir.template).toEqual(['

', '

']) + expect(ir.templates).toEqual(['
', '

']) expect(helpers).contains('createIf') expect(ir.block.effect).lengthOf(0) @@ -140,7 +140,7 @@ describe('compiler: v-if', () => { `<>

`, ) expect(code).matchSnapshot() - expect(ir.template).toEqual(['

', '

']) + expect(ir.templates).toEqual(['
', '

']) const op = ir.block.dynamic.children[0].operation expect(op).toMatchObject({ @@ -180,7 +180,7 @@ describe('compiler: v-if', () => { `<>

`, ) expect(code).matchSnapshot() - expect(ir.template).toEqual(['

', '

', 'fine']) + expect(ir.templates).toEqual(['
', '

', 'fine']) expect(ir.block.returns).toEqual([0]) const op = ir.block.dynamic.children[0].operation @@ -236,7 +236,7 @@ describe('compiler: v-if', () => { `, ) expect(code).toMatchSnapshot() - expect(ir.template).toEqual([ + expect(ir.templates).toEqual([ '
', '

', 'fine', diff --git a/packages/compiler/test/transforms/vOn.spec.ts b/packages/compiler/test/transforms/vOn.spec.ts index 3d4650e..c9b6547 100644 --- a/packages/compiler/test/transforms/vOn.spec.ts +++ b/packages/compiler/test/transforms/vOn.spec.ts @@ -108,7 +108,7 @@ describe('v-on', () => { expect(onError).not.toHaveBeenCalled() }) - test('should support multiple modifiers and event options w/ prefixIdentifiers: true', () => { + test('should support multiple modifiers and event options', () => { const { code, ir, helpers } = compileWithVOn( `
`, ) @@ -140,7 +140,7 @@ describe('v-on', () => { ) }) - test('should support multiple events and modifiers options w/ prefixIdentifiers: true', () => { + test('should support multiple events and modifiers options', () => { const { code, ir } = compileWithVOn( `
`, ) @@ -241,9 +241,7 @@ describe('v-on', () => { }) test('should wrap keys guard for static key event w/ left/right modifiers', () => { - const { code, ir } = compileWithVOn(`
`, { - prefixIdentifiers: true, - }) + const { code, ir } = compileWithVOn(`
`) expect(ir.block.operation).toMatchObject([ { @@ -260,9 +258,9 @@ describe('v-on', () => { }) test('should transform click.right', () => { - const { code, ir } = compileWithVOn(`
`, { - inline: false, - }) + const { code, ir, delegates } = compileWithVOn( + `
`, + ) expect(ir.block.operation).toMatchObject([ { type: IRNodeTypes.SET_EVENT, @@ -277,13 +275,13 @@ describe('v-on', () => { ]) expect(code).toMatchSnapshot() - expect(code).contains('"contextmenu"') + expect(delegates).includes('contextmenu') }) test('should transform click.middle', () => { - const { code, ir } = compileWithVOn(`
`, { - inline: false, - }) + const { code, ir, delegates } = compileWithVOn( + `
`, + ) expect(ir.block.operation).toMatchObject([ { type: IRNodeTypes.SET_EVENT, @@ -298,16 +296,16 @@ describe('v-on', () => { ]) expect(code).matchSnapshot() - expect(code).contains('"mouseup"') + expect(delegates).includes('mouseup') }) test('should delegate event', () => { - const { code, ir, helpers } = compileWithVOn(`
`, { - inline: false, - }) + const { code, ir, helpers, delegates } = compileWithVOn( + `
`, + ) expect(code).matchSnapshot() - expect(code).contains('_delegateEvents("click")') + expect(delegates).contains('click') expect(helpers).contains('delegateEvents') expect(ir.block.operation).toMatchObject([ { @@ -320,13 +318,12 @@ describe('v-on', () => { test('should use delegate helper when have multiple events of same name', () => { const { code, helpers } = compileWithVOn( `
`, - { inline: false }, ) expect(helpers).contains('delegate') expect(code).toMatchSnapshot() - expect(code).contains('_delegate(n0, "click", e => _ctx.test(e))') + expect(code).contains('_delegate(n0, "click", e => test(e))') expect(code).contains( - '_delegate(n0, "click", _withModifiers(e => _ctx.test(e), ["stop"]))', + '_delegate(n0, "click", _withModifiers(e => test(e), ["stop"]))', ) }) diff --git a/packages/compiler/test/transforms/vShow.spec.ts b/packages/compiler/test/transforms/vShow.spec.ts index d33d777..5be94aa 100644 --- a/packages/compiler/test/transforms/vShow.spec.ts +++ b/packages/compiler/test/transforms/vShow.spec.ts @@ -12,7 +12,7 @@ const compileWithVShow = makeCompile({ describe('compiler: v-show transform', () => { test('simple expression', () => { - const { code } = compileWithVShow(`
`) + const { code } = compileWithVShow(`
`) expect(code).toMatchSnapshot() }) diff --git a/packages/compiler/test/transforms/vSlot.spec.ts b/packages/compiler/test/transforms/vSlot.spec.ts index 7fcba29..5ff2cd9 100644 --- a/packages/compiler/test/transforms/vSlot.spec.ts +++ b/packages/compiler/test/transforms/vSlot.spec.ts @@ -34,7 +34,7 @@ describe('compiler: transform slot', () => { const { ir, code } = compileWithSlots(`
`) expect(code).toMatchSnapshot() - expect(ir.template).toEqual(['
']) + expect(ir.templates).toEqual(['
']) const op = ir.block.dynamic.children[0].operation expect(op).toMatchObject({ type: IRNodeTypes.CREATE_COMPONENT_NODE, @@ -181,7 +181,7 @@ describe('compiler: transform slot', () => { return n6 " `) - expect(ir.template).toEqual(['foo', 'bar', '']) + expect(ir.templates).toEqual(['foo', 'bar', '']) const op = ir.block.dynamic.children[0].operation expect(op).toMatchObject({ type: IRNodeTypes.CREATE_COMPONENT_NODE, diff --git a/packages/compiler/test/transforms/vText.spec.ts b/packages/compiler/test/transforms/vText.spec.ts index c17aac0..61a6241 100644 --- a/packages/compiler/test/transforms/vText.spec.ts +++ b/packages/compiler/test/transforms/vText.spec.ts @@ -59,7 +59,7 @@ describe('v-text', () => { test('should raise error and ignore children when v-text is present', () => { const onError = vi.fn() - const { code, ir, preamble } = compileWithVText( + const { code, ir, templates } = compileWithVText( `
hello
`, { onError, @@ -70,7 +70,7 @@ describe('v-text', () => { ]) // children should have been removed - expect(ir.template).toEqual(['
']) + expect(ir.templates).toEqual(['
']) expect(ir.block.effect).toMatchObject([ { @@ -99,7 +99,7 @@ describe('v-text', () => { expect(code).matchSnapshot() // children should have been removed - expect(preamble).contains('template("
", true)') + expect(templates).contains('_template("
", true)') }) test('should raise error if has no expression', () => { diff --git a/playground/package.json b/playground/package.json index 7c13a11..b23278d 100644 --- a/playground/package.json +++ b/playground/package.json @@ -10,6 +10,7 @@ "devDependencies": { "vite": "catalog:", "vite-hyper-config": "^0.7.0", + "vite-node": "^3.2.4", "vite-plugin-inspect": "^11.3.0", "vue": "catalog:", "vue-jsx-vapor": "workspace:*" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5911a2f..f16649d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,15 +57,15 @@ catalogs: '@vue/compiler-sfc': specifier: https://pkg.pr.new/@vue/compiler-sfc@51677cd version: 3.5.17 - '@vue/compiler-vapor': - specifier: https://pkg.pr.new/@vue/compiler-vapor@51677cd - version: 3.5.13 '@vue/shared': specifier: https://pkg.pr.new/@vue/shared@51677cd version: 3.5.17 hash-sum: specifier: ^2.0.0 version: 2.0.0 + source-map-js: + specifier: ^1.2.1 + version: 1.2.1 unplugin: specifier: ^2.3.5 version: 2.3.5 @@ -170,7 +170,7 @@ importers: specifier: workspace:* version: link:../compiler source-map-js: - specifier: ^1.2.1 + specifier: 'catalog:' version: 1.2.1 devDependencies: '@types/babel__core': @@ -197,12 +197,15 @@ importers: '@vue/compiler-dom': specifier: 'catalog:' version: https://pkg.pr.new/@vue/compiler-dom@51677cd - '@vue/compiler-vapor': - specifier: 'catalog:' - version: https://pkg.pr.new/@vue/compiler-vapor@51677cd '@vue/shared': specifier: 'catalog:' version: https://pkg.pr.new/@vue/shared@51677cd + ast-kit: + specifier: ^2.1.1 + version: 2.1.1 + source-map-js: + specifier: 'catalog:' + version: 1.2.1 packages/eslint: dependencies: @@ -347,6 +350,9 @@ importers: vite-hyper-config: specifier: ^0.7.0 version: 0.7.0(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.2)(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.2)(yaml@2.8.0))(yaml@2.8.0) + vite-node: + specifier: ^3.2.4 + version: 3.2.4(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.2)(yaml@2.8.0) vite-plugin-inspect: specifier: ^11.3.0 version: 11.3.0(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.2)(yaml@2.8.0)) @@ -2203,10 +2209,6 @@ packages: resolution: {tarball: https://pkg.pr.new/vuejs/core/@vue/compiler-ssr@51677cd} version: 3.5.17 - '@vue/compiler-vapor@https://pkg.pr.new/@vue/compiler-vapor@51677cd': - resolution: {tarball: https://pkg.pr.new/@vue/compiler-vapor@51677cd} - version: 3.5.13 - '@vue/compiler-vapor@https://pkg.pr.new/vuejs/core/@vue/compiler-vapor@51677cd': resolution: {tarball: https://pkg.pr.new/vuejs/core/@vue/compiler-vapor@51677cd} version: 3.5.13 @@ -2481,6 +2483,10 @@ packages: resolution: {integrity: sha512-ROM2LlXbZBZVk97crfw8PGDOBzzsJvN2uJCmwswvPUNyfH14eg90mSN3xNqsri1JS1G9cz0VzeDUhxJkTrr4Ew==} engines: {node: '>=20.18.0'} + ast-kit@2.1.1: + resolution: {integrity: sha512-mfh6a7gKXE8pDlxTvqIc/syH/P3RkzbOF6LeHdcKztLEzYe6IMsRCL7N8vI7hqTGWNxpkCuuRTpT21xNWqhRtQ==} + engines: {node: '>=20.18.0'} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -4574,11 +4580,6 @@ packages: peerDependencies: vite: ^4.0.0 || ^5.0.0 || ^6.0.0 - vite-node@3.1.4: - resolution: {integrity: sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -4983,7 +4984,7 @@ snapshots: '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.7) '@babel/helpers': 7.27.6 - '@babel/parser': 7.27.7 + '@babel/parser': 7.28.0 '@babel/template': 7.27.2 '@babel/traverse': 7.27.7 '@babel/types': 7.28.0 @@ -5017,7 +5018,7 @@ snapshots: '@babel/generator@7.27.5': dependencies: - '@babel/parser': 7.27.7 + '@babel/parser': 7.28.0 '@babel/types': 7.27.7 '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 @@ -6720,7 +6721,7 @@ snapshots: '@vue/compiler-core@3.5.17': dependencies: - '@babel/parser': 7.27.7 + '@babel/parser': 7.28.0 '@vue/shared': 3.5.17 entities: 4.5.0 estree-walker: 2.0.2 @@ -6797,12 +6798,6 @@ snapshots: '@vue/compiler-dom': https://pkg.pr.new/vuejs/core/@vue/compiler-dom@51677cd '@vue/shared': https://pkg.pr.new/vuejs/core/@vue/shared@51677cd - '@vue/compiler-vapor@https://pkg.pr.new/@vue/compiler-vapor@51677cd': - dependencies: - '@vue/compiler-dom': https://pkg.pr.new/vuejs/core/@vue/compiler-dom@51677cd - '@vue/shared': https://pkg.pr.new/vuejs/core/@vue/shared@51677cd - source-map-js: 1.2.1 - '@vue/compiler-vapor@https://pkg.pr.new/vuejs/core/@vue/compiler-vapor@51677cd': dependencies: '@vue/compiler-dom': https://pkg.pr.new/vuejs/core/@vue/compiler-dom@51677cd @@ -7100,6 +7095,11 @@ snapshots: '@babel/parser': 7.27.7 pathe: 2.0.3 + ast-kit@2.1.1: + dependencies: + '@babel/parser': 7.28.0 + pathe: 2.0.3 + asynckit@0.4.0: optional: true @@ -9619,28 +9619,7 @@ snapshots: cac: 6.7.14 picocolors: 1.1.1 vite: 7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.2)(yaml@2.8.0) - vite-node: 3.1.4(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.2)(yaml@2.8.0) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - vite-node@3.1.4(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.2)(yaml@2.8.0): - dependencies: - cac: 6.7.14 - debug: 4.4.1 - es-module-lexer: 1.7.0 - pathe: 2.0.3 - vite: 6.3.5(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.2)(yaml@2.8.0) + vite-node: 3.2.4(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.2)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b2874c3..a4eb3de 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -22,7 +22,6 @@ catalog: '@vue/compiler-dom': https://pkg.pr.new/@vue/compiler-dom@51677cd '@vue/compiler-sfc': https://pkg.pr.new/@vue/compiler-sfc@51677cd - '@vue/compiler-vapor': https://pkg.pr.new/@vue/compiler-vapor@51677cd '@vue/shared': https://pkg.pr.new/@vue/shared@51677cd '@vue-macros/common': *vue-macros @@ -33,6 +32,7 @@ catalog: '@ts-macro/tsc': *ts-macro '@types/hash-sum': ^1.0.2 hash-sum: ^2.0.0 + source-map-js: ^1.2.1 ts-macro: *ts-macro unplugin: ^2.3.5 unplugin-utils: ^0.2.4