diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts index a1e084eb9..93b990dbb 100644 --- a/packages/compiler-core/src/utils.ts +++ b/packages/compiler-core/src/utils.ts @@ -152,11 +152,17 @@ export const isMemberExpressionBrowser = (path: string): boolean => { } export const isMemberExpressionNode = __BROWSER__ - ? (NOOP as any as (path: string, context: TransformContext) => boolean) - : (path: string, context: TransformContext): boolean => { + ? (NOOP as any as ( + path: string, + options: Pick + ) => boolean) + : ( + path: string, + options: Pick + ): boolean => { try { let ret: Expression = parseExpression(path, { - plugins: context.expressionPlugins + plugins: options.expressionPlugins }) if (ret.type === 'TSAsExpression' || ret.type === 'TSTypeAssertion') { ret = ret.expression diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap index 335280313..910d019cd 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`compiler: codegen v-bind > .camel modifier 1`] = ` +exports[`compiler v-bind > .camel modifier 1`] = ` "import { template as _template, children as _children, effect as _effect, setAttr as _setAttr } from 'vue/vapor'; export function render(_ctx) { @@ -8,27 +8,27 @@ export function render(_ctx) { const n0 = t0() const { 0: [n1],} = _children(n0) _effect(() => { - _setAttr(n1, "foo-bar", undefined, _ctx.id) + _setAttr(n1, "fooBar", undefined, _ctx.id) }) return n0 }" `; -exports[`compiler: codegen v-bind > dynamic arg 1`] = ` -"import { template as _template, children as _children, effect as _effect, setAttr as _setAttr } from 'vue/vapor'; +exports[`compiler v-bind > .camel modifier w/ dynamic arg 1`] = ` +"import { camelize as _camelize } from 'vue'; export function render(_ctx) { const t0 = _template("
") const n0 = t0() const { 0: [n1],} = _children(n0) _effect(() => { - _setAttr(n1, _ctx.id, undefined, _ctx.id) + _setAttr(n1, _camelize(_ctx.foo), undefined, _ctx.id) }) return n0 }" `; -exports[`compiler: codegen v-bind > no expression (shorthand) 1`] = ` +exports[`compiler v-bind > .camel modifier w/ no expression 1`] = ` "import { template as _template, children as _children, effect as _effect, setAttr as _setAttr } from 'vue/vapor'; export function render(_ctx) { @@ -36,13 +36,13 @@ export function render(_ctx) { const n0 = t0() const { 0: [n1],} = _children(n0) _effect(() => { - _setAttr(n1, "camel-case", undefined, _ctx.camelCase) + _setAttr(n1, "fooBar", undefined, _ctx.fooBar) }) return n0 }" `; -exports[`compiler: codegen v-bind > no expression 1`] = ` +exports[`compiler v-bind > basic 1`] = ` "import { template as _template, children as _children, effect as _effect, setAttr as _setAttr } from 'vue/vapor'; export function render(_ctx) { @@ -56,17 +56,35 @@ export function render(_ctx) { }" `; -exports[`compiler: codegen v-bind > should error if no expression 1`] = ` -"import { template as _template } from 'vue/vapor'; +exports[`compiler v-bind > dynamic arg 1`] = ` +"import { template as _template, children as _children, effect as _effect, setAttr as _setAttr } from 'vue/vapor'; export function render(_ctx) { - const t0 = _template("
") + const t0 = _template("
") const n0 = t0() + const { 0: [n1],} = _children(n0) + _effect(() => { + _setAttr(n1, _ctx.id, undefined, _ctx.id) + }) return n0 }" `; -exports[`compiler: codegen v-bind > simple expression 1`] = ` +exports[`compiler v-bind > no expression (shorthand) 1`] = ` +"import { template as _template, children as _children, effect as _effect, setAttr as _setAttr } from 'vue/vapor'; + +export function render(_ctx) { + const t0 = _template("
") + const n0 = t0() + const { 0: [n1],} = _children(n0) + _effect(() => { + _setAttr(n1, "camel-case", undefined, _ctx.camelCase) + }) + return n0 +}" +`; + +exports[`compiler v-bind > no expression 1`] = ` "import { template as _template, children as _children, effect as _effect, setAttr as _setAttr } from 'vue/vapor'; export function render(_ctx) { @@ -79,3 +97,13 @@ export function render(_ctx) { return n0 }" `; + +exports[`compiler v-bind > should error if empty expression 1`] = ` +"import { template as _template } from 'vue/vapor'; + +export function render(_ctx) { + const t0 = _template("
") + const n0 = t0() + return n0 +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts index fbaf2b089..c81f2e51a 100644 --- a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts @@ -1,9 +1,4 @@ -import { - type RootNode, - ErrorCodes, - NodeTypes, - BindingTypes, -} from '@vue/compiler-dom' +import { ErrorCodes, NodeTypes } from '@vue/compiler-dom' import { type RootIRNode, type CompilerOptions, @@ -13,48 +8,45 @@ import { transformElement, IRNodeTypes, compile as _compile, + generate, } from '../../src' -function parseWithVBind( +function compileWithVBind( template: string, options: CompilerOptions = {}, -): RootIRNode { - const ast = parse(template) +): { + ir: RootIRNode + code: string +} { + const ast = parse(template, { prefixIdentifiers: true, ...options }) const ir = transform(ast, { nodeTransforms: [transformElement], directiveTransforms: { bind: transformVBind, }, - ...options, - }) - return ir -} - -function compile(template: string | RootNode, options: CompilerOptions = {}) { - let { code } = _compile(template, { - ...options, - mode: 'module', prefixIdentifiers: true, + ...options, }) - return code + const { code } = generate(ir, { prefixIdentifiers: true, ...options }) + return { ir, code } } -describe('compiler: transform v-bind', () => { +describe('compiler v-bind', () => { test('basic', () => { - const node = parseWithVBind(`
`) + const { ir, code } = compileWithVBind(`
`) - expect(node.dynamic.children[0]).toMatchObject({ + expect(ir.dynamic.children[0]).toMatchObject({ id: 1, referenced: true, }) - expect(node.template[0]).toMatchObject({ + expect(ir.template[0]).toMatchObject({ type: IRNodeTypes.TEMPLATE_FACTORY, template: '
', }) - expect(node.effect).lengthOf(1) - expect(node.effect[0].expressions).lengthOf(1) - expect(node.effect[0].operations).lengthOf(1) - expect(node.effect[0]).toMatchObject({ + expect(ir.effect).lengthOf(1) + expect(ir.effect[0].expressions).lengthOf(1) + expect(ir.effect[0].operations).lengthOf(1) + expect(ir.effect[0]).toMatchObject({ expressions: [ { type: NodeTypes.SIMPLE_EXPRESSION, @@ -89,12 +81,15 @@ describe('compiler: transform v-bind', () => { }, ], }) + + expect(code).matchSnapshot() + expect(code).contains('_setAttr(n1, "id", undefined, _ctx.id)') }) test('no expression', () => { - const node = parseWithVBind(`
`) + const { ir, code } = compileWithVBind(`
`) - expect(node.effect[0].operations[0]).toMatchObject({ + expect(ir.effect[0].operations[0]).toMatchObject({ type: IRNodeTypes.SET_PROP, key: { content: `id`, @@ -113,27 +108,35 @@ describe('compiler: transform v-bind', () => { }, }, }) + + expect(code).matchSnapshot() + expect(code).contains('_setAttr(n1, "id", undefined, _ctx.id)') }) test('no expression (shorthand)', () => { - const node = parseWithVBind(`
`) + const { ir, code } = compileWithVBind(`
`) - expect(node.effect[0].operations[0]).toMatchObject({ + expect(ir.effect[0].operations[0]).toMatchObject({ type: IRNodeTypes.SET_PROP, key: { - content: `id`, + content: `camel-case`, isStatic: true, }, value: { - content: `id`, + content: `camelCase`, isStatic: false, }, }) + + expect(code).matchSnapshot() + expect(code).contains( + '_setAttr(n1, "camel-case", undefined, _ctx.camelCase)', + ) }) test('dynamic arg', () => { - const node = parseWithVBind(`
`) - expect(node.effect[0].operations[0]).toMatchObject({ + const { ir, code } = compileWithVBind(`
`) + expect(ir.effect[0].operations[0]).toMatchObject({ type: IRNodeTypes.SET_PROP, element: 1, key: { @@ -147,11 +150,17 @@ describe('compiler: transform v-bind', () => { isStatic: false, }, }) + + expect(code).matchSnapshot() + expect(code).contains('_setAttr(n1, _ctx.id, undefined, _ctx.id)') }) test('should error if empty expression', () => { const onError = vi.fn() - const node = parseWithVBind(`
`, { onError }) + const { ir, code } = compileWithVBind(`
`, { + onError, + }) + expect(onError.mock.calls[0][0]).toMatchObject({ code: ErrorCodes.X_V_BIND_NO_EXPRESSION, loc: { @@ -159,15 +168,19 @@ describe('compiler: transform v-bind', () => { end: { line: 1, column: 19 }, }, }) - expect(node.template[0]).toMatchObject({ + expect(ir.template[0]).toMatchObject({ type: IRNodeTypes.TEMPLATE_FACTORY, template: '
', }) + + expect(code).matchSnapshot() + expect(code).contains(JSON.stringify('
')) }) - test.fails('.camel modifier', () => { - const node = parseWithVBind(`
`) - expect(node.effect[0].operations[0]).toMatchObject({ + test('.camel modifier', () => { + const { ir, code } = compileWithVBind(`
`) + + expect(ir.effect[0].operations[0]).toMatchObject({ key: { content: `fooBar`, isStatic: true, @@ -177,11 +190,15 @@ describe('compiler: transform v-bind', () => { isStatic: false, }, }) + + expect(code).matchSnapshot() + expect(code).contains('_setAttr(n1, "fooBar", undefined, _ctx.id)') }) - test.fails('.camel modifier w/ no expression', () => { - const node = parseWithVBind(`
`) - expect(node.effect[0].operations[0]).toMatchObject({ + test('.camel modifier w/ no expression', () => { + const { ir, code } = compileWithVBind(`
`) + + expect(ir.effect[0].operations[0]).toMatchObject({ key: { content: `fooBar`, isStatic: true, @@ -191,21 +208,32 @@ describe('compiler: transform v-bind', () => { isStatic: false, }, }) + + expect(code).matchSnapshot() + expect(code).contains('effect') + expect(code).contains('_setAttr(n1, "fooBar", undefined, _ctx.fooBar)') }) - test.fails('.camel modifier w/ dynamic arg', () => { - const node = parseWithVBind(`
`) - expect(node.effect[0].operations[0]).toMatchObject({ + test('.camel modifier w/ dynamic arg', () => { + const { ir, code } = compileWithVBind(`
`) + + expect(ir.effect[0].operations[0]).toMatchObject({ + runtimeCamelize: true, key: { content: `foo`, isStatic: false, - somethingShouldBeTrue: true, }, value: { content: `id`, isStatic: false, }, }) + + expect(code).matchSnapshot() + expect(code).contains('effect') + expect(code).contains( + `_setAttr(n1, _camelize(_ctx.foo), undefined, _ctx.id)`, + ) }) test.todo('.camel modifier w/ dynamic arg + prefixIdentifiers') @@ -219,81 +247,3 @@ describe('compiler: transform v-bind', () => { test.todo('.attr modifier') test.todo('.attr modifier w/ no expression') }) - -// TODO: combine with above -describe('compiler: codegen v-bind', () => { - test('simple expression', () => { - const code = compile(`
`, { - bindingMetadata: { - id: BindingTypes.SETUP_REF, - }, - }) - expect(code).matchSnapshot() - }) - - test('should error if no expression', () => { - const onError = vi.fn() - const code = compile(`
`, { onError }) - - expect(onError.mock.calls[0][0]).toMatchObject({ - code: ErrorCodes.X_V_BIND_NO_EXPRESSION, - loc: { - start: { - line: 1, - column: 6, - }, - end: { - line: 1, - column: 19, - }, - }, - }) - - expect(code).matchSnapshot() - // the arg is static - expect(code).contains(JSON.stringify('
')) - }) - - test('no expression', () => { - const code = compile('
', { - bindingMetadata: { - id: BindingTypes.SETUP_REF, - }, - }) - - expect(code).matchSnapshot() - expect(code).contains('_setAttr(n1, "id", undefined, _ctx.id)') - }) - - test('no expression (shorthand)', () => { - const code = compile('
', { - bindingMetadata: { - camelCase: BindingTypes.SETUP_REF, - }, - }) - - expect(code).matchSnapshot() - expect(code).contains( - '_setAttr(n1, "camel-case", undefined, _ctx.camelCase)', - ) - }) - - test('dynamic arg', () => { - const code = compile('
', { - bindingMetadata: { - id: BindingTypes.SETUP_REF, - }, - }) - - expect(code).matchSnapshot() - expect(code).contains('_setAttr(n1, _ctx.id, undefined, _ctx.id)') - }) - - // TODO: camel modifier for v-bind - test.fails('.camel modifier', () => { - const code = compile(`
`) - - expect(code).matchSnapshot() - expect(code).contains('fooBar') - }) -}) diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index d92def7fe..b79ac2901 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -30,7 +30,7 @@ import { IRNodeTypes, } from './ir' import { SourceMapGenerator } from 'source-map-js' -import { camelize, isString, makeMap } from '@vue/shared' +import { camelize, isGloballyAllowed, isString, makeMap } from '@vue/shared' import type { Identifier } from '@babel/types' // remove when stable @@ -52,14 +52,18 @@ export interface CodegenContext extends Required { loc?: SourceLocation, name?: string, ): void - pushWithNewline( + pushNewline( code: string, newlineIndex?: number, loc?: SourceLocation, name?: string, ): void - indent(): void - deindent(): void + pushMulti( + codes: [left: string, right: string, segment?: string], + ...fn: Array void)> + ): void + pushFnCall(name: string, ...args: Array void)>): void + withIndent(fn: () => void): void newline(): void helpers: Set @@ -166,25 +170,36 @@ function createCodegenContext( } } }, - pushWithNewline(code, newlineIndex, node) { + pushNewline(code, newlineIndex, node) { context.newline() context.push(code, newlineIndex, node) }, - indent() { - ++context.indentLevel + pushMulti([left, right, seg], ...fns) { + fns = fns.filter(Boolean) + context.push(left) + for (let i = 0; i < fns.length; i++) { + const fn = fns[i] as string | (() => void) + + if (isString(fn)) context.push(fn) + else fn() + if (seg && i < fns.length - 1) context.push(seg) + } + context.push(right) }, - deindent() { + pushFnCall(name, ...args) { + context.push(name) + context.pushMulti(['(', ')', ', '], ...args) + }, + withIndent(fn) { + ++context.indentLevel + fn() --context.indentLevel }, newline() { - newline(context.indentLevel) + context.push(`\n${` `.repeat(context.indentLevel)}`, NewlineType.Start) }, } - function newline(n: number) { - context.push(`\n${` `.repeat(n)}`, NewlineType.Start) - } - function addMapping(loc: Position, name: string | null = null) { // we use the private property to directly add the mapping // because the addMapping() implementation in source-map-js has a bunch of @@ -218,8 +233,15 @@ export function generate( options: CodegenOptions = {}, ): CodegenResult { const ctx = createCodegenContext(ir, options) - const { push, pushWithNewline, indent, deindent, newline } = ctx - const { vaporHelper, helpers, vaporHelpers } = ctx + const { + push, + pushNewline, + withIndent, + newline, + helpers, + vaporHelper, + vaporHelpers, + } = ctx const functionName = 'render' const isSetupInlined = !!options.inline @@ -228,64 +250,64 @@ export function generate( } else { // placeholder for preamble newline() - pushWithNewline(`export function ${functionName}(_ctx) {`) + pushNewline(`export function ${functionName}(_ctx) {`) } - indent() - - ir.template.forEach((template, i) => { - if (template.type === IRNodeTypes.TEMPLATE_FACTORY) { - // TODO source map? - pushWithNewline( - `const t${i} = ${vaporHelper('template')}(${JSON.stringify( - template.template, - )})`, - ) - } else { - // fragment - pushWithNewline( - `const t0 = ${vaporHelper('fragment')}()\n`, - NewlineType.End, - ) - } - }) - { - pushWithNewline(`const n${ir.dynamic.id} = t0()`) + withIndent(() => { + ir.template.forEach((template, i) => { + if (template.type === IRNodeTypes.TEMPLATE_FACTORY) { + // TODO source map? + pushNewline( + `const t${i} = ${vaporHelper('template')}(${JSON.stringify( + template.template, + )})`, + ) + } else { + // fragment + pushNewline( + `const t0 = ${vaporHelper('fragment')}()\n`, + NewlineType.End, + ) + } + }) - const children = genChildren(ir.dynamic.children) - if (children) { - pushWithNewline( - `const ${children} = ${vaporHelper('children')}(n${ir.dynamic.id})`, - ) - } + { + pushNewline(`const n${ir.dynamic.id} = t0()`) - for (const oper of ir.operation.filter( - (oper): oper is WithDirectiveIRNode => - oper.type === IRNodeTypes.WITH_DIRECTIVE, - )) { - genWithDirective(oper, ctx) - } + const children = genChildren(ir.dynamic.children) + if (children) { + pushNewline( + `const ${children} = ${vaporHelper('children')}(n${ir.dynamic.id})`, + ) + } - for (const operation of ir.operation) { - genOperation(operation, ctx) - } + for (const oper of ir.operation.filter( + (oper): oper is WithDirectiveIRNode => + oper.type === IRNodeTypes.WITH_DIRECTIVE, + )) { + genWithDirective(oper, ctx) + } - for (const { operations } of ir.effect) { - pushWithNewline(`${vaporHelper('effect')}(() => {`) - indent() - for (const operation of operations) { + for (const operation of ir.operation) { genOperation(operation, ctx) } - deindent() - pushWithNewline('})') - } - // TODO multiple-template - // TODO return statement in IR - pushWithNewline(`return n${ir.dynamic.id}`) - } + for (const { operations } of ir.effect) { + pushNewline(`${vaporHelper('effect')}(() => {`) + withIndent(() => { + for (const operation of operations) { + genOperation(operation, ctx) + } + }) + pushNewline('})') + } + + // TODO multiple-template + // TODO return statement in IR + pushNewline(`return n${ir.dynamic.id}`) + } + }) - deindent() newline() if (isSetupInlined) { push('})()') @@ -318,7 +340,6 @@ export function generate( function genChildren(children: IRDynamicChildren) { let code = '' - // TODO let offset = 0 for (const [index, child] of Object.entries(children)) { const childrenLength = Object.keys(child.children).length @@ -370,145 +391,194 @@ function genOperation(oper: OperationNode, context: CodegenContext) { } function genSetProp(oper: SetPropIRNode, context: CodegenContext) { - const { push, pushWithNewline, vaporHelper } = context - pushWithNewline(`${vaporHelper('setAttr')}(n${oper.element}, `) - genExpression(oper.key, context) - push(`, undefined, `) - genExpression(oper.value, context) - push(')') + const { pushFnCall, newline, vaporHelper, helper } = context + + newline() + pushFnCall( + vaporHelper('setAttr'), + `n${oper.element}`, + // 2. key name + () => { + if (oper.runtimeCamelize) { + pushFnCall(helper('camelize'), () => genExpression(oper.key, context)) + } else { + genExpression(oper.key, context) + } + }, + 'undefined', + () => genExpression(oper.value, context), + ) } function genSetText(oper: SetTextIRNode, context: CodegenContext) { - const { push, pushWithNewline, vaporHelper } = context - pushWithNewline(`${vaporHelper('setText')}(n${oper.element}, undefined, `) - genExpression(oper.value, context) - push(')') + const { pushFnCall, newline, vaporHelper } = context + newline() + pushFnCall(vaporHelper('setText'), `n${oper.element}`, 'undefined', () => + genExpression(oper.value, context), + ) } function genSetHtml(oper: SetHtmlIRNode, context: CodegenContext) { - const { push, pushWithNewline, vaporHelper } = context - pushWithNewline(`${vaporHelper('setHtml')}(n${oper.element}, undefined, `) - genExpression(oper.value, context) - push(')') + const { newline, pushFnCall, vaporHelper } = context + newline() + pushFnCall(vaporHelper('setHtml'), `n${oper.element}`, 'undefined', () => + genExpression(oper.value, context), + ) } function genCreateTextNode( oper: CreateTextNodeIRNode, context: CodegenContext, ) { - const { push, pushWithNewline, vaporHelper } = context - pushWithNewline(`const n${oper.id} = ${vaporHelper('createTextNode')}(`) - genExpression(oper.value, context) - push(')') + const { pushNewline, pushFnCall, vaporHelper } = context + pushNewline(`const n${oper.id} = `) + pushFnCall(vaporHelper('createTextNode'), () => + genExpression(oper.value, context), + ) } function genInsertNode(oper: InsertNodeIRNode, context: CodegenContext) { - const { pushWithNewline, vaporHelper } = context + const { newline, pushFnCall, vaporHelper } = context const elements = ([] as number[]).concat(oper.element) let element = elements.map((el) => `n${el}`).join(', ') if (elements.length > 1) element = `[${element}]` - pushWithNewline( - `${vaporHelper('insert')}(${element}, n${ - oper.parent - }${`, n${oper.anchor}`})`, + newline() + pushFnCall( + vaporHelper('insert'), + element, + `n${oper.parent}`, + `n${oper.anchor}`, ) } function genPrependNode(oper: PrependNodeIRNode, context: CodegenContext) { - const { pushWithNewline, vaporHelper } = context - pushWithNewline( - `${vaporHelper('prepend')}(n${oper.parent}, ${oper.elements - .map((el) => `n${el}`) - .join(', ')})`, + const { newline, pushFnCall, vaporHelper } = context + newline() + pushFnCall( + vaporHelper('prepend'), + `n${oper.parent}`, + oper.elements.map((el) => `n${el}`).join(', '), ) } function genAppendNode(oper: AppendNodeIRNode, context: CodegenContext) { - const { pushWithNewline, vaporHelper } = context - pushWithNewline( - `${vaporHelper('append')}(n${oper.parent}, ${oper.elements - .map((el) => `n${el}`) - .join(', ')})`, + const { newline, pushFnCall, vaporHelper } = context + newline() + pushFnCall( + vaporHelper('append'), + `n${oper.parent}`, + oper.elements.map((el) => `n${el}`).join(', '), ) } function genSetEvent(oper: SetEventIRNode, context: CodegenContext) { - const { vaporHelper, push, pushWithNewline } = context - - pushWithNewline(`${vaporHelper('on')}(n${oper.element}, `) - // second arg: event name - genExpression(oper.key, context) - push(', ') - + const { vaporHelper, push, newline, pushMulti, pushFnCall } = context const { keys, nonKeys, options } = oper.modifiers - if (keys.length) { - push(`${vaporHelper('withKeys')}(`) - } - if (nonKeys.length) { - push(`${vaporHelper('withModifiers')}(`) - } - - // gen event handler - push('(...args) => (') - genExpression(oper.value, context) - push(' && ') - genExpression(oper.value, context) - push('(...args))') - if (nonKeys.length) { - push(`, ${genArrayExpression(nonKeys)})`) - } - if (keys.length) { - push(`, ${genArrayExpression(keys)})`) - } - if (options.length) { - push(`, { ${options.map((v) => `${v}: true`).join(', ')} }`) - } - - push(')') + newline() + pushFnCall( + vaporHelper('on'), + // 1st arg: event name + () => push(`n${oper.element}`), + // 2nd arg: event name + () => { + if (oper.keyOverride) { + const find = JSON.stringify(oper.keyOverride[0]) + const replacement = JSON.stringify(oper.keyOverride[1]) + pushMulti(['(', ')'], () => genExpression(oper.key, context)) + push(` === ${find} ? ${replacement} : `) + pushMulti(['(', ')'], () => genExpression(oper.key, context)) + } else { + genExpression(oper.key, context) + } + }, + // 3rd arg: event handler + () => { + const pushWithKeys = (fn: () => void) => { + push(`${vaporHelper('withKeys')}(`) + fn() + push(`, ${genArrayExpression(keys)})`) + } + const pushWithModifiers = (fn: () => void) => { + push(`${vaporHelper('withModifiers')}(`) + fn() + push(`, ${genArrayExpression(nonKeys)})`) + } + const pushNoop = (fn: () => void) => fn() + + ;(keys.length ? pushWithKeys : pushNoop)(() => + (nonKeys.length ? pushWithModifiers : pushNoop)(() => { + if (oper.value && oper.value.content.trim()) { + push('(...args) => (') + genExpression(oper.value, context) + push(' && ') + genExpression(oper.value, context) + push('(...args))') + } else { + push('() => {}') + } + }), + ) + }, + // 4th arg, gen options + !!options.length && + (() => push(`{ ${options.map((v) => `${v}: true`).join(', ')} }`)), + ) } function genWithDirective(oper: WithDirectiveIRNode, context: CodegenContext) { - const { push, pushWithNewline, vaporHelper, bindingMetadata } = context + const { push, newline, pushFnCall, pushMulti, vaporHelper, bindingMetadata } = + context const { dir } = oper // TODO merge directive for the same node - pushWithNewline(`${vaporHelper('withDirectives')}(n${oper.element}, [[`) - - if (dir.name === 'show') { - push(vaporHelper('vShow')) - } else { - const directiveReference = camelize(`v-${dir.name}`) - // TODO resolve directive - if (bindingMetadata[directiveReference]) { - const directiveExpression = createSimpleExpression(directiveReference) - directiveExpression.ast = null - genExpression(directiveExpression, context) - } - } + newline() + pushFnCall( + vaporHelper('withDirectives'), + // 1st arg: node + `n${oper.element}`, + // 2nd arg: directives + () => { + push('[') + // directive + pushMulti(['[', ']', ', '], () => { + if (dir.name === 'show') { + push(vaporHelper('vShow')) + } else { + const directiveReference = camelize(`v-${dir.name}`) + // TODO resolve directive + if (bindingMetadata[directiveReference]) { + const directiveExpression = + createSimpleExpression(directiveReference) + directiveExpression.ast = null + genExpression(directiveExpression, context) + } + } - if (dir.exp) { - push(', () => ') - genExpression(dir.exp, context) - } else if (dir.arg || dir.modifiers.length) { - push(', void 0') - } + if (dir.exp) { + push(', () => ') + genExpression(dir.exp, context) + } else if (dir.arg || dir.modifiers.length) { + push(', void 0') + } - if (dir.arg) { - push(', ') - genExpression(dir.arg, context) - } else if (dir.modifiers.length) { - push(', void 0') - } + if (dir.arg) { + push(', ') + genExpression(dir.arg, context) + } else if (dir.modifiers.length) { + push(', void 0') + } - if (dir.modifiers.length) { - push(', ') - push('{ ') - push(genDirectiveModifiers(dir.modifiers)) - push(' }') - } - push(']])') - return + if (dir.modifiers.length) { + push(', ') + push('{ ') + push(genDirectiveModifiers(dir.modifiers)) + push(' }') + } + }) + push(']') + }, + ) } // TODO: other types (not only string) @@ -523,22 +593,20 @@ function genExpression(node: IRExpression, context: CodegenContext): void { if (isString(node)) return push(node) const { content: rawExpr, ast, isStatic, loc } = node - if (__BROWSER__) { - return push(rawExpr) + if (isStatic) { + return push(JSON.stringify(rawExpr), NewlineType.None, loc) } - if ( + __BROWSER__ || !context.prefixIdentifiers || !node.content.trim() || // there was a parsing error ast === false || + isGloballyAllowed(rawExpr) || isLiteralWhitelisted(rawExpr) ) { return push(rawExpr, NewlineType.None, loc) } - if (isStatic) { - return push(JSON.stringify(rawExpr), NewlineType.None, loc) - } if (ast === null) { // the expression is a simple identifier diff --git a/packages/compiler-vapor/src/ir.ts b/packages/compiler-vapor/src/ir.ts index 172f32fd7..2af5455d6 100644 --- a/packages/compiler-vapor/src/ir.ts +++ b/packages/compiler-vapor/src/ir.ts @@ -60,6 +60,7 @@ export interface SetPropIRNode extends BaseIRNode { element: number key: IRExpression value: IRExpression + runtimeCamelize: boolean } export interface SetTextIRNode extends BaseIRNode { @@ -68,11 +69,12 @@ export interface SetTextIRNode extends BaseIRNode { value: IRExpression } +export type KeyOverride = [find: string, replacement: string] export interface SetEventIRNode extends BaseIRNode { type: IRNodeTypes.SET_EVENT element: number key: IRExpression - value: IRExpression + value?: SimpleExpressionNode modifiers: { // modifiers for addEventListener() options, e.g. .passive & .capture options: string[] @@ -81,6 +83,7 @@ export interface SetEventIRNode extends BaseIRNode { // modifiers that needs runtime guards, withModifiers nonKeys: string[] } + keyOverride?: KeyOverride } export interface SetHtmlIRNode extends BaseIRNode { diff --git a/packages/compiler-vapor/src/transforms/vBind.ts b/packages/compiler-vapor/src/transforms/vBind.ts index 6bd4cc939..84fd1ac43 100644 --- a/packages/compiler-vapor/src/transforms/vBind.ts +++ b/packages/compiler-vapor/src/transforms/vBind.ts @@ -8,7 +8,7 @@ import { IRNodeTypes } from '../ir' import type { DirectiveTransform } from '../transform' export const transformVBind: DirectiveTransform = (dir, node, context) => { - let { arg, exp, loc } = dir + let { arg, exp, loc, modifiers } = dir if (!arg) { // TODO support v-bind="{}" @@ -21,6 +21,15 @@ export const transformVBind: DirectiveTransform = (dir, node, context) => { exp.ast = null } + let camel = false + if (modifiers.includes('camel')) { + if (arg.isStatic) { + arg.content = camelize(arg.content) + } else { + camel = true + } + } + if (!exp.content.trim()) { context.options.onError( createCompilerError(ErrorCodes.X_V_BIND_NO_EXPRESSION, loc), @@ -38,6 +47,7 @@ export const transformVBind: DirectiveTransform = (dir, node, context) => { element: context.reference(), key: arg, value: exp, + runtimeCamelize: camel, }, ], ) diff --git a/packages/compiler-vapor/src/transforms/vOn.ts b/packages/compiler-vapor/src/transforms/vOn.ts index f1764ffb0..de5fc38a9 100644 --- a/packages/compiler-vapor/src/transforms/vOn.ts +++ b/packages/compiler-vapor/src/transforms/vOn.ts @@ -1,70 +1,64 @@ -import { - createCompilerError, - createSimpleExpression, - ErrorCodes, - ExpressionNode, - isStaticExp, - NodeTypes, -} from '@vue/compiler-core' +import { createCompilerError, ErrorCodes } from '@vue/compiler-core' import type { DirectiveTransform } from '../transform' -import { IRNodeTypes } from '../ir' +import { IRNodeTypes, KeyOverride } from '../ir' import { resolveModifiers } from '@vue/compiler-dom' export const transformVOn: DirectiveTransform = (dir, node, context) => { - const { arg, exp, loc, modifiers } = dir + let { arg, exp, loc, modifiers } = dir if (!exp && !modifiers.length) { context.options.onError( createCompilerError(ErrorCodes.X_V_ON_NO_EXPRESSION, loc), ) - return } if (!arg) { // TODO support v-on="{}" return - } else if (exp === undefined) { - // TODO X_V_ON_NO_EXPRESSION error - return } - const handlerKey = `on${arg.content}` const { keyModifiers, nonKeyModifiers, eventOptionModifiers } = - resolveModifiers(handlerKey, modifiers, null, loc) + resolveModifiers( + arg.isStatic ? `on${arg.content}` : arg, + modifiers, + null, + loc, + ) + + let keyOverride: KeyOverride | undefined // normalize click.right and click.middle since they don't actually fire - let name = arg.content + + const isStaticClick = arg.isStatic && arg.content.toLowerCase() === 'click' + if (nonKeyModifiers.includes('right')) { - name = transformClick(arg, 'contextmenu') + if (isStaticClick) { + arg = { ...arg, content: 'contextmenu' } + } else if (!arg.isStatic) { + keyOverride = ['click', 'contextmenu'] + } } if (nonKeyModifiers.includes('middle')) { - name = transformClick(arg, 'mouseup') + if (keyOverride) { + // TODO error here + } + if (isStaticClick) { + arg = { ...arg, content: 'mouseup' } + } else if (!arg.isStatic) { + keyOverride = ['click', 'mouseup'] + } } - // TODO reactive context.registerOperation({ type: IRNodeTypes.SET_EVENT, loc, element: context.reference(), - key: createSimpleExpression(name, true, arg.loc), + key: arg, value: exp, modifiers: { keys: keyModifiers, nonKeys: nonKeyModifiers, options: eventOptionModifiers, }, + keyOverride, }) } - -function transformClick(key: ExpressionNode, event: string) { - const isStaticClick = - isStaticExp(key) && key.content.toLowerCase() === 'click' - - if (isStaticClick) { - return event - } else if (key.type !== NodeTypes.SIMPLE_EXPRESSION) { - // TODO: handle CompoundExpression - return 'TODO' - } else { - return key.content.toLowerCase() - } -} diff --git a/packages/runtime-vapor/src/apiLifecycle.ts b/packages/runtime-vapor/src/apiLifecycle.ts new file mode 100644 index 000000000..732884ace --- /dev/null +++ b/packages/runtime-vapor/src/apiLifecycle.ts @@ -0,0 +1,66 @@ +import { pauseTracking, resetTracking } from '@vue/reactivity' +import { + type ComponentInternalInstance, + currentInstance, + setCurrentInstance, +} from './component' + +export enum VaporLifecycleHooks { + BEFORE_CREATE = 'bc', + CREATED = 'c', + BEFORE_MOUNT = 'bm', + MOUNTED = 'm', + BEFORE_UPDATE = 'bu', + UPDATED = 'u', + BEFORE_UNMOUNT = 'bum', + UNMOUNTED = 'um', + DEACTIVATED = 'da', + ACTIVATED = 'a', + RENDER_TRIGGERED = 'rtg', + RENDER_TRACKED = 'rtc', + ERROR_CAPTURED = 'ec', + SERVER_PREFETCH = 'sp', +} + +export const injectHook = ( + type: VaporLifecycleHooks, + hook: Function & { __weh?: Function }, + target: ComponentInternalInstance | null = currentInstance, + prepend: boolean = false, +) => { + if (target) { + const hooks = target[type] || (target[type] = []) + const wrappedHook = + hook.__weh || + (hook.__weh = (...args: unknown[]) => { + if (target.isUnmounted) { + return + } + pauseTracking() + setCurrentInstance(target) + // TODO: call error handling + const res = hook(...args) + resetTracking() + return res + }) + if (prepend) { + hooks.unshift(wrappedHook) + } else { + hooks.push(wrappedHook) + } + return wrappedHook + } else if (__DEV__) { + // TODO: warn need + } +} +export const createHook = + any>(lifecycle: VaporLifecycleHooks) => + (hook: T, target: ComponentInternalInstance | null = currentInstance) => + injectHook(lifecycle, (...args: unknown[]) => hook(...args), target) + +export const onMounted = createHook(VaporLifecycleHooks.MOUNTED) +export const onBeforeMount = createHook(VaporLifecycleHooks.BEFORE_MOUNT) +export const onBeforeUpdate = createHook(VaporLifecycleHooks.BEFORE_UPDATE) +export const onUpdated = createHook(VaporLifecycleHooks.UPDATED) +export const onBeforeUnmount = createHook(VaporLifecycleHooks.BEFORE_UNMOUNT) +export const onUnmounted = createHook(VaporLifecycleHooks.UNMOUNTED) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 26518598d..19b1baee8 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -1,30 +1,111 @@ -import { type Ref, EffectScope, ref } from '@vue/reactivity' -import type { Block } from './render' -import type { DirectiveBinding } from './directive' +import { EffectScope, Ref, ref } from '@vue/reactivity' + +import { EMPTY_OBJ } from '@vue/shared' +import { Block } from './render' +import { type DirectiveBinding } from './directive' +import { + type ComponentPropsOptions, + type NormalizedPropsOptions, + normalizePropsOptions, +} from './componentProps' + import type { Data } from '@vue/shared' +export type Component = FunctionalComponent | ObjectComponent +import { VaporLifecycleHooks } from './apiLifecycle' +import { VaporLifecycleHooks } from './apiLifecycle' + export type SetupFn = (props: any, ctx: any) => Block | Data export type FunctionalComponent = SetupFn & { + props: ComponentPropsOptions render(ctx: any): Block } export interface ObjectComponent { + props: ComponentPropsOptions setup: SetupFn render(ctx: any): Block } - +type LifecycleHook = TFn[] | null export interface ComponentInternalInstance { uid: number container: ParentNode block: Block | null scope: EffectScope - component: FunctionalComponent | ObjectComponent - get isMounted(): boolean - isMountedRef: Ref + propsOptions: NormalizedPropsOptions + + // TODO: type + proxy: Data | null + + // state + props: Data + get isUnmounted(): boolean + setupState: Data + isUnmountedRef: Ref /** directives */ dirs: Map + + // lifecycle + get isMounted(): boolean + isMountedRef: Ref // TODO: registory of provides, appContext, lifecycles, ... + /** + * @internal + */ + [VaporLifecycleHooks.BEFORE_CREATE]: LifecycleHook + /** + * @internal + */ + [VaporLifecycleHooks.CREATED]: LifecycleHook + /** + * @internal + */ + [VaporLifecycleHooks.BEFORE_MOUNT]: LifecycleHook + /** + * @internal + */ + [VaporLifecycleHooks.MOUNTED]: LifecycleHook + /** + * @internal + */ + [VaporLifecycleHooks.BEFORE_UPDATE]: LifecycleHook + /** + * @internal + */ + [VaporLifecycleHooks.UPDATED]: LifecycleHook + /** + * @internal + */ + [VaporLifecycleHooks.BEFORE_UNMOUNT]: LifecycleHook + /** + * @internal + */ + [VaporLifecycleHooks.UNMOUNTED]: LifecycleHook + /** + * @internal + */ + [VaporLifecycleHooks.RENDER_TRACKED]: LifecycleHook + /** + * @internal + */ + [VaporLifecycleHooks.RENDER_TRIGGERED]: LifecycleHook + /** + * @internal + */ + [VaporLifecycleHooks.ACTIVATED]: LifecycleHook + /** + * @internal + */ + [VaporLifecycleHooks.DEACTIVATED]: LifecycleHook + /** + * @internal + */ + [VaporLifecycleHooks.ERROR_CAPTURED]: LifecycleHook + /** + * @internal + */ + [VaporLifecycleHooks.SERVER_PREFETCH]: LifecycleHook<() => Promise> } // TODO @@ -46,20 +127,92 @@ export const createComponentInstance = ( component: ObjectComponent | FunctionalComponent, ): ComponentInternalInstance => { const isMountedRef = ref(false) + const isUnmountedRef = ref(false) const instance: ComponentInternalInstance = { uid: uid++, block: null, - container: null!, // set on mount + container: null!, scope: new EffectScope(true /* detached */)!, - component, + + // resolved props and emits options + propsOptions: normalizePropsOptions(component), + // emitsOptions: normalizeEmitsOptions(type, appContext), // TODO: + + proxy: null, + + // state + props: EMPTY_OBJ, + setupState: EMPTY_OBJ, + + dirs: new Map(), + + // lifecycle get isMounted() { return isMountedRef.value }, + get isUnmounted() { + return isUnmountedRef.value + }, isMountedRef, - - dirs: new Map(), + isUnmountedRef, // TODO: registory of provides, appContext, lifecycles, ... + /** + * @internal + */ + [VaporLifecycleHooks.BEFORE_CREATE]: null, + /** + * @internal + */ + [VaporLifecycleHooks.CREATED]: null, + /** + * @internal + */ + [VaporLifecycleHooks.BEFORE_MOUNT]: null, + /** + * @internal + */ + [VaporLifecycleHooks.MOUNTED]: null, + /** + * @internal + */ + [VaporLifecycleHooks.BEFORE_UPDATE]: null, + /** + * @internal + */ + [VaporLifecycleHooks.UPDATED]: null, + /** + * @internal + */ + [VaporLifecycleHooks.BEFORE_UNMOUNT]: null, + /** + * @internal + */ + [VaporLifecycleHooks.UNMOUNTED]: null, + /** + * @internal + */ + [VaporLifecycleHooks.RENDER_TRACKED]: null, + /** + * @internal + */ + [VaporLifecycleHooks.RENDER_TRIGGERED]: null, + /** + * @internal + */ + [VaporLifecycleHooks.ACTIVATED]: null, + /** + * @internal + */ + [VaporLifecycleHooks.DEACTIVATED]: null, + /** + * @internal + */ + [VaporLifecycleHooks.ERROR_CAPTURED]: null, + /** + * @internal + */ + [VaporLifecycleHooks.SERVER_PREFETCH]: null, } return instance } diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts new file mode 100644 index 000000000..5cd0f1d21 --- /dev/null +++ b/packages/runtime-vapor/src/componentProps.ts @@ -0,0 +1,267 @@ +// NOTE: runtime-core/src/componentProps.ts + +import { + Data, + EMPTY_ARR, + EMPTY_OBJ, + camelize, + extend, + hasOwn, + hyphenate, + isArray, + isFunction, + isReservedProp, +} from '@vue/shared' +import { shallowReactive, toRaw } from '@vue/reactivity' +import { type ComponentInternalInstance, type Component } from './component' + +export type ComponentPropsOptions

= + | ComponentObjectPropsOptions

+ | string[] + +export type ComponentObjectPropsOptions

= { + [K in keyof P]: Prop | null +} + +export type Prop = PropOptions | PropType + +type DefaultFactory = (props: Data) => T | null | undefined + +export interface PropOptions { + type?: PropType | true | null + required?: boolean + default?: D | DefaultFactory | null | undefined | object + validator?(value: unknown): boolean + /** + * @internal + */ + skipFactory?: boolean +} + +export type PropType = PropConstructor | PropConstructor[] + +type PropConstructor = + | { new (...args: any[]): T & {} } + | { (): T } + | PropMethod + +type PropMethod = [T] extends [ + ((...args: any) => any) | undefined, +] // if is function with args, allowing non-required functions + ? { new (): TConstructor; (): T; readonly prototype: TConstructor } // Create Function like constructor + : never + +enum BooleanFlags { + shouldCast, + shouldCastTrue, +} + +type NormalizedProp = + | null + | (PropOptions & { + [BooleanFlags.shouldCast]?: boolean + [BooleanFlags.shouldCastTrue]?: boolean + }) + +export type NormalizedProps = Record +export type NormalizedPropsOptions = [NormalizedProps, string[]] | [] + +export function initProps( + instance: ComponentInternalInstance, + rawProps: Data | null, +) { + const props: Data = {} + + const [options, needCastKeys] = instance.propsOptions + let rawCastValues: Data | undefined + if (rawProps) { + for (let key in rawProps) { + // key, ref are reserved and never passed down + if (isReservedProp(key)) { + continue + } + + const valueGetter = () => rawProps[key] + let camelKey + if (options && hasOwn(options, (camelKey = camelize(key)))) { + if (!needCastKeys || !needCastKeys.includes(camelKey)) { + // NOTE: must getter + // props[camelKey] = value + Object.defineProperty(props, camelKey, { + get() { + return valueGetter() + }, + }) + } else { + // NOTE: must getter + // ;(rawCastValues || (rawCastValues = {}))[camelKey] = value + rawCastValues || (rawCastValues = {}) + Object.defineProperty(rawCastValues, camelKey, { + get() { + return valueGetter() + }, + }) + } + } else { + // TODO: + } + } + } + + if (needCastKeys) { + const rawCurrentProps = toRaw(props) + const castValues = rawCastValues || EMPTY_OBJ + for (let i = 0; i < needCastKeys.length; i++) { + const key = needCastKeys[i] + + // NOTE: must getter + // props[key] = resolvePropValue( + // options!, + // rawCurrentProps, + // key, + // castValues[key], + // instance, + // !hasOwn(castValues, key), + // ) + Object.defineProperty(props, key, { + get() { + return resolvePropValue( + options!, + rawCurrentProps, + key, + castValues[key], + instance, + !hasOwn(castValues, key), + ) + }, + }) + } + } + + instance.props = shallowReactive(props) +} + +function resolvePropValue( + options: NormalizedProps, + props: Data, + key: string, + value: unknown, + instance: ComponentInternalInstance, + isAbsent: boolean, +) { + const opt = options[key] + if (opt != null) { + const hasDefault = hasOwn(opt, 'default') + // default values + if (hasDefault && value === undefined) { + const defaultValue = opt.default + if ( + opt.type !== Function && + !opt.skipFactory && + isFunction(defaultValue) + ) { + // TODO: caching? + // const { propsDefaults } = instance + // if (key in propsDefaults) { + // value = propsDefaults[key] + // } else { + // setCurrentInstance(instance) + // value = propsDefaults[key] = defaultValue.call( + // __COMPAT__ && + // isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS, instance) + // ? createPropsDefaultThis(instance, props, key) + // : null, + // props, + // ) + // unsetCurrentInstance() + // } + } else { + value = defaultValue + } + } + // boolean casting + if (opt[BooleanFlags.shouldCast]) { + if (isAbsent && !hasDefault) { + value = false + } else if ( + opt[BooleanFlags.shouldCastTrue] && + (value === '' || value === hyphenate(key)) + ) { + value = true + } + } + } + return value +} + +export function normalizePropsOptions(comp: Component): NormalizedPropsOptions { + // TODO: cahching? + + const raw = comp.props as any + const normalized: NormalizedPropsOptions[0] = {} + const needCastKeys: NormalizedPropsOptions[1] = [] + + if (!raw) { + return EMPTY_ARR as any + } + + if (isArray(raw)) { + for (let i = 0; i < raw.length; i++) { + const normalizedKey = camelize(raw[i]) + if (validatePropName(normalizedKey)) { + normalized[normalizedKey] = EMPTY_OBJ + } + } + } else if (raw) { + for (const key in raw) { + const normalizedKey = camelize(key) + if (validatePropName(normalizedKey)) { + const opt = raw[key] + const prop: NormalizedProp = (normalized[normalizedKey] = + isArray(opt) || isFunction(opt) ? { type: opt } : extend({}, opt)) + if (prop) { + const booleanIndex = getTypeIndex(Boolean, prop.type) + const stringIndex = getTypeIndex(String, prop.type) + prop[BooleanFlags.shouldCast] = booleanIndex > -1 + prop[BooleanFlags.shouldCastTrue] = + stringIndex < 0 || booleanIndex < stringIndex + // if the prop needs boolean casting or default value + if (booleanIndex > -1 || hasOwn(prop, 'default')) { + needCastKeys.push(normalizedKey) + } + } + } + } + } + + const res: NormalizedPropsOptions = [normalized, needCastKeys] + return res +} + +function validatePropName(key: string) { + if (key[0] !== '$') { + return true + } + return false +} + +function getType(ctor: Prop): string { + const match = ctor && ctor.toString().match(/^\s*(function|class) (\w+)/) + return match ? match[2] : ctor === null ? 'null' : '' +} + +function isSameType(a: Prop, b: Prop): boolean { + return getType(a) === getType(b) +} + +function getTypeIndex( + type: Prop, + expectedTypes: PropType | void | null | true, +): number { + if (isArray(expectedTypes)) { + return expectedTypes.findIndex((t) => isSameType(t, type)) + } else if (isFunction(expectedTypes)) { + return isSameType(expectedTypes, type) ? 0 : -1 + } + return -1 +} diff --git a/packages/runtime-vapor/src/componentPublicInstance.ts b/packages/runtime-vapor/src/componentPublicInstance.ts new file mode 100644 index 000000000..8bfacf981 --- /dev/null +++ b/packages/runtime-vapor/src/componentPublicInstance.ts @@ -0,0 +1,22 @@ +import { hasOwn } from '@vue/shared' +import { type ComponentInternalInstance } from './component' + +export interface ComponentRenderContext { + [key: string]: any + _: ComponentInternalInstance +} + +export const PublicInstanceProxyHandlers: ProxyHandler = { + get({ _: instance }: ComponentRenderContext, key: string) { + let normalizedProps + const { setupState, props } = instance + if (hasOwn(setupState, key)) { + return setupState[key] + } else if ( + (normalizedProps = instance.propsOptions[0]) && + hasOwn(normalizedProps, key) + ) { + return props![key] + } + }, +} diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 0c56476ad..69cca595a 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -44,3 +44,4 @@ export * from './scheduler' export * from './directive' export * from './dom' export * from './directives/vShow' +export * from './apiLifecycle' diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index b03e56ae8..ed4aa32da 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -1,14 +1,21 @@ -import { reactive } from '@vue/reactivity' +import { markRaw, proxyRefs } from '@vue/reactivity' +import { type Data } from '@vue/shared' + import { + type Component, type ComponentInternalInstance, - type FunctionalComponent, - type ObjectComponent, createComponentInstance, setCurrentInstance, unsetCurrentInstance, } from './component' + +import { initProps } from './componentProps' + import { invokeDirectiveHook } from './directive' + import { insert, remove } from './dom' +import { PublicInstanceProxyHandlers } from './componentPublicInstance' +import { invokeArrayFns } from '@vue/shared' export type Block = Node | Fragment | Block[] export type ParentBlock = ParentNode | Node[] @@ -16,13 +23,13 @@ export type Fragment = { nodes: Block; anchor: Node } export type BlockFn = (props: any, ctx: any) => Block export function render( - comp: ObjectComponent | FunctionalComponent, + comp: Component, + props: Data, container: string | ParentNode, ): ComponentInternalInstance { const instance = createComponentInstance(comp) - setCurrentInstance(instance) - mountComponent(instance, (container = normalizeContainer(container))) - return instance + initProps(instance, props) + return mountComponent(instance, (container = normalizeContainer(container))) } export function normalizeContainer(container: string | ParentNode): ParentNode { @@ -30,7 +37,6 @@ export function normalizeContainer(container: string | ParentNode): ParentNode { ? (document.querySelector(container) as ParentNode) : container } - export function mountComponent( instance: ComponentInternalInstance, container: ParentNode, @@ -39,29 +45,34 @@ export function mountComponent( setCurrentInstance(instance) const block = instance.scope.run(() => { - const { component } = instance - const props = {} + const { component, props } = instance const ctx = { expose: () => {} } const setupFn = typeof component === 'function' ? component : component.setup const state = setupFn(props, ctx) + instance.proxy = markRaw( + new Proxy({ _: instance }, PublicInstanceProxyHandlers), + ) if (state && '__isScriptSetup' in state) { - return (instance.block = component.render(reactive(state))) + instance.setupState = proxyRefs(state) + return (instance.block = component.render(instance.proxy)) } else { return (instance.block = state as Block) } })! - invokeDirectiveHook(instance, 'beforeMount') insert(block, instance.container) instance.isMountedRef.value = true invokeDirectiveHook(instance, 'mounted') + unsetCurrentInstance() + const { m } = instance + if (m) { + invokeArrayFns(m) + } - // TODO: lifecycle hooks (mounted, ...) - // const { m } = instance - // m && invoke(m) + return instance } export function unmountComponent(instance: ComponentInternalInstance) { @@ -73,8 +84,9 @@ export function unmountComponent(instance: ComponentInternalInstance) { instance.isMountedRef.value = false invokeDirectiveHook(instance, 'unmounted') unsetCurrentInstance() - - // TODO: lifecycle hooks (unmounted, ...) - // const { um } = instance - // um && invoke(um) + const { um } = instance + if (um) { + invokeArrayFns(um) + } + instance.isUnmountedRef.value = true } diff --git a/playground/src/main.ts b/playground/src/main.ts index 43565bc94..717629057 100644 --- a/playground/src/main.ts +++ b/playground/src/main.ts @@ -1,6 +1,6 @@ import { render } from 'vue/vapor' -const modules = import.meta.glob('./*.vue') +const modules = import.meta.glob('./*.(vue|js)') const mod = (modules['.' + location.pathname] || modules['./App.vue'])() -mod.then(({ default: mod }) => render(mod, '#app')) +mod.then(({ default: mod }) => render(mod, {}, '#app')) diff --git a/playground/src/props.js b/playground/src/props.js new file mode 100644 index 000000000..b80768dcc --- /dev/null +++ b/playground/src/props.js @@ -0,0 +1,104 @@ +import { watch } from 'vue' +import { + children, + on, + ref, + template, + effect, + setText, + render as renderComponent // TODO: +} from '@vue/vapor' + +export default { + props: undefined, + + setup(_, {}) { + const count = ref(1) + const handleClick = () => { + count.value++ + } + + const __returned__ = { count, handleClick } + + Object.defineProperty(__returned__, '__isScriptSetup', { + enumerable: false, + value: true + }) + + return __returned__ + }, + + render(_ctx) { + const t0 = template('') + const n0 = t0() + const { + 0: [n1] + } = children(n0) + on(n1, 'click', _ctx.handleClick) + effect(() => { + setText(n1, void 0, _ctx.count) + }) + + // TODO: create component fn? + // const c0 = createComponent(...) + // insert(n0, c0) + renderComponent( + child, + + // TODO: proxy?? + { + /* */ + get count() { + return _ctx.count + }, + + /* */ + get inlineDouble() { + return _ctx.count * 2 + } + }, + n0 + ) + + return n0 + } +} + +const child = { + props: { + count: { type: Number, default: 1 }, + inlineDouble: { type: Number, default: 2 } + }, + + setup(props) { + watch( + () => props.count, + v => console.log('count changed', v) + ) + watch( + () => props.inlineDouble, + v => console.log('inlineDouble changed', v) + ) + + const __returned__ = {} + + Object.defineProperty(__returned__, '__isScriptSetup', { + enumerable: false, + value: true + }) + + return __returned__ + }, + + render(_ctx) { + const t0 = template('

') + const n0 = t0() + const { + 0: [n1] + } = children(n0) + effect(() => { + setText(n1, void 0, _ctx.count + ' * 2 = ' + _ctx.inlineDouble) + }) + return n0 + } +}