diff --git a/packages/uni-app-uts/__tests__/android/transforms/transformElement.spec.ts b/packages/uni-app-uts/__tests__/android/transforms/transformElement.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..8f41b94c601a231abb2cde92e340315834a4d87b --- /dev/null +++ b/packages/uni-app-uts/__tests__/android/transforms/transformElement.spec.ts @@ -0,0 +1,1291 @@ +import { PatchFlags } from '@vue/shared' +import { + RESOLVE_COMPONENT, + CREATE_VNODE, + MERGE_PROPS, + RESOLVE_DIRECTIVE, + TO_HANDLERS, + helperNameMap, + TELEPORT, + RESOLVE_DYNAMIC_COMPONENT, + SUSPENSE, + KEEP_ALIVE, + BASE_TRANSITION, + NORMALIZE_CLASS, + NORMALIZE_STYLE, + NORMALIZE_PROPS, + GUARD_REACTIVE_PROPS, + baseParse as parse, + BindingTypes, +} from '@vue/compiler-core' +import { + NodeTypes, + createObjectProperty, + DirectiveNode, + RootNode, + VNodeCall, +} from '@vue/compiler-core' +import { transformElement as baseTransformElement } from '@vue/compiler-core' +import { compile as baseCompile } from '../../../src/plugins/android/uvue/compiler' +import { transformStyle } from '../../../src/plugins/android/uvue/compiler/transforms/transformStyle' +import { transformOn } from '../../../src/plugins/android/uvue/compiler/transforms/vOn' +import { transformBind } from '../../../src/plugins/android/uvue/compiler/transforms/vBind' + +import { createObjectMatcher, genFlagText } from '../testUtils' +import { transformText } from '../../../src/plugins/android/uvue/compiler/transforms/transformText' +import { CompilerOptions } from '../../../src/plugins/android/uvue/compiler/options' +import { + NodeTransform, + transform, +} from '../../../src/plugins/android/uvue/compiler/transform' +import { transformExpression } from '../../../src/plugins/android/uvue/compiler/transforms/transformExpression' + +const transformElement = baseTransformElement as unknown as NodeTransform + +function parseWithElementTransform( + template: string, + options: CompilerOptions = {} +): { + root: RootNode + node: VNodeCall +} { + // wrap raw template in an extra view so that it doesn't get turned into a + // block as root node + const ast = parse(`${template}`, options) + transform(ast, { + nodeTransforms: [transformElement, transformText], + ...options, + }) + const codegenNode = (ast as any).children[0].children[0] + .codegenNode as VNodeCall + expect(codegenNode.type).toBe(NodeTypes.VNODE_CALL) + return { + root: ast, + node: codegenNode, + } +} + +function parseWithBind(template: string, options?: CompilerOptions) { + return parseWithElementTransform(template, { + ...options, + directiveTransforms: { + ...options?.directiveTransforms, + bind: transformBind, + }, + }) +} + +describe('compiler: element transform', () => { + test('import + resolve component', () => { + const { root } = parseWithElementTransform(``) + expect(root.helpers).toContain(RESOLVE_COMPONENT) + expect(root.components).toContain(`Foo`) + }) + + test('resolve implicitly self-referencing component', () => { + const { root } = parseWithElementTransform(``, { + filename: `/foo/bar/Example.vue?vue&type=template`, + }) + expect(root.helpers).toContain(RESOLVE_COMPONENT) + expect(root.components).toContain(`Example__self`) + }) + + test('resolve component from setup bindings', () => { + const { root, node } = parseWithElementTransform(``, { + bindingMetadata: { + Example: BindingTypes.SETUP_MAYBE_REF, + }, + }) + expect(root.helpers).not.toContain(RESOLVE_COMPONENT) + expect(node.tag).toBe(`$setup["Example"]`) + }) + + // test('resolve component from setup bindings (inline)', () => { + // const { root, node } = parseWithElementTransform(``, { + // inline: true, + // bindingMetadata: { + // Example: BindingTypes.SETUP_MAYBE_REF, + // }, + // }) + // expect(root.helpers).not.toContain(RESOLVE_COMPONENT) + // expect(node.tag).toBe(`_unref(Example)`) + // }) + + // test('resolve component from setup bindings (inline const)', () => { + // const { root, node } = parseWithElementTransform(``, { + // inline: true, + // bindingMetadata: { + // Example: BindingTypes.SETUP_CONST, + // }, + // }) + // expect(root.helpers).not.toContain(RESOLVE_COMPONENT) + // expect(node.tag).toBe(`Example`) + // }) + + test('resolve namespaced component from setup bindings', () => { + const { root, node } = parseWithElementTransform(``, { + bindingMetadata: { + Foo: BindingTypes.SETUP_MAYBE_REF, + }, + }) + expect(root.helpers).not.toContain(RESOLVE_COMPONENT) + expect(node.tag).toBe(`$setup["Foo"].Example`) + }) + + // test('resolve namespaced component from setup bindings (inline)', () => { + // const { root, node } = parseWithElementTransform(``, { + // inline: true, + // bindingMetadata: { + // Foo: BindingTypes.SETUP_MAYBE_REF, + // }, + // }) + // expect(root.helpers).not.toContain(RESOLVE_COMPONENT) + // expect(node.tag).toBe(`_unref(Foo).Example`) + // }) + + // test('resolve namespaced component from setup bindings (inline const)', () => { + // const { root, node } = parseWithElementTransform(``, { + // inline: true, + // bindingMetadata: { + // Foo: BindingTypes.SETUP_CONST, + // }, + // }) + // expect(root.helpers).not.toContain(RESOLVE_COMPONENT) + // expect(node.tag).toBe(`Foo.Example`) + // }) + + test('do not resolve component from non-script-setup bindings', () => { + const bindingMetadata = { + Example: BindingTypes.SETUP_MAYBE_REF, + } + Object.defineProperty(bindingMetadata, '__isScriptSetup', { value: false }) + const { root } = parseWithElementTransform(``, { + bindingMetadata, + }) + expect(root.helpers).toContain(RESOLVE_COMPONENT) + expect(root.components).toContain(`Example`) + }) + + test('static props', () => { + const { node } = parseWithElementTransform(``) + expect(node).toMatchObject({ + tag: `"view"`, + props: createObjectMatcher({ + id: 'foo', + class: 'bar', + }), + children: undefined, + }) + }) + + test('props + children', () => { + const { node } = parseWithElementTransform(``) + + expect(node).toMatchObject({ + tag: `"view"`, + props: createObjectMatcher({ + id: 'foo', + }), + children: [ + { + type: NodeTypes.ELEMENT, + tag: 'text', + codegenNode: { + type: NodeTypes.VNODE_CALL, + tag: `"text"`, + }, + }, + ], + }) + }) + + test('0 placeholder for children with no props', () => { + const { node } = parseWithElementTransform(``) + + expect(node).toMatchObject({ + tag: `"view"`, + props: undefined, + children: [ + { + type: NodeTypes.ELEMENT, + tag: 'text', + codegenNode: { + type: NodeTypes.VNODE_CALL, + tag: `"text"`, + }, + }, + ], + }) + }) + + test('v-bind="obj"', () => { + const { root, node } = parseWithElementTransform(``) + // single v-bind doesn't need mergeProps + expect(root.helpers).not.toContain(MERGE_PROPS) + expect(root.helpers).toContain(NORMALIZE_PROPS) + expect(root.helpers).toContain(GUARD_REACTIVE_PROPS) + + // should directly use `obj` in props position + expect(node.props).toMatchObject({ + type: NodeTypes.JS_CALL_EXPRESSION, + callee: NORMALIZE_PROPS, + arguments: [ + { + type: NodeTypes.JS_CALL_EXPRESSION, + callee: GUARD_REACTIVE_PROPS, + arguments: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `obj`, + }, + ], + }, + ], + }) + }) + + test('v-bind="obj" after static prop', () => { + const { root, node } = parseWithElementTransform( + `` + ) + expect(root.helpers).toContain(MERGE_PROPS) + + expect(node.props).toMatchObject({ + type: NodeTypes.JS_CALL_EXPRESSION, + callee: MERGE_PROPS, + arguments: [ + createObjectMatcher({ + id: 'foo', + }), + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `obj`, + }, + ], + }) + }) + + test('v-bind="obj" before static prop', () => { + const { root, node } = parseWithElementTransform( + `` + ) + expect(root.helpers).toContain(MERGE_PROPS) + + expect(node.props).toMatchObject({ + type: NodeTypes.JS_CALL_EXPRESSION, + callee: MERGE_PROPS, + arguments: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `obj`, + }, + createObjectMatcher({ + id: 'foo', + }), + ], + }) + }) + + test('v-bind="obj" between static props', () => { + const { root, node } = parseWithElementTransform( + `` + ) + expect(root.helpers).toContain(MERGE_PROPS) + + expect(node.props).toMatchObject({ + type: NodeTypes.JS_CALL_EXPRESSION, + callee: MERGE_PROPS, + arguments: [ + createObjectMatcher({ + id: 'foo', + }), + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `obj`, + }, + createObjectMatcher({ + class: 'bar', + }), + ], + }) + }) + + test('v-on="obj"', () => { + const { root, node } = parseWithElementTransform( + `` + ) + expect(root.helpers).toContain(MERGE_PROPS) + + expect(node.props).toMatchObject({ + type: NodeTypes.JS_CALL_EXPRESSION, + callee: MERGE_PROPS, + arguments: [ + createObjectMatcher({ + id: 'foo', + }), + { + type: NodeTypes.JS_CALL_EXPRESSION, + callee: TO_HANDLERS, + arguments: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `obj`, + }, + `true`, + ], + }, + createObjectMatcher({ + class: 'bar', + }), + ], + }) + }) + + test('v-on="obj" on component', () => { + const { root, node } = parseWithElementTransform( + `` + ) + expect(root.helpers).toContain(MERGE_PROPS) + + expect(node.props).toMatchObject({ + type: NodeTypes.JS_CALL_EXPRESSION, + callee: MERGE_PROPS, + arguments: [ + createObjectMatcher({ + id: 'foo', + }), + { + type: NodeTypes.JS_CALL_EXPRESSION, + callee: TO_HANDLERS, + arguments: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `obj`, + }, + ], + }, + createObjectMatcher({ + class: 'bar', + }), + ], + }) + }) + + test('v-on="obj" + v-bind="obj"', () => { + const { root, node } = parseWithElementTransform( + `` + ) + expect(root.helpers).toContain(MERGE_PROPS) + + expect(node.props).toMatchObject({ + type: NodeTypes.JS_CALL_EXPRESSION, + callee: MERGE_PROPS, + arguments: [ + createObjectMatcher({ + id: 'foo', + }), + { + type: NodeTypes.JS_CALL_EXPRESSION, + callee: TO_HANDLERS, + arguments: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `handlers`, + }, + `true`, + ], + }, + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `obj`, + }, + ], + }) + }) + + test('should handle plain as normal element', () => { + const { node } = parseWithElementTransform(``) + + expect(node).toMatchObject({ + tag: `"template"`, + props: createObjectMatcher({ + id: 'foo', + }), + }) + }) + + test('should handle with normal children', () => { + function assert(tag: string) { + const { root, node } = parseWithElementTransform( + `<${tag} target="#foo">${tag}>` + ) + expect(root.components.length).toBe(0) + expect(root.helpers).toContain(TELEPORT) + + expect(node).toMatchObject({ + tag: TELEPORT, + props: createObjectMatcher({ + target: '#foo', + }), + children: [ + { + type: NodeTypes.ELEMENT, + tag: 'text', + codegenNode: { + type: NodeTypes.VNODE_CALL, + tag: `"text"`, + }, + }, + ], + }) + } + + assert(`teleport`) + assert(`Teleport`) + }) + + test('should handle ', () => { + function assert(tag: string, content: string, hasFallback?: boolean) { + const { root, node } = parseWithElementTransform( + `<${tag}>${content}${tag}>` + ) + expect(root.components.length).toBe(0) + expect(root.helpers).toContain(SUSPENSE) + + expect(node).toMatchObject({ + tag: SUSPENSE, + props: undefined, + children: hasFallback + ? createObjectMatcher({ + default: { + type: NodeTypes.JS_FUNCTION_EXPRESSION, + }, + fallback: { + type: NodeTypes.JS_FUNCTION_EXPRESSION, + }, + _: `[1 /* STABLE */]`, + }) + : createObjectMatcher({ + default: { + type: NodeTypes.JS_FUNCTION_EXPRESSION, + }, + _: `[1 /* STABLE */]`, + }), + }) + } + + assert(`suspense`, `foo`) + assert(`suspense`, `foo`) + assert( + `suspense`, + `foofallback`, + true + ) + }) + + test('should handle ', () => { + function assert(tag: string) { + const root = parse(`<${tag}>${tag}>`) + transform(root, { + nodeTransforms: [transformElement, transformText], + }) + expect(root.components.length).toBe(0) + expect(root.helpers).toContain(KEEP_ALIVE) + const node = (root.children[0] as any).children[0].codegenNode + expect(node).toMatchObject({ + type: NodeTypes.VNODE_CALL, + tag: KEEP_ALIVE, + isBlock: true, // should be forced into a block + props: undefined, + // keep-alive should not compile content to slots + children: [{ type: NodeTypes.ELEMENT, tag: 'text' }], + // should get a dynamic slots flag to force updates + patchFlag: genFlagText(PatchFlags.DYNAMIC_SLOTS), + }) + } + + assert(`keep-alive`) + assert(`KeepAlive`) + }) + + test('should handle ', () => { + function assert(tag: string) { + const { root, node } = parseWithElementTransform( + `<${tag}>${tag}>` + ) + expect(root.components.length).toBe(0) + expect(root.helpers).toContain(BASE_TRANSITION) + + expect(node).toMatchObject({ + tag: BASE_TRANSITION, + props: undefined, + children: createObjectMatcher({ + default: { + type: NodeTypes.JS_FUNCTION_EXPRESSION, + }, + _: `[1 /* STABLE */]`, + }), + }) + } + + assert(`base-transition`) + assert(`BaseTransition`) + }) + + // test('error on v-bind with no argument', async () => { + // const onError = vi.fn() + // parseWithElementTransform(``, { onError }) + // expect(onError.mock.calls[0]).toMatchObject([ + // { + // code: ErrorCodes.X_V_BIND_NO_EXPRESSION, + // }, + // ]) + // }) + + test('directiveTransforms', () => { + let _dir: DirectiveNode + const { node } = parseWithElementTransform(``, { + directiveTransforms: { + foo(dir) { + _dir = dir + return { + props: [createObjectProperty(dir.arg!, dir.exp!)], + } + }, + }, + }) + + expect(node.props).toMatchObject({ + type: NodeTypes.JS_OBJECT_EXPRESSION, + properties: [ + { + type: NodeTypes.JS_PROPERTY, + key: _dir!.arg, + value: _dir!.exp, + }, + ], + }) + // should factor in props returned by custom directive transforms + // in patchFlag analysis + expect(node.patchFlag).toMatch(PatchFlags.PROPS + '') + expect(node.dynamicProps).toMatch(`"bar"`) + }) + + test('directiveTransform with needRuntime: true', () => { + const { root, node } = parseWithElementTransform( + ``, + { + directiveTransforms: { + foo() { + return { + props: [], + needRuntime: true, + } + }, + }, + } + ) + expect(root.helpers).toContain(RESOLVE_DIRECTIVE) + expect(root.directives).toContain(`foo`) + expect(node).toMatchObject({ + tag: `"view"`, + props: undefined, + children: undefined, + patchFlag: genFlagText(PatchFlags.NEED_PATCH), // should generate appropriate flag + directives: { + type: NodeTypes.JS_ARRAY_EXPRESSION, + elements: [ + { + type: NodeTypes.JS_ARRAY_EXPRESSION, + elements: [ + `_directive_foo`, + // exp + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `hello`, + isStatic: false, + }, + // arg + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `bar`, + isStatic: true, + }, + ], + }, + ], + }, + }) + }) + + test('directiveTransform with needRuntime: Symbol', () => { + const { root, node } = parseWithElementTransform( + ``, + { + directiveTransforms: { + foo() { + return { + props: [], + needRuntime: CREATE_VNODE, + } + }, + }, + } + ) + + expect(root.helpers).toContain(CREATE_VNODE) + expect(root.helpers).not.toContain(RESOLVE_DIRECTIVE) + expect(root.directives.length).toBe(0) + expect(node.directives!.elements[0].elements[0]).toBe( + `${helperNameMap[CREATE_VNODE]}` + ) + }) + + test('runtime directives', () => { + const { root, node } = parseWithElementTransform( + `` + ) + expect(root.helpers).toContain(RESOLVE_DIRECTIVE) + expect(root.directives).toContain(`foo`) + expect(root.directives).toContain(`bar`) + expect(root.directives).toContain(`baz`) + + expect(node).toMatchObject({ + directives: { + type: NodeTypes.JS_ARRAY_EXPRESSION, + elements: [ + { + type: NodeTypes.JS_ARRAY_EXPRESSION, + elements: [`_directive_foo`], + }, + { + type: NodeTypes.JS_ARRAY_EXPRESSION, + elements: [ + `_directive_bar`, + // exp + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `x`, + }, + ], + }, + { + type: NodeTypes.JS_ARRAY_EXPRESSION, + elements: [ + `_directive_baz`, + // exp + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `y`, + isStatic: false, + }, + // arg + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `arg`, + isStatic: false, + }, + // modifiers + { + type: NodeTypes.JS_OBJECT_EXPRESSION, + properties: [ + { + type: NodeTypes.JS_PROPERTY, + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `mod`, + isStatic: true, + }, + value: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `true`, + isStatic: false, + }, + }, + { + type: NodeTypes.JS_PROPERTY, + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `mad`, + isStatic: true, + }, + value: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `true`, + isStatic: false, + }, + }, + ], + }, + ], + }, + ], + }, + }) + }) + + test(`props merging: event handlers`, () => { + const { node } = parseWithElementTransform( + ``, + { + directiveTransforms: { + on: transformOn, + }, + } + ) + expect(node.props).toMatchObject({ + type: NodeTypes.JS_OBJECT_EXPRESSION, + properties: [ + { + type: NodeTypes.JS_PROPERTY, + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `onClick`, + isStatic: true, + }, + value: { + type: NodeTypes.JS_ARRAY_EXPRESSION, + elements: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `a`, + isStatic: false, + }, + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `b`, + isStatic: false, + }, + ], + }, + }, + ], + }) + }) + + test(`props merging: style`, () => { + const { node, root } = parseWithElementTransform( + ``, + { + nodeTransforms: [transformStyle, transformElement], + directiveTransforms: { + bind: transformBind, + }, + } + ) + expect(root.helpers).toContain(NORMALIZE_STYLE) + expect(node.props).toMatchObject({ + type: NodeTypes.JS_OBJECT_EXPRESSION, + properties: [ + { + type: NodeTypes.JS_PROPERTY, + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `style`, + isStatic: true, + }, + value: { + type: NodeTypes.JS_CALL_EXPRESSION, + callee: NORMALIZE_STYLE, + arguments: [ + { + type: NodeTypes.JS_ARRAY_EXPRESSION, + elements: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `color: green`, + isStatic: true, + // TODO 待处理 + // content: `{"color":"green"}`, + // isStatic: false, + }, + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `{ color: 'red' }`, + isStatic: false, + }, + ], + }, + ], + }, + }, + ], + }) + }) + + test(`props merging: style w/ transformExpression`, () => { + const { node, root } = parseWithElementTransform( + ``, + { + nodeTransforms: [transformExpression, transformStyle, transformElement], + directiveTransforms: { + bind: transformBind, + }, + prefixIdentifiers: true, + } + ) + expect(root.helpers).toContain(NORMALIZE_STYLE) + expect(node.props).toMatchObject({ + type: NodeTypes.JS_OBJECT_EXPRESSION, + properties: [ + { + type: NodeTypes.JS_PROPERTY, + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `style`, + isStatic: true, + }, + value: { + type: NodeTypes.JS_CALL_EXPRESSION, + callee: NORMALIZE_STYLE, + }, + }, + ], + }) + }) + + test(':style with array literal', () => { + const { node, root } = parseWithElementTransform( + ``, + { + nodeTransforms: [transformExpression, transformStyle, transformElement], + directiveTransforms: { + bind: transformBind, + }, + prefixIdentifiers: true, + } + ) + expect(root.helpers).toContain(NORMALIZE_STYLE) + expect(node.props).toMatchObject({ + type: NodeTypes.JS_OBJECT_EXPRESSION, + properties: [ + { + type: NodeTypes.JS_PROPERTY, + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `style`, + isStatic: true, + }, + value: { + type: NodeTypes.JS_CALL_EXPRESSION, + callee: NORMALIZE_STYLE, + }, + }, + ], + }) + }) + + test(`props merging: class`, () => { + const { node, root } = parseWithElementTransform( + ``, + { + directiveTransforms: { + bind: transformBind, + }, + } + ) + expect(root.helpers).toContain(NORMALIZE_CLASS) + expect(node.props).toMatchObject({ + type: NodeTypes.JS_OBJECT_EXPRESSION, + properties: [ + { + type: NodeTypes.JS_PROPERTY, + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `class`, + isStatic: true, + }, + value: { + type: NodeTypes.JS_CALL_EXPRESSION, + callee: NORMALIZE_CLASS, + arguments: [ + { + type: NodeTypes.JS_ARRAY_EXPRESSION, + elements: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `foo`, + isStatic: true, + }, + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `{ bar: isBar }`, + isStatic: false, + }, + ], + }, + ], + }, + }, + ], + }) + }) + + describe('patchFlag analysis', () => { + test('TEXT', () => { + const { node } = parseWithBind(`foo`) + expect(node.patchFlag).toBeUndefined() + + const { node: node2 } = parseWithBind(`{{ foo }}`) + expect(node2.patchFlag).toBe(genFlagText(PatchFlags.TEXT)) + + // multiple nodes, merged with optimize text + // const { node: node3 } = parseWithBind(`foo {{ bar }} baz`) + // expect(node3.patchFlag).toBe(genFlagText(PatchFlags.TEXT)) + }) + + test('CLASS', () => { + const { node } = parseWithBind(``) + expect(node.patchFlag).toBe(genFlagText(PatchFlags.CLASS)) + }) + + test('STYLE', () => { + const { node } = parseWithBind(``) + expect(node.patchFlag).toBe(genFlagText(PatchFlags.STYLE)) + }) + + test('PROPS', () => { + const { node } = parseWithBind(``) + expect(node.patchFlag).toBe(genFlagText(PatchFlags.PROPS)) + expect(node.dynamicProps).toBe(`["foo", "baz"]`) + }) + + test('CLASS + STYLE + PROPS', () => { + const { node } = parseWithBind( + `` + ) + expect(node.patchFlag).toBe( + genFlagText([PatchFlags.CLASS, PatchFlags.STYLE, PatchFlags.PROPS]) + ) + expect(node.dynamicProps).toBe(`["foo", "baz"]`) + }) + + // should treat `class` and `style` as PROPS + test('PROPS on component', () => { + const { node } = parseWithBind( + `` + ) + expect(node.patchFlag).toBe(genFlagText(PatchFlags.PROPS)) + expect(node.dynamicProps).toBe(`["id", "class", "style"]`) + }) + + test('FULL_PROPS (v-bind)', () => { + const { node } = parseWithBind(``) + expect(node.patchFlag).toBe(genFlagText(PatchFlags.FULL_PROPS)) + }) + + test('FULL_PROPS (dynamic key)', () => { + const { node } = parseWithBind(``) + expect(node.patchFlag).toBe(genFlagText(PatchFlags.FULL_PROPS)) + }) + + test('FULL_PROPS (w/ others)', () => { + const { node } = parseWithBind( + `` + ) + expect(node.patchFlag).toBe(genFlagText(PatchFlags.FULL_PROPS)) + }) + + test('NEED_PATCH (static ref)', () => { + const { node } = parseWithBind(``) + expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH)) + }) + + test('NEED_PATCH (dynamic ref)', () => { + const { node } = parseWithBind(``) + expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH)) + }) + + test('NEED_PATCH (custom directives)', () => { + const { node } = parseWithBind(``) + expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH)) + }) + + test('NEED_PATCH (vnode hooks)', () => { + const root = baseCompile(``, { + prefixIdentifiers: true, + // cacheHandlers: true, 暂不支持 + }).ast + const node = (root as any).children[0].codegenNode + expect(node.patchFlag).toBe(genFlagText(PatchFlags.PROPS)) + // expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH)) + }) + + // test('script setup inline mode template ref (binding exists)', () => { + // const { node } = parseWithElementTransform(``, { + // inline: true, + // bindingMetadata: { + // input: BindingTypes.SETUP_REF, + // }, + // }) + // expect(node.props).toMatchObject({ + // type: NodeTypes.JS_OBJECT_EXPRESSION, + // properties: [ + // { + // type: NodeTypes.JS_PROPERTY, + // key: { + // content: 'ref_key', + // isStatic: true, + // }, + // value: { + // content: 'input', + // isStatic: true, + // }, + // }, + // { + // type: NodeTypes.JS_PROPERTY, + // key: { + // content: 'ref', + // isStatic: true, + // }, + // value: { + // content: 'input', + // isStatic: false, + // }, + // }, + // ], + // }) + // }) + + // test('script setup inline mode template ref (binding does not exist)', () => { + // const { node } = parseWithElementTransform(``, { + // inline: true, + // }) + // expect(node.props).toMatchObject({ + // type: NodeTypes.JS_OBJECT_EXPRESSION, + // properties: [ + // { + // type: NodeTypes.JS_PROPERTY, + // key: { + // content: 'ref', + // isStatic: true, + // }, + // value: { + // content: 'input', + // isStatic: true, + // }, + // }, + // ], + // }) + // }) + + // test('script setup inline mode template ref (binding does not exist but props with the same name exist)', () => { + // const { node } = parseWithElementTransform(``, { + // inline: true, + // bindingMetadata: { + // msg: BindingTypes.PROPS, + // ref: BindingTypes.SETUP_CONST, + // }, + // }) + // expect(node.props).toMatchObject({ + // type: NodeTypes.JS_OBJECT_EXPRESSION, + // properties: [ + // { + // type: NodeTypes.JS_PROPERTY, + // key: { + // content: 'ref', + // isStatic: true, + // }, + // value: { + // content: 'msg', + // isStatic: true, + // }, + // }, + // ], + // }) + // }) + + test('HYDRATE_EVENTS', () => { + // ignore click events (has dedicated fast path) + const { node } = parseWithElementTransform(``, { + directiveTransforms: { + on: transformOn, + }, + }) + // should only have props flag + expect(node.patchFlag).toBe(genFlagText(PatchFlags.PROPS)) + + const { node: node2 } = parseWithElementTransform( + ``, + { + directiveTransforms: { + on: transformOn, + }, + } + ) + expect(node2.patchFlag).toBe( + genFlagText([PatchFlags.PROPS, PatchFlags.HYDRATE_EVENTS]) + ) + }) + + // #5870 + test('HYDRATE_EVENTS on dynamic component', () => { + const { node } = parseWithElementTransform( + ``, + { + directiveTransforms: { + on: transformOn, + }, + } + ) + expect(node.patchFlag).toBe( + genFlagText([PatchFlags.PROPS, PatchFlags.HYDRATE_EVENTS]) + ) + }) + }) + + describe('dynamic component', () => { + test('static binding', () => { + const { node, root } = parseWithBind(``) + expect(root.helpers).toContain(RESOLVE_DYNAMIC_COMPONENT) + expect(node).toMatchObject({ + isBlock: true, + tag: { + callee: RESOLVE_DYNAMIC_COMPONENT, + arguments: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'foo', + isStatic: true, + }, + ], + }, + }) + }) + + test('capitalized version w/ static binding', () => { + const { node, root } = parseWithBind(``) + expect(root.helpers).toContain(RESOLVE_DYNAMIC_COMPONENT) + expect(node).toMatchObject({ + isBlock: true, + tag: { + callee: RESOLVE_DYNAMIC_COMPONENT, + arguments: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'foo', + isStatic: true, + }, + ], + }, + }) + }) + + test('dynamic binding', () => { + const { node, root } = parseWithBind(``) + expect(root.helpers).toContain(RESOLVE_DYNAMIC_COMPONENT) + expect(node).toMatchObject({ + isBlock: true, + tag: { + callee: RESOLVE_DYNAMIC_COMPONENT, + arguments: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'foo', + isStatic: false, + }, + ], + }, + }) + }) + + test('v-is', () => { + const { node, root } = parseWithBind(``) + expect(root.helpers).toContain(RESOLVE_DYNAMIC_COMPONENT) + expect(node).toMatchObject({ + tag: { + callee: RESOLVE_DYNAMIC_COMPONENT, + arguments: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `'foo'`, + isStatic: false, + }, + ], + }, + // should skip v-is runtime check + directives: undefined, + }) + }) + + // #3934 + test('normal component with is prop', () => { + const { node, root } = parseWithBind(``, { + isNativeTag: () => false, + }) + expect(root.helpers).toContain(RESOLVE_COMPONENT) + expect(root.helpers).not.toContain(RESOLVE_DYNAMIC_COMPONENT) + expect(node).toMatchObject({ + tag: '_component_custom_input', + }) + }) + }) + + test(' should be forced into blocks', () => { + const ast = parse(``) + transform(ast, { + nodeTransforms: [transformElement], + }) + expect((ast as any).children[0].children[0].codegenNode).toMatchObject({ + type: NodeTypes.VNODE_CALL, + tag: `"svg"`, + isBlock: true, + }) + }) + + test('force block for runtime custom directive w/ children', () => { + const { node } = parseWithElementTransform(`hello`) + expect(node.isBlock).toBe(true) + }) + + test('force block for inline before-update handlers w/ children', () => { + expect( + parseWithElementTransform(`hello`).node + .isBlock + ).toBe(true) + }) + + // #938 + test('element with dynamic keys should be forced into blocks', () => { + const ast = parse(``) + transform(ast, { + nodeTransforms: [transformElement], + }) + expect((ast as any).children[0].children[0].codegenNode).toMatchObject({ + type: NodeTypes.VNODE_CALL, + tag: `"view"`, + isBlock: true, + }) + }) + + test('should process node when node has been replaced', () => { + // a NodeTransform that swaps out with + const customNodeTransform: NodeTransform = (node, context) => { + if ( + node.type === NodeTypes.ELEMENT && + node.tag === 'view' && + node.props.some( + (prop) => + prop.type === NodeTypes.ATTRIBUTE && + prop.name === 'id' && + prop.value && + prop.value.content === 'foo' + ) + ) { + context.replaceNode({ + ...node, + tag: 'text', + }) + } + } + const ast = parse(``) + transform(ast, { + nodeTransforms: [transformElement, transformText, customNodeTransform], + }) + expect((ast as any).children[0].children[0].codegenNode).toMatchObject({ + type: NodeTypes.VNODE_CALL, + tag: '"text"', + isBlock: false, + }) + }) +}) diff --git a/packages/uni-app-uts/src/plugins/android/uvue/compiler/index.ts b/packages/uni-app-uts/src/plugins/android/uvue/compiler/index.ts index ad44c4b60cc0bb103f8bfb4571b43e3a53cd75e0..27bba83af673b3c52607445e70279300885d4dba 100644 --- a/packages/uni-app-uts/src/plugins/android/uvue/compiler/index.ts +++ b/packages/uni-app-uts/src/plugins/android/uvue/compiler/index.ts @@ -78,14 +78,17 @@ export function compile( ): CodegenResult { options.rootDir = options.rootDir || '' options.targetLanguage = options.targetLanguage || 'kotlin' - const ast = baseParse(template, { - comments: false, - isNativeTag(tag) { + const isNativeTag = + options?.isNativeTag || + function (tag: string) { return ( isAppUVueNativeTag(tag) || !!options.parseUTSComponent?.(tag, options.targetLanguage!) ) - }, + } + const ast = baseParse(template, { + comments: false, + isNativeTag, }) const [nodeTransforms, directiveTransforms] = getBaseTransformPreset( options.prefixIdentifiers diff --git a/packages/uni-app-uts/src/plugins/android/uvue/compiler/options.ts b/packages/uni-app-uts/src/plugins/android/uvue/compiler/options.ts index 11a96e9fd0762e4bfb7ad4394d342ba036cc0beb..0587e9765219169bedfa6c04c6c5b743e9d0f616 100644 --- a/packages/uni-app-uts/src/plugins/android/uvue/compiler/options.ts +++ b/packages/uni-app-uts/src/plugins/android/uvue/compiler/options.ts @@ -111,7 +111,13 @@ export interface TransformOptions slotted?: boolean } -export type CompilerOptions = TransformOptions & CodegenOptions +export type CompilerOptions = { + /** + * e.g. platform native elements, e.g. `` for browsers + */ + isNativeTag?: (tag: string) => boolean +} & TransformOptions & + CodegenOptions export interface CodegenResult { ast?: RootNode diff --git a/packages/uni-app-uts/src/plugins/android/uvue/compiler/transform.ts b/packages/uni-app-uts/src/plugins/android/uvue/compiler/transform.ts index ea6ed54120913bd79c5e2e99b0f2c795c8bb3ee8..a9afe6216539fbfbb6ab4b26b7ad33250582f457 100644 --- a/packages/uni-app-uts/src/plugins/android/uvue/compiler/transform.ts +++ b/packages/uni-app-uts/src/plugins/android/uvue/compiler/transform.ts @@ -30,6 +30,7 @@ import { isString, PatchFlags, PatchFlagNames, + EMPTY_OBJ, } from '@vue/shared' import { defaultOnError, defaultOnWarn } from './errors' import { TransformOptions } from './options' @@ -123,6 +124,7 @@ export function createTransformContext( directiveTransforms = {}, scopeId = null, slotted = true, + bindingMetadata = EMPTY_OBJ, isBuiltInComponent = NOOP, isCustomElement = NOOP, onError = defaultOnError, @@ -136,7 +138,7 @@ export function createTransformContext( targetLanguage, selfName: nameMatch && capitalize(camelize(nameMatch[1])), prefixIdentifiers, - bindingMetadata: {}, + bindingMetadata, nodeTransforms, directiveTransforms, elements: new Set(), diff --git a/packages/uni-app-uts/src/plugins/android/uvue/compiler/transforms/transformStyle.ts b/packages/uni-app-uts/src/plugins/android/uvue/compiler/transforms/transformStyle.ts index 9c13061304e55b610b9e975818108dab0afe8d9b..adc1f0411715d2d23bea03f2debcbf762c9f7dc7 100644 --- a/packages/uni-app-uts/src/plugins/android/uvue/compiler/transforms/transformStyle.ts +++ b/packages/uni-app-uts/src/plugins/android/uvue/compiler/transforms/transformStyle.ts @@ -1,6 +1,7 @@ -import { NodeTransform, NodeTypes } from '@vue/compiler-core' +import { NodeTypes } from '@vue/compiler-core' import { parse } from '@dcloudio/uni-nvue-styler' import { createCompilerError } from '../errors' +import { NodeTransform } from '../transform' // Verify that the template style is in compliance with specifications export const transformStyle: NodeTransform = (node, context) => { diff --git a/packages/uni-mp-lark/dist/uni.compiler.js b/packages/uni-mp-lark/dist/uni.compiler.js index bddd47934b2410e1642852d3a108b637d4d69c62..ad78abd054e18c408d2eb136698833dfdf12ec65 100644 --- a/packages/uni-mp-lark/dist/uni.compiler.js +++ b/packages/uni-mp-lark/dist/uni.compiler.js @@ -72,6 +72,7 @@ const customElements = [ 'live-preview', 'aweme-live-book', 'aweme-user-card', + 'rtc-room', ]; const projectConfigFilename = 'project.config.json'; const nodeTransforms = [ diff --git a/packages/uni-mp-toutiao/dist/uni.compiler.js b/packages/uni-mp-toutiao/dist/uni.compiler.js index da00980ef8461dd5649982202b4f2d3e107dbfb3..9cdbec0c0a4cd95bacea541a25361f7377a1c643 100644 --- a/packages/uni-mp-toutiao/dist/uni.compiler.js +++ b/packages/uni-mp-toutiao/dist/uni.compiler.js @@ -72,6 +72,7 @@ const customElements = [ 'live-preview', 'aweme-live-book', 'aweme-user-card', + 'rtc-room', ]; const projectConfigFilename = 'project.config.json'; const nodeTransforms = [ diff --git a/packages/uni-mp-toutiao/src/compiler/options.ts b/packages/uni-mp-toutiao/src/compiler/options.ts index 482e50a573c535a10078fae2c0593a6734456bec..cd47c31c66b8b637ee2f66cbf97d955d5a44c640 100644 --- a/packages/uni-mp-toutiao/src/compiler/options.ts +++ b/packages/uni-mp-toutiao/src/compiler/options.ts @@ -22,7 +22,7 @@ export const customElements = [ 'live-preview', 'aweme-live-book', 'aweme-user-card', - 'rtc-room' + 'rtc-room', ] const projectConfigFilename = 'project.config.json' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e3516b5610d4479be51113f3963ebd49ee86048..7ec6b4b101bd6b075d580f1b694eee986e2bf1a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5581,7 +5581,7 @@ packages: normalize-path: 3.0.0 readdirp: 3.6.0 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 /ci-info@1.6.0: resolution: {integrity: sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==} @@ -6865,8 +6865,8 @@ packages: /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - /fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] requiresBuild: true @@ -7780,7 +7780,7 @@ packages: micromatch: 4.0.5 walker: 1.0.8 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: false /jest-haste-map@29.5.0: @@ -7799,7 +7799,7 @@ packages: micromatch: 4.0.5 walker: 1.0.8 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /jest-jasmine2@27.5.1: @@ -9860,14 +9860,14 @@ packages: engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 /rollup@3.21.5: resolution: {integrity: sha512-a4NTKS4u9PusbUJcfF4IMxuqjFzjm6ifj76P54a7cKnvVzJaG12BLVR+hgU2YDGHzyMMQNxLAZWuALsn8q2oQg==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -10795,7 +10795,7 @@ packages: rollup: 3.20.6 terser: 5.4.0 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /vite@4.3.5(@types/node@18.16.2)(terser@5.4.0): @@ -10829,7 +10829,7 @@ packages: rollup: 3.21.5 terser: 5.4.0 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 /vlq@0.2.3: resolution: {integrity: sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==}