From 6874a80a9620e082f216400a64ddf8eb2da9560d Mon Sep 17 00:00:00 2001 From: Marsel Shaikhin Date: Sat, 2 Nov 2024 21:39:50 +0100 Subject: [PATCH] test(#41): add `defineEmits` tests --- .../__snapshots__/compileScript.spec.ts.snap | 27 +- .../__snapshots__/defineEmits.spec.ts.snap | 353 ++++++++++++++++++ .../__tests__/compileHelloWorld.spec.ts | 3 +- .../__tests__/compileScript.spec.ts | 42 +-- .../fervid_napi/__tests__/defineEmits.spec.ts | 298 +++++++++++++++ crates/fervid_napi/__tests__/utils.ts | 43 +++ crates/fervid_transform/src/script/setup.rs | 40 +- 7 files changed, 751 insertions(+), 55 deletions(-) create mode 100644 crates/fervid_napi/__tests__/__snapshots__/defineEmits.spec.ts.snap create mode 100644 crates/fervid_napi/__tests__/defineEmits.spec.ts create mode 100644 crates/fervid_napi/__tests__/utils.ts diff --git a/crates/fervid_napi/__tests__/__snapshots__/compileScript.spec.ts.snap b/crates/fervid_napi/__tests__/__snapshots__/compileScript.spec.ts.snap index a140f47..468d759 100644 --- a/crates/fervid_napi/__tests__/__snapshots__/compileScript.spec.ts.snap +++ b/crates/fervid_napi/__tests__/__snapshots__/compileScript.spec.ts.snap @@ -7,7 +7,8 @@ export default { render (_ctx, _cache, $props, $setup, $data, $options) { return _toDisplayString(a); }, - setup () { + setup (__props, { expose: __expose }) { + __expose(); const a = 1; return { a @@ -25,7 +26,8 @@ export default { render (_ctx, _cache, $props, $setup, $data, $options) { return _toDisplayString(a); }, - setup () { + setup (__props, { expose: __expose }) { + __expose(); const a = 1; return { a @@ -42,7 +44,8 @@ export default { render (_ctx, _cache, $props, $setup, $data, $options) { return _toDisplayString(a); }, - setup () { + setup (__props, { expose: __expose }) { + __expose(); const a = 1; return { a @@ -62,7 +65,8 @@ class dd { import { x } from './x'; export default { __name: "anonymous", - setup () { + setup (__props, { expose: __expose }) { + __expose(); let a = 1; const b = 2; function c() {} @@ -110,7 +114,8 @@ import { x } from './x'; import { defineComponent as _defineComponent } from "vue"; export default _defineComponent({ __name: "anonymous", - setup () { + setup (__props, { expose: __expose }) { + __expose(); let a = 1; const b = 2; function c() {} @@ -144,7 +149,8 @@ export default _defineComponent({ exports[`SFC genDefaultAs > + `) + assertCode(content) + // expect(bindings).toStrictEqual({ + // myEmit: BindingTypes.SETUP_CONST, + // }) + + // should remove defineEmits import and call + expect(content).not.toMatch('defineEmits') + + // should generate correct setup signature + expect(content).toMatch( + `setup (__props, { emit: __emit, expose: __expose }) {`, + ) + expect(content).toMatch('const myEmit = __emit') + + // should include context options in default export + expect(content).toMatch(`export default { + emits: [ + 'foo', + 'bar' + ],`) + }) + + test('w/ runtime options', () => { + const { content } = compile(` + + `) + assertCode(content) +// expect(content).toMatch(`export default /*@__PURE__*/_defineComponent({ +// emits: ['a', 'b'], +// setup(__props, { expose: __expose, emit: __emit }) {`) + + // TODO PURE + expect(content).toMatch(`export default _defineComponent({ + emits: [ + 'a', + 'b' + ], + __name: "anonymous", + setup (__props, { emit: __emit, expose: __expose }) {`) + expect(content).toMatch('const emit = __emit') + }) + + test('w/ type', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`emits: [ + "foo", + "bar" + ]`) + }) + + test('w/ type (union)', () => { + const type = `((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)` + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`emits: [ + "foo", + "bar", + "baz" + ]`) + }) + + test('w/ type (type literal w/ call signatures)', () => { + const type = `{(e: 'foo' | 'bar'): void; (e: 'baz', id: number): void;}` + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`emits: [ + "foo", + "bar", + "baz" + ]`) + }) + + test('w/ type (interface)', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`emits: [ + "foo", + "bar" + ]`) + }) + + test('w/ type (interface w/ extends)', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`emits: [ + "bar", + "foo" + ]`) + }) + + test('w/ type (exported interface)', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`emits: [ + "foo", + "bar" + ]`) + }) + + test('w/ type from normal script', () => { + const { content } = compile(` + + + `) + assertCode(content) + expect(content).toMatch(`emits: [ + "foo", + "bar" + ]`) + }) + + test('w/ type (type alias)', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`emits: [ + "foo", + "bar" + ]`) + }) + + test('w/ type (exported type alias)', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`emits: [ + "foo", + "bar" + ]`) + }) + + test('w/ type (referenced function type)', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`emits: [ + "foo", + "bar" + ]`) + }) + + test('w/ type (referenced exported function type)', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`emits: [ + "foo", + "bar" + ]`) + }) + + // #5393 + test('w/ type (interface ts type)', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`emits: [ + 'foo' + ]`) + }) + + test('w/ type (property syntax)', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`emits: [ + "foo", + "bar" + ]`) + }) + + // #8040 + test('w/ type (property syntax string literal)', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`emits: [ + "foo:bar" + ]`) + }) + + // #7943 + test('w/ type (type references in union)', () => { + const { content } = compile(` + + `) + + assertCode(content) + expect(content).toMatch(`emits: [ + "another", + "some", + "emit", + "change" + ]`) + }) + + describe('errors', () => { + test('w/ both type and non-type args', () => { + const { errors } = compile(``) + + expect(errors.length).toBe(1) + expect(errors[0].message).toMatch('DefineEmitsTypeAndNonTypeArguments') + }) + + test('mixed usage of property / call signature', () => { + const { errors } = compile(``) + + expect(errors.length).toBe(1) + expect(errors[0].message).toMatch('DefineEmitsMixedCallAndPropertySyntax') + }) + }) +}) diff --git a/crates/fervid_napi/__tests__/utils.ts b/crates/fervid_napi/__tests__/utils.ts new file mode 100644 index 0000000..ba9abd0 --- /dev/null +++ b/crates/fervid_napi/__tests__/utils.ts @@ -0,0 +1,43 @@ +import { expect } from 'vitest' +import { parse as babelParse } from '@babel/parser' +import { Compiler, FervidCompileOptions } from '..' + +const mockId = 'xxxxxxxx' + +// https://github.com/vuejs/core/blob/272ab9fbdcb1af0535108b9f888e80d612f9171d/packages/compiler-sfc/__tests__/utils.ts#L11-L24 +export function compile(src: string, options?: Partial) { + const normalizedOptions: FervidCompileOptions = { + filename: 'anonymous.vue', + id: mockId, + ...options, + } + + const compiler = new Compiler() + const result = compiler.compileSync(src, normalizedOptions) + + if (result.errors.length) { + console.warn(result.errors[0]) + } + + return { + content: result.code, + errors: result.errors + } +} + +export function assertCode(code: string) { + // parse the generated code to make sure it is valid + try { + babelParse(code, { + sourceType: 'module', + plugins: [ + 'typescript', + ['importAttributes', { deprecatedAssertSyntax: true }], + ], + }) + } catch (e: any) { + console.log(code) + throw e + } + expect(code).toMatchSnapshot() +} diff --git a/crates/fervid_transform/src/script/setup.rs b/crates/fervid_transform/src/script/setup.rs index 39aec36..705ea5d 100644 --- a/crates/fervid_transform/src/script/setup.rs +++ b/crates/fervid_transform/src/script/setup.rs @@ -1,10 +1,10 @@ -use fervid_core::{BindingTypes, IntoIdent, SfcScriptBlock}; +use fervid_core::{BindingTypes, IntoIdent, SfcScriptBlock, TemplateGenerationMode}; use swc_core::{ common::{Span, DUMMY_SP}, ecma::ast::{ - BindingIdent, BlockStmt, Decl, ExprStmt, Function, IdentName, KeyValuePatProp, - KeyValueProp, ModuleDecl, ModuleItem, ObjectPat, ObjectPatProp, Param, Pat, Prop, PropName, - PropOrSpread, Stmt, VarDeclKind, + BindingIdent, BlockStmt, CallExpr, Callee, Decl, Expr, ExprStmt, Function, Ident, + IdentName, KeyValuePatProp, KeyValueProp, ModuleDecl, ModuleItem, ObjectPat, ObjectPatProp, + Param, Pat, Prop, PropName, PropOrSpread, Stmt, VarDeclKind, }, }; @@ -131,6 +131,38 @@ pub fn transform_and_record_script_setup( // Post-process macros, e.g. merge models to `props` and `emits` postprocess_macros(&mut ctx.bindings_helper, &mut sfc_object_helper); + // Add `__expose()` in non-inline mode when user did not call `defineExpose()` + // https://github.com/vuejs/core/blob/664d2e553d8622bbdeae6bc02836233f6113eb4e/packages/compiler-sfc/src/compileScript.ts#L966-L969 + if !sfc_object_helper.is_setup_expose_referenced + && !matches!( + ctx.bindings_helper.template_generation_mode, + TemplateGenerationMode::Inline + ) + { + sfc_object_helper.is_setup_expose_referenced = true; + + // We insert at index 0 to resemble official compiler, + // even though `push`ing would be obviously more performant (and likely correct as well) + setup_body_stmts.insert( + 0, + Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + ctxt: Default::default(), + callee: Callee::Expr(Box::new(Expr::Ident(Ident { + span: DUMMY_SP, + ctxt: Default::default(), + sym: EXPOSE_HELPER.to_owned(), + optional: false, + }))), + args: vec![], + type_args: None, + })), + }), + ); + } + // Should we check that this function was not assigned anywhere else? let setup_fn = Some(Box::new(Function { params: get_setup_fn_params(&sfc_object_helper),