diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d022e11cca4..c52bbc06970 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,11 +26,40 @@ jobs: node-version: 18 cache: 'pnpm' - - run: PUPPETEER_SKIP_DOWNLOAD=1 pnpm install + - name: Skip Puppeteer download + run: echo "PUPPETEER_SKIP_DOWNLOAD=1" >> $GITHUB_ENV + + - run: pnpm install - name: Run unit tests run: pnpm run test-unit + unit-test-windows: + runs-on: windows-latest + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository + steps: + - uses: actions/checkout@v3 + + - name: Install pnpm + uses: pnpm/action-setup@v2 + + - name: Set node version to 18 + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: 'pnpm' + + - name: Skip Puppeteer download + run: echo "PUPPETEER_SKIP_DOWNLOAD=1" >> $env:GITHUB_ENV + + - run: pnpm install + + - name: Run compiler unit tests + run: pnpm run test-unit compiler + + - name: Run ssr unit tests + run: pnpm run test-unit server-renderer + e2e-test: runs-on: ubuntu-latest if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository @@ -72,7 +101,10 @@ jobs: node-version: 18 cache: 'pnpm' - - run: PUPPETEER_SKIP_DOWNLOAD=1 pnpm install + - name: Skip Puppeteer download + run: echo "PUPPETEER_SKIP_DOWNLOAD=1" >> $GITHUB_ENV + + - run: pnpm install - name: Run eslint run: pnpm run lint diff --git a/CHANGELOG.md b/CHANGELOG.md index 59681946234..f2659ed589a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3172 +1,412 @@ -# [3.3.0-alpha.9](https://github.com/vuejs/core/compare/v3.3.0-alpha.8...v3.3.0-alpha.9) (2023-04-08) - - -### Bug Fixes - -* **compiler-sfc:** accept `StringLiteral` node in `defineEmit` tuple syntax ([#8041](https://github.com/vuejs/core/issues/8041)) ([3ccbea0](https://github.com/vuejs/core/commit/3ccbea08e09217b50a410d7b49ebb138e0c4c1e7)), closes [#8040](https://github.com/vuejs/core/issues/8040) -* **compiler-sfc:** fix binding type for constants when hoistStatic is disabled ([#8029](https://github.com/vuejs/core/issues/8029)) ([f7f4624](https://github.com/vuejs/core/commit/f7f4624191bbdc09600dbb0eb048b947c3a4f761)) -* **compiler-sfc:** skip empty `defineOptions` and support TypeScript type assertions ([#8028](https://github.com/vuejs/core/issues/8028)) ([9557529](https://github.com/vuejs/core/commit/955752951e1d31b90d817bd20830fe3f89018771)) -* **compiler-ssr:** disable v-once transform in ssr vdom fallback branch ([05f94cf](https://github.com/vuejs/core/commit/05f94cf7b01dd05ed7d3170916a38b175d5df292)), closes [#7644](https://github.com/vuejs/core/issues/7644) -* **types:** improve defineProps return type with generic arguments ([91a931a](https://github.com/vuejs/core/commit/91a931ae8707b8d43f10216e1ce8e18b12158f99)) -* **types:** more public type argument order fix ([af563bf](https://github.com/vuejs/core/commit/af563bf428200367b6f5bb7944f690c85d810202)) -* **types:** retain type parameters order for public types ([bdf557f](https://github.com/vuejs/core/commit/bdf557f6f233c039fff8007b1b16aec00c4e68aa)) - - -### Features - -* **app:** app.runWithContext() ([#7451](https://github.com/vuejs/core/issues/7451)) ([869f3fb](https://github.com/vuejs/core/commit/869f3fb93e61400be4fd925e0850c2b1564749e2)) -* **sfc:** introduce `defineModel` macro and `useModel` helper ([#8018](https://github.com/vuejs/core/issues/8018)) ([14f3d74](https://github.com/vuejs/core/commit/14f3d747a34d45415b0036b274517d70a27ec0d3)) - - -### Reverts - -* Revert "chore: remove unused args passed to ssrRender" ([b117b88](https://github.com/vuejs/core/commit/b117b8844881a732a021432066230ff2215049ea)) - - - -# [3.3.0-alpha.8](https://github.com/vuejs/core/compare/v3.3.0-alpha.7...v3.3.0-alpha.8) (2023-04-04) - - -### Bug Fixes - -* **compiler-sfc:** check binding is prop before erroring ([f3145a9](https://github.com/vuejs/core/commit/f3145a915aaec11c915f1df258c5209ae4782bcc)), closes [#8017](https://github.com/vuejs/core/issues/8017) - - - -# [3.3.0-alpha.7](https://github.com/vuejs/core/compare/v3.3.0-alpha.6...v3.3.0-alpha.7) (2023-04-03) - - -### Bug Fixes - -* **compiler-dom:** handle newlines when evaluating constants during stringification ([#7995](https://github.com/vuejs/core/issues/7995)) ([5261085](https://github.com/vuejs/core/commit/52610851137b9c5f6f57d771fd604fba309b3c97)), closes [#7994](https://github.com/vuejs/core/issues/7994) -* **compiler-sfc:** use dynamic defaults merging for methods with computed keys ([482f2e3](https://github.com/vuejs/core/commit/482f2e3434a1edc47a181890354838e206d08922)), closes [#7113](https://github.com/vuejs/core/issues/7113) - - -### Features - -* **compiler-sfc:** codegen support for defineEmits() short syntax (followup of [#7992](https://github.com/vuejs/core/issues/7992)) ([ef73ea5](https://github.com/vuejs/core/commit/ef73ea53eaf853d43e70946d2d448ae8c0a83e4f)) -* **compiler-sfc:** support arbitrary expression as withDefaults argument ([fe61944](https://github.com/vuejs/core/commit/fe619443d2e99301975de120685dbae8d66c03a6)), closes [#6459](https://github.com/vuejs/core/issues/6459) -* **reactivity:** improve support of getter usage in reactivity APIs ([#7997](https://github.com/vuejs/core/issues/7997)) ([59e8284](https://github.com/vuejs/core/commit/59e828448e7f37643cd0eaea924a764e9d314448)) -* **sfc:** revert withDefaults() deprecation ([4af5d1b](https://github.com/vuejs/core/commit/4af5d1b0754035058436f9e4e5c12aedef199177)) -* **sfc:** support more ergnomic defineEmits type syntax ([#7992](https://github.com/vuejs/core/issues/7992)) ([8876dcc](https://github.com/vuejs/core/commit/8876dccf42a7f05375d97cb18c1afdfd0fc51c94)) -* **types/slots:** support slot presence / props type checks via `defineSlots` macro and `slots` option ([#7982](https://github.com/vuejs/core/issues/7982)) ([5a2f5d5](https://github.com/vuejs/core/commit/5a2f5d59cffa36a99e6f2feab6b3ba7958b7362f)) - - - -# [3.3.0-alpha.6](https://github.com/vuejs/core/compare/v3.3.0-alpha.5...v3.3.0-alpha.6) (2023-03-30) - - -### Bug Fixes - -* **compiler-core:** check if expression is constant ([#7974](https://github.com/vuejs/core/issues/7974)) ([77686cf](https://github.com/vuejs/core/commit/77686cf4765e7e345bef364c0b03739e3c2da91d)), closes [#7973](https://github.com/vuejs/core/issues/7973) -* **compiler-core:** fix codegen for literal const in non-inline mode ([6bda4b6](https://github.com/vuejs/core/commit/6bda4b66886240b28993c88b7ecd4640a199be65)) -* **compiler-sfc:** allow ` - `) - // should generate working code - assertCode(content) - // should analyze bindings - expect(bindings).toStrictEqual({ - foo: BindingTypes.PROPS, - bar: BindingTypes.LITERAL_CONST, - props: BindingTypes.SETUP_REACTIVE_CONST - }) - - // should remove defineOptions import and call - expect(content).not.toMatch('defineProps') - // should generate correct setup signature - expect(content).toMatch(`setup(__props, { expose: __expose }) {`) - // should assign user identifier to it - expect(content).toMatch(`const props = __props`) - // should include context options in default export - expect(content).toMatch(`export default { - props: { - foo: String -},`) - }) - - test('defineProps w/ external definition', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`export default { - props: propsModel,`) - }) - - // #4764 - test('defineProps w/ leading code', () => { - const { content } = compile(` - - `) - // props declaration should be inside setup, not moved along with the import - expect(content).not.toMatch(`const props = __props\nimport`) - assertCode(content) - }) - - test('defineEmits()', () => { - const { content, bindings } = compile(` - - `) - 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, { expose: __expose, emit: myEmit }) {` - ) - // should include context options in default export - expect(content).toMatch(`export default { - emits: ['foo', 'bar'],`) - }) - test('defineProps/defineEmits in multi-variable declaration', () => { const { content } = compile(` - `) - assertCode(content) - // should remove defineOptions import and call - expect(content).not.toMatch('defineOptions') - // should include context options in default export - expect(content).toMatch( - `export default /*#__PURE__*/Object.assign({ name: 'FooApp' }, ` - ) - }) - - test('empty argument', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`export default {`) - // should remove defineOptions import and call - expect(content).not.toMatch('defineOptions') - }) - - it('should emit an error with two defineProps', () => { - expect(() => - compile(` - - `) - ).toThrowError('[@vue/compiler-sfc] duplicate defineOptions() call') - }) - - it('should emit an error with props or emits property', () => { - expect(() => - compile(` - - `) - ).toThrowError( - '[@vue/compiler-sfc] defineOptions() cannot be used to declare props. Use defineProps() instead.' - ) - - expect(() => - compile(` - - `) - ).toThrowError( - '[@vue/compiler-sfc] defineOptions() cannot be used to declare emits. Use defineEmits() instead.' - ) - - expect(() => - compile(` - - `) - ).toThrowError( - '[@vue/compiler-sfc] defineOptions() cannot be used to declare expose. Use defineExpose() instead.' - ) - - expect(() => - compile(` - - `) - ).toThrowError( - '[@vue/compiler-sfc] defineOptions() cannot be used to declare slots. Use defineSlots() instead.' - ) - }) - - it('should emit an error with type generic', () => { - expect(() => - compile(` - - `) - ).toThrowError( - '[@vue/compiler-sfc] defineOptions() cannot accept type arguments' - ) - }) - - it('should emit an error with type assertion', () => { - expect(() => - compile(` - - `) - ).toThrowError( - '[@vue/compiler-sfc] defineOptions() cannot be used to declare props. Use defineProps() instead.' - ) - }) - - it('should emit an error with declaring props/emits/slots/expose', () => { - expect(() => - compile(` - - `) - ).toThrowError( - '[@vue/compiler-sfc] defineOptions() cannot be used to declare props. Use defineProps() instead' - ) - - expect(() => - compile(` - - `) - ).toThrowError( - '[@vue/compiler-sfc] defineOptions() cannot be used to declare emits. Use defineEmits() instead' - ) - - expect(() => - compile(` - - `) - ).toThrowError( - '[@vue/compiler-sfc] defineOptions() cannot be used to declare expose. Use defineExpose() instead' - ) - - expect(() => - compile(` - - `) - ).toThrowError( - '[@vue/compiler-sfc] defineOptions() cannot be used to declare slots. Use defineSlots() instead' - ) - }) - }) - - test('defineExpose()', () => { - const { content } = compile(` - - `) - assertCode(content) - // should remove defineOptions import and call - expect(content).not.toMatch('defineExpose') - // should generate correct setup signature - expect(content).toMatch(`setup(__props, { expose: __expose }) {`) - // should replace callee - expect(content).toMatch(/\b__expose\(\{ foo: 123 \}\)/) - }) - - describe('defineModel()', () => { - test('basic usage', () => { - const { content, bindings } = compile( - ` - - `, - { defineModel: true } - ) - assertCode(content) - expect(content).toMatch('props: {') - expect(content).toMatch('"modelValue": { required: true },') - expect(content).toMatch('"count": {},') - expect(content).toMatch('emits: ["update:modelValue", "update:count"],') - expect(content).toMatch( - `const modelValue = _useModel(__props, "modelValue")` - ) - expect(content).toMatch(`const c = _useModel(__props, "count")`) - expect(content).toMatch(`return { modelValue, c }`) - expect(content).not.toMatch('defineModel') - - expect(bindings).toStrictEqual({ - modelValue: BindingTypes.SETUP_REF, - count: BindingTypes.PROPS, - c: BindingTypes.SETUP_REF - }) - }) - - test('w/ defineProps and defineEmits', () => { - const { content, bindings } = compile( - ` - - `, - { defineModel: true } - ) - assertCode(content) - expect(content).toMatch(`props: _mergeModels({ foo: String }`) - expect(content).toMatch(`"modelValue": { default: 0 }`) - expect(content).toMatch(`const count = _useModel(__props, "modelValue")`) - expect(content).not.toMatch('defineModel') - expect(bindings).toStrictEqual({ - count: BindingTypes.SETUP_REF, - foo: BindingTypes.PROPS, - modelValue: BindingTypes.PROPS - }) - }) - - test('w/ array props', () => { - const { content, bindings } = compile( - ` - - `, - { defineModel: true } - ) - assertCode(content) - expect(content).toMatch(`props: _mergeModels(['foo', 'bar'], { - "count": {}, - })`) - expect(content).toMatch(`const count = _useModel(__props, "count")`) - expect(content).not.toMatch('defineModel') - expect(bindings).toStrictEqual({ - foo: BindingTypes.PROPS, - bar: BindingTypes.PROPS, - count: BindingTypes.SETUP_REF - }) - }) - - test('w/ local flag', () => { - const { content } = compile( - ``, - { defineModel: true } - ) - assertCode(content) - expect(content).toMatch( - `_useModel(__props, "modelValue", { local: true })` - ) - expect(content).toMatch(`_useModel(__props, "bar", { [key]: true })`) - expect(content).toMatch(`_useModel(__props, "baz", { ...x })`) - expect(content).toMatch(`_useModel(__props, "qux", x)`) - expect(content).toMatch(`_useModel(__props, "foo2", { local: true })`) - expect(content).toMatch(`_useModel(__props, "hoist", { local })`) - }) - }) - - test(' - - `) - assertCode(content) - }) - describe(' + + `, + { + inlineTemplate: false + } + ) + ).not.toThrowError() + }) }) describe('with TypeScript', () => { @@ -1168,837 +835,51 @@ defineExpose({ foo: 123 }) assertCode(content) }) - test('defineProps/Emit w/ runtime options', () => { - const { content } = compile(` - - `) + test('runtime Enum', () => { + const { content, bindings } = compile( + `` + ) assertCode(content) - expect(content).toMatch(`export default /*#__PURE__*/_defineComponent({ - props: { foo: String }, - emits: ['a', 'b'], - setup(__props, { expose: __expose, emit }) {`) + expect(bindings).toStrictEqual({ + Foo: BindingTypes.LITERAL_CONST + }) }) - test('defineProps w/ type', () => { - const { content, bindings } = compile(` - `) - assertCode(content) - expect(content).toMatch(`string: { type: String, required: true }`) - expect(content).toMatch(`number: { type: Number, required: true }`) - expect(content).toMatch(`boolean: { type: Boolean, required: true }`) - expect(content).toMatch(`object: { type: Object, required: true }`) - expect(content).toMatch(`objectLiteral: { type: Object, required: true }`) - expect(content).toMatch(`fn: { type: Function, required: true }`) - expect(content).toMatch(`functionRef: { type: Function, required: true }`) - expect(content).toMatch(`objectRef: { type: Object, required: true }`) - expect(content).toMatch(`dateTime: { type: Date, required: true }`) - expect(content).toMatch(`array: { type: Array, required: true }`) - expect(content).toMatch(`arrayRef: { type: Array, required: true }`) - expect(content).toMatch(`tuple: { type: Array, required: true }`) - expect(content).toMatch(`set: { type: Set, required: true }`) - expect(content).toMatch(`literal: { type: String, required: true }`) - expect(content).toMatch(`optional: { type: null, required: false }`) - expect(content).toMatch(`recordRef: { type: Object, required: true }`) - expect(content).toMatch(`interface: { type: Object, required: true }`) - expect(content).toMatch(`alias: { type: Array, required: true }`) - expect(content).toMatch(`method: { type: Function, required: true }`) - expect(content).toMatch(`symbol: { type: Symbol, required: true }`) - expect(content).toMatch( - `objectOrFn: { type: [Function, Object], required: true },` - ) - expect(content).toMatch(`extract: { type: Number, required: true }`) - expect(content).toMatch( - `exclude: { type: [Number, Boolean], required: true }` - ) - expect(content).toMatch(`uppercase: { type: String, required: true }`) - expect(content).toMatch(`params: { type: Array, required: true }`) - expect(content).toMatch(`nonNull: { type: String, required: true }`) - expect(content).toMatch( - `union: { type: [String, Number], required: true }` - ) - expect(content).toMatch(`literalUnion: { type: String, required: true }`) - expect(content).toMatch( - `literalUnionNumber: { type: Number, required: true }` - ) - expect(content).toMatch( - `literalUnionMixed: { type: [String, Number, Boolean], required: true }` - ) - expect(content).toMatch(`intersection: { type: Object, required: true }`) - expect(content).toMatch(`intersection2: { type: String, required: true }`) - expect(content).toMatch(`foo: { type: [Function, null], required: true }`) - expect(content).toMatch(`unknown: { type: null, required: true }`) - // uninon containing unknown type: skip check - expect(content).toMatch(`unknownUnion: { type: null, required: true }`) - // intersection containing unknown type: narrow to the known types - expect(content).toMatch( - `unknownIntersection: { type: Object, required: true },` - ) - expect(content).toMatch( - `unknownUnionWithBoolean: { type: Boolean, required: true, skipCheck: true },` - ) - expect(content).toMatch( - `unknownUnionWithFunction: { type: Function, required: true, skipCheck: true }` + test('runtime Enum in normal script', () => { + const { content, bindings } = compile( + ` + ` ) + assertCode(content) expect(bindings).toStrictEqual({ - string: BindingTypes.PROPS, - number: BindingTypes.PROPS, - boolean: BindingTypes.PROPS, - object: BindingTypes.PROPS, - objectLiteral: BindingTypes.PROPS, - fn: BindingTypes.PROPS, - functionRef: BindingTypes.PROPS, - objectRef: BindingTypes.PROPS, - dateTime: BindingTypes.PROPS, - array: BindingTypes.PROPS, - arrayRef: BindingTypes.PROPS, - tuple: BindingTypes.PROPS, - set: BindingTypes.PROPS, - literal: BindingTypes.PROPS, - optional: BindingTypes.PROPS, - recordRef: BindingTypes.PROPS, - interface: BindingTypes.PROPS, - alias: BindingTypes.PROPS, - method: BindingTypes.PROPS, - symbol: BindingTypes.PROPS, - objectOrFn: BindingTypes.PROPS, - extract: BindingTypes.PROPS, - exclude: BindingTypes.PROPS, - union: BindingTypes.PROPS, - literalUnion: BindingTypes.PROPS, - literalUnionNumber: BindingTypes.PROPS, - literalUnionMixed: BindingTypes.PROPS, - intersection: BindingTypes.PROPS, - intersection2: BindingTypes.PROPS, - foo: BindingTypes.PROPS, - uppercase: BindingTypes.PROPS, - params: BindingTypes.PROPS, - nonNull: BindingTypes.PROPS, - unknown: BindingTypes.PROPS, - unknownUnion: BindingTypes.PROPS, - unknownIntersection: BindingTypes.PROPS, - unknownUnionWithBoolean: BindingTypes.PROPS, - unknownUnionWithFunction: BindingTypes.PROPS + D: BindingTypes.LITERAL_CONST, + C: BindingTypes.LITERAL_CONST, + B: BindingTypes.LITERAL_CONST, + Foo: BindingTypes.LITERAL_CONST }) }) - test('defineProps w/ interface', () => { - const { content, bindings } = compile(` - - `) + test('const Enum', () => { + const { content, bindings } = compile( + ``, + { hoistStatic: true } + ) assertCode(content) - expect(content).toMatch(`x: { type: Number, required: false }`) expect(bindings).toStrictEqual({ - x: BindingTypes.PROPS + Foo: BindingTypes.LITERAL_CONST }) }) - test('defineProps w/ extends interface', () => { - const { content, bindings } = compile(` - - - `) - assertCode(content) - expect(content).toMatch(`z: { type: Number, required: true }`) - expect(content).toMatch(`y: { type: String, required: true }`) - expect(content).toMatch(`x: { type: Number, required: false }`) - expect(bindings).toStrictEqual({ - x: BindingTypes.PROPS, - y: BindingTypes.PROPS, - z: BindingTypes.PROPS - }) - }) - - test('defineProps w/ exported interface', () => { - const { content, bindings } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`x: { type: Number, required: false }`) - expect(bindings).toStrictEqual({ - x: BindingTypes.PROPS - }) - }) - - test('defineProps w/ exported interface in normal script', () => { - const { content, bindings } = compile(` - - - `) - assertCode(content) - expect(content).toMatch(`x: { type: Number, required: false }`) - expect(bindings).toStrictEqual({ - x: BindingTypes.PROPS - }) - }) - - test('defineProps w/ type alias', () => { - const { content, bindings } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`x: { type: Number, required: false }`) - expect(bindings).toStrictEqual({ - x: BindingTypes.PROPS - }) - }) - - test('defineProps w/ exported type alias', () => { - const { content, bindings } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`x: { type: Number, required: false }`) - expect(bindings).toStrictEqual({ - x: BindingTypes.PROPS - }) - }) - - test('defineProps w/ TS assertion', () => { - const { content, bindings } = compile(` - - `) - expect(content).toMatch(`props: ['foo']`) - assertCode(content) - expect(bindings).toStrictEqual({ - foo: BindingTypes.PROPS - }) - }) - - test('withDefaults (static)', () => { - const { content, bindings } = compile(` - - `) - assertCode(content) - expect(content).toMatch( - `foo: { type: String, required: false, default: 'hi' }` - ) - expect(content).toMatch(`bar: { type: Number, required: false }`) - expect(content).toMatch(`baz: { type: Boolean, required: true }`) - expect(content).toMatch( - `qux: { type: Function, required: false, default() { return 1 } }` - ) - expect(content).toMatch( - `quux: { type: Function, required: false, default() { } }` - ) - expect(content).toMatch( - `quuxx: { type: Promise, required: false, async default() { return await Promise.resolve('hi') } }` - ) - expect(content).toMatch( - `fred: { type: String, required: false, get default() { return 'fred' } }` - ) - expect(content).toMatch( - `{ foo: string, bar?: number, baz: boolean, qux(): number, quux(): void, quuxx: Promise, fred: string }` - ) - expect(content).toMatch(`const props = __props`) - expect(bindings).toStrictEqual({ - foo: BindingTypes.PROPS, - bar: BindingTypes.PROPS, - baz: BindingTypes.PROPS, - qux: BindingTypes.PROPS, - quux: BindingTypes.PROPS, - quuxx: BindingTypes.PROPS, - fred: BindingTypes.PROPS, - props: BindingTypes.SETUP_CONST - }) - }) - - test('withDefaults (static) + normal script', () => { - const { content } = compile(` - - - `) - assertCode(content) - }) - - // #7111 - test('withDefaults (static) w/ production mode', () => { - const { content } = compile( - ` - - `, - { isProd: true } - ) - assertCode(content) - expect(content).toMatch(`const props = __props`) - - // foo has no default value, the Function can be dropped - expect(content).toMatch(`foo: {}`) - expect(content).toMatch(`bar: { type: Boolean }`) - expect(content).toMatch( - `baz: { type: [Boolean, Function], default: true }` - ) - expect(content).toMatch(`qux: { default: 'hi' }`) - }) - - test('withDefaults (dynamic)', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`import { mergeDefaults as _mergeDefaults`) - expect(content).toMatch( - ` - _mergeDefaults({ - foo: { type: String, required: false }, - bar: { type: Number, required: false }, - baz: { type: Boolean, required: true } - }, { ...defaults })`.trim() - ) - }) - - test('withDefaults (reference)', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`import { mergeDefaults as _mergeDefaults`) - expect(content).toMatch( - ` - _mergeDefaults({ - foo: { type: String, required: false }, - bar: { type: Number, required: false }, - baz: { type: Boolean, required: true } - }, defaults)`.trim() - ) - }) - - // #7111 - test('withDefaults (dynamic) w/ production mode', () => { - const { content } = compile( - ` - - `, - { isProd: true } - ) - assertCode(content) - expect(content).toMatch(`import { mergeDefaults as _mergeDefaults`) - expect(content).toMatch( - ` - _mergeDefaults({ - foo: { type: Function }, - bar: { type: Boolean }, - baz: { type: [Boolean, Function] }, - qux: {} - }, { ...defaults })`.trim() - ) - }) - - test('withDefaults w/ dynamic object method', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`import { mergeDefaults as _mergeDefaults`) - expect(content).toMatch( - ` - _mergeDefaults({ - foo: { type: Function, required: false } - }, { - ['fo' + 'o']() { return 'foo' } - })`.trim() - ) - }) - - test('defineEmits w/ type', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`emits: ["foo", "bar"]`) - }) - - test('defineEmits w/ type (union)', () => { - const type = `((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)` - expect(() => - compile(` - - `) - ).toThrow() - }) - - test('defineEmits 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('defineEmits w/ type (interface)', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`emits: ["foo", "bar"]`) - }) - - test('defineEmits w/ type (exported interface)', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`emits: ["foo", "bar"]`) - }) - - test('defineEmits w/ type from normal script', () => { - const { content } = compile(` - - - `) - assertCode(content) - expect(content).toMatch(`emits: ["foo", "bar"]`) - }) - - test('defineEmits w/ type (type alias)', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`emits: ["foo", "bar"]`) - }) - - test('defineEmits w/ type (exported type alias)', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`emits: ["foo", "bar"]`) - }) - - test('defineEmits w/ type (referenced function type)', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`emits: ["foo", "bar"]`) - }) - - test('defineEmits w/ type (referenced exported function type)', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`emits: ["foo", "bar"]`) - }) - - // #5393 - test('defineEmits w/ type (interface ts type)', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`emits: ['foo']`) - }) - - test('defineEmits w/ type (property syntax)', () => { - const { content } = compile(` - - `) - expect(content).toMatch(`emits: ["foo", "bar"]`) - assertCode(content) - }) - - // #8040 - test('defineEmits w/ type (property syntax string literal)', () => { - const { content } = compile(` - - `) - expect(content).toMatch(`emits: ["foo:bar"]`) - assertCode(content) - }) - - describe('defineSlots()', () => { - test('basic usage', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`const slots = _useSlots()`) - expect(content).not.toMatch('defineSlots') - }) - - test('w/o return value', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).not.toMatch('defineSlots') - expect(content).not.toMatch(`_useSlots`) - }) - - test('w/o generic params', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`const slots = _useSlots()`) - expect(content).not.toMatch('defineSlots') - }) - }) - - describe('defineModel()', () => { - test('basic usage', () => { - const { content, bindings } = compile( - ` - - `, - { defineModel: true } - ) - assertCode(content) - expect(content).toMatch('"modelValue": { type: [Boolean, String] }') - expect(content).toMatch('"count": { type: Number }') - expect(content).toMatch( - '"disabled": { type: Number, ...{ required: false } }' - ) - expect(content).toMatch('"any": { type: Boolean, skipCheck: true }') - expect(content).toMatch( - 'emits: ["update:modelValue", "update:count", "update:disabled", "update:any"]' - ) - - expect(content).toMatch( - `const modelValue = _useModel(__props, "modelValue")` - ) - expect(content).toMatch(`const count = _useModel(__props, "count")`) - expect(content).toMatch( - `const disabled = _useModel(__props, "disabled")` - ) - expect(content).toMatch(`const any = _useModel(__props, "any")`) - - expect(bindings).toStrictEqual({ - modelValue: BindingTypes.SETUP_REF, - count: BindingTypes.SETUP_REF, - disabled: BindingTypes.SETUP_REF, - any: BindingTypes.SETUP_REF - }) - }) - - test('w/ production mode', () => { - const { content, bindings } = compile( - ` - - `, - { defineModel: true, isProd: true } - ) - assertCode(content) - expect(content).toMatch('"modelValue": { type: Boolean }') - expect(content).toMatch('"fn": {}') - expect(content).toMatch( - '"fnWithDefault": { type: Function, ...{ default: () => null } },' - ) - expect(content).toMatch('"str": {}') - expect(content).toMatch('"optional": { required: false }') - expect(content).toMatch( - 'emits: ["update:modelValue", "update:fn", "update:fnWithDefault", "update:str", "update:optional"]' - ) - expect(content).toMatch( - `const modelValue = _useModel(__props, "modelValue")` - ) - expect(content).toMatch(`const fn = _useModel(__props, "fn")`) - expect(content).toMatch(`const str = _useModel(__props, "str")`) - expect(bindings).toStrictEqual({ - modelValue: BindingTypes.SETUP_REF, - fn: BindingTypes.SETUP_REF, - fnWithDefault: BindingTypes.SETUP_REF, - str: BindingTypes.SETUP_REF, - optional: BindingTypes.SETUP_REF - }) - }) - }) - - test('runtime Enum', () => { - const { content, bindings } = compile( - `` - ) - assertCode(content) - expect(bindings).toStrictEqual({ - Foo: BindingTypes.LITERAL_CONST - }) - }) - - test('runtime Enum in normal script', () => { - const { content, bindings } = compile( - ` - ` - ) - assertCode(content) - expect(bindings).toStrictEqual({ - D: BindingTypes.LITERAL_CONST, - C: BindingTypes.LITERAL_CONST, - B: BindingTypes.LITERAL_CONST, - Foo: BindingTypes.LITERAL_CONST - }) - }) - - test('const Enum', () => { - const { content, bindings } = compile( - ``, - { hoistStatic: true } - ) - assertCode(content) - expect(bindings).toStrictEqual({ - Foo: BindingTypes.LITERAL_CONST - }) - }) - - test('runtime inference for Enum in defineProps', () => { - expect( - compile( - ``, - { hoistStatic: true } - ).content - ).toMatch(`foo: { type: Number`) - - expect( - compile( - ``, - { hoistStatic: true } - ).content - ).toMatch(`foo: { type: String`) - - expect( - compile( - ``, - { hoistStatic: true } - ).content - ).toMatch(`foo: { type: [String, Number]`) - - expect( - compile( - ``, - { hoistStatic: true } - ).content - ).toMatch(`foo: { type: Number`) - }) - test('import type', () => { const { content } = compile( ``) - }).toThrow(`cannot accept both type and non-type arguments`) - - expect(() => { - compile(``) - }).toThrow(`cannot accept both type and non-type arguments`) - }) - test('defineProps/Emit() referencing local var', () => { expect(() => compile(``).content ) }) - - test('mixed usage of property / call signature in defineEmits', () => { - expect(() => - compile(``) - ).toThrow( - `defineEmits() type cannot mixed call signature and property syntax.` - ) - }) }) }) @@ -2685,4 +1539,19 @@ describe('SFC genDefaultAs', () => { ) assertCode(content) }) + + test('binding type for edge cases', () => { + const { bindings } = compile( + `` + ) + expect(bindings).toStrictEqual({ + toRef: BindingTypes.SETUP_CONST, + props: BindingTypes.SETUP_REACTIVE_CONST, + foo: BindingTypes.SETUP_REF + }) + }) }) diff --git a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineEmits.spec.ts.snap b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineEmits.spec.ts.snap new file mode 100644 index 00000000000..a8bd930fbbc --- /dev/null +++ b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineEmits.spec.ts.snap @@ -0,0 +1,266 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`defineEmits > basic usage 1`] = ` +"export default { + emits: ['foo', 'bar'], + setup(__props, { expose: __expose, emit: myEmit }) { + __expose(); + + + +return { myEmit } +} + +}" +`; + +exports[`defineEmits > w/ runtime options 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' + +export default /*#__PURE__*/_defineComponent({ + emits: ['a', 'b'], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type (exported interface) 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' +export interface Emits { (e: 'foo' | 'bar'): void } + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"foo\\", \\"bar\\"], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type (exported type alias) 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' +export type Emits = { (e: 'foo' | 'bar'): void } + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"foo\\", \\"bar\\"], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type (interface ts type) 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' +interface Emits { (e: 'foo'): void } + +export default /*#__PURE__*/_defineComponent({ + emits: ['foo'], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type (interface) 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' +interface Emits { (e: 'foo' | 'bar'): void } + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"foo\\", \\"bar\\"], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type (property syntax string literal) 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"foo:bar\\"], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type (property syntax) 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"foo\\", \\"bar\\"], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type (referenced exported function type) 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' +export type Emits = (e: 'foo' | 'bar') => void + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"foo\\", \\"bar\\"], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type (referenced function type) 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' +type Emits = (e: 'foo' | 'bar') => void + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"foo\\", \\"bar\\"], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type (type alias) 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' +type Emits = { (e: 'foo' | 'bar'): void } + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"foo\\", \\"bar\\"], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type (type literal w/ call signatures) 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"foo\\", \\"bar\\", \\"baz\\"], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type (type references in union) 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' +type BaseEmit = \\"change\\" + type Emit = \\"some\\" | \\"emit\\" | BaseEmit + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"some\\", \\"emit\\", \\"change\\", \\"another\\"], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type (union) 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"foo\\", \\"bar\\", \\"baz\\"], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"foo\\", \\"bar\\"], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type from normal script 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' + + export interface Emits { (e: 'foo' | 'bar'): void } + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"foo\\", \\"bar\\"], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; diff --git a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineExpose.spec.ts.snap b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineExpose.spec.ts.snap new file mode 100644 index 00000000000..d72726460bf --- /dev/null +++ b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineExpose.spec.ts.snap @@ -0,0 +1,28 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` + `) + 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, { expose: __expose, emit: myEmit }) {` + ) + // 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 }) {`) + }) + + 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 (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(` + + `) + expect(content).toMatch(`emits: ["foo", "bar"]`) + assertCode(content) + }) + + // #8040 + test('w/ type (property syntax string literal)', () => { + const { content } = compile(` + + `) + expect(content).toMatch(`emits: ["foo:bar"]`) + assertCode(content) + }) + + // #7943 + test('w/ type (type references in union)', () => { + const { content } = compile(` + + `) + + expect(content).toMatch(`emits: ["some", "emit", "change", "another"]`) + assertCode(content) + }) + + describe('errors', () => { + test('w/ both type and non-type args', () => { + expect(() => { + compile(``) + }).toThrow(`cannot accept both type and non-type arguments`) + }) + + test('mixed usage of property / call signature', () => { + expect(() => + compile(``) + ).toThrow( + `defineEmits() type cannot mixed call signature and property syntax.` + ) + }) + }) +}) diff --git a/packages/compiler-sfc/__tests__/compileScript/defineExpose.spec.ts b/packages/compiler-sfc/__tests__/compileScript/defineExpose.spec.ts new file mode 100644 index 00000000000..8ddd28a89e6 --- /dev/null +++ b/packages/compiler-sfc/__tests__/compileScript/defineExpose.spec.ts @@ -0,0 +1,26 @@ +import { compileSFCScript as compile, assertCode } from '../utils' + +test('defineExpose()', () => { + const { content } = compile(` + +`) + assertCode(content) + // should remove defineOptions import and call + expect(content).not.toMatch('defineExpose') + // should generate correct setup signature + expect(content).toMatch(`setup(__props, { expose: __expose }) {`) + // should replace callee + expect(content).toMatch(/\b__expose\(\{ foo: 123 \}\)/) +}) + +test(' + + `) + assertCode(content) +}) diff --git a/packages/compiler-sfc/__tests__/compileScript/defineModel.spec.ts b/packages/compiler-sfc/__tests__/compileScript/defineModel.spec.ts new file mode 100644 index 00000000000..61a9adcbe0d --- /dev/null +++ b/packages/compiler-sfc/__tests__/compileScript/defineModel.spec.ts @@ -0,0 +1,179 @@ +import { BindingTypes } from '@vue/compiler-core' +import { compileSFCScript as compile, assertCode } from '../utils' + +describe('defineModel()', () => { + test('basic usage', () => { + const { content, bindings } = compile( + ` + + `, + { defineModel: true } + ) + assertCode(content) + expect(content).toMatch('props: {') + expect(content).toMatch('"modelValue": { required: true },') + expect(content).toMatch('"count": {},') + expect(content).toMatch('emits: ["update:modelValue", "update:count"],') + expect(content).toMatch( + `const modelValue = _useModel(__props, "modelValue")` + ) + expect(content).toMatch(`const c = _useModel(__props, "count")`) + expect(content).toMatch(`return { modelValue, c }`) + expect(content).not.toMatch('defineModel') + + expect(bindings).toStrictEqual({ + modelValue: BindingTypes.SETUP_REF, + count: BindingTypes.PROPS, + c: BindingTypes.SETUP_REF + }) + }) + + test('w/ defineProps and defineEmits', () => { + const { content, bindings } = compile( + ` + + `, + { defineModel: true } + ) + assertCode(content) + expect(content).toMatch(`props: _mergeModels({ foo: String }`) + expect(content).toMatch(`"modelValue": { default: 0 }`) + expect(content).toMatch(`const count = _useModel(__props, "modelValue")`) + expect(content).not.toMatch('defineModel') + expect(bindings).toStrictEqual({ + count: BindingTypes.SETUP_REF, + foo: BindingTypes.PROPS, + modelValue: BindingTypes.PROPS + }) + }) + + test('w/ array props', () => { + const { content, bindings } = compile( + ` + + `, + { defineModel: true } + ) + assertCode(content) + expect(content).toMatch(`props: _mergeModels(['foo', 'bar'], { + "count": {}, + })`) + expect(content).toMatch(`const count = _useModel(__props, "count")`) + expect(content).not.toMatch('defineModel') + expect(bindings).toStrictEqual({ + foo: BindingTypes.PROPS, + bar: BindingTypes.PROPS, + count: BindingTypes.SETUP_REF + }) + }) + + test('w/ local flag', () => { + const { content } = compile( + ``, + { defineModel: true } + ) + assertCode(content) + expect(content).toMatch(`_useModel(__props, "modelValue", { local: true })`) + expect(content).toMatch(`_useModel(__props, "bar", { [key]: true })`) + expect(content).toMatch(`_useModel(__props, "baz", { ...x })`) + expect(content).toMatch(`_useModel(__props, "qux", x)`) + expect(content).toMatch(`_useModel(__props, "foo2", { local: true })`) + expect(content).toMatch(`_useModel(__props, "hoist", { local })`) + }) + + test('w/ types, basic usage', () => { + const { content, bindings } = compile( + ` + + `, + { defineModel: true } + ) + assertCode(content) + expect(content).toMatch('"modelValue": { type: [Boolean, String] }') + expect(content).toMatch('"count": { type: Number }') + expect(content).toMatch( + '"disabled": { type: Number, ...{ required: false } }' + ) + expect(content).toMatch('"any": { type: Boolean, skipCheck: true }') + expect(content).toMatch( + 'emits: ["update:modelValue", "update:count", "update:disabled", "update:any"]' + ) + + expect(content).toMatch( + `const modelValue = _useModel(__props, "modelValue")` + ) + expect(content).toMatch(`const count = _useModel(__props, "count")`) + expect(content).toMatch(`const disabled = _useModel(__props, "disabled")`) + expect(content).toMatch(`const any = _useModel(__props, "any")`) + + expect(bindings).toStrictEqual({ + modelValue: BindingTypes.SETUP_REF, + count: BindingTypes.SETUP_REF, + disabled: BindingTypes.SETUP_REF, + any: BindingTypes.SETUP_REF + }) + }) + + test('w/ types, production mode', () => { + const { content, bindings } = compile( + ` + + `, + { defineModel: true, isProd: true } + ) + assertCode(content) + expect(content).toMatch('"modelValue": { type: Boolean }') + expect(content).toMatch('"fn": {}') + expect(content).toMatch( + '"fnWithDefault": { type: Function, ...{ default: () => null } },' + ) + expect(content).toMatch('"str": {}') + expect(content).toMatch('"optional": { required: false }') + expect(content).toMatch( + 'emits: ["update:modelValue", "update:fn", "update:fnWithDefault", "update:str", "update:optional"]' + ) + expect(content).toMatch( + `const modelValue = _useModel(__props, "modelValue")` + ) + expect(content).toMatch(`const fn = _useModel(__props, "fn")`) + expect(content).toMatch(`const str = _useModel(__props, "str")`) + expect(bindings).toStrictEqual({ + modelValue: BindingTypes.SETUP_REF, + fn: BindingTypes.SETUP_REF, + fnWithDefault: BindingTypes.SETUP_REF, + str: BindingTypes.SETUP_REF, + optional: BindingTypes.SETUP_REF + }) + }) +}) diff --git a/packages/compiler-sfc/__tests__/compileScript/defineOptions.spec.ts b/packages/compiler-sfc/__tests__/compileScript/defineOptions.spec.ts new file mode 100644 index 00000000000..e4f50be38f7 --- /dev/null +++ b/packages/compiler-sfc/__tests__/compileScript/defineOptions.spec.ts @@ -0,0 +1,149 @@ +import { compileSFCScript as compile, assertCode } from '../utils' + +describe('defineOptions()', () => { + test('basic usage', () => { + const { content } = compile(` + + `) + assertCode(content) + // should remove defineOptions import and call + expect(content).not.toMatch('defineOptions') + // should include context options in default export + expect(content).toMatch( + `export default /*#__PURE__*/Object.assign({ name: 'FooApp' }, ` + ) + }) + + test('empty argument', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`export default {`) + // should remove defineOptions import and call + expect(content).not.toMatch('defineOptions') + }) + + it('should emit an error with two defineOptions', () => { + expect(() => + compile(` + + `) + ).toThrowError('[@vue/compiler-sfc] duplicate defineOptions() call') + }) + + it('should emit an error with props or emits property', () => { + expect(() => + compile(` + + `) + ).toThrowError( + '[@vue/compiler-sfc] defineOptions() cannot be used to declare props. Use defineProps() instead.' + ) + + expect(() => + compile(` + + `) + ).toThrowError( + '[@vue/compiler-sfc] defineOptions() cannot be used to declare emits. Use defineEmits() instead.' + ) + + expect(() => + compile(` + + `) + ).toThrowError( + '[@vue/compiler-sfc] defineOptions() cannot be used to declare expose. Use defineExpose() instead.' + ) + + expect(() => + compile(` + + `) + ).toThrowError( + '[@vue/compiler-sfc] defineOptions() cannot be used to declare slots. Use defineSlots() instead.' + ) + }) + + it('should emit an error with type generic', () => { + expect(() => + compile(` + + `) + ).toThrowError( + '[@vue/compiler-sfc] defineOptions() cannot accept type arguments' + ) + }) + + it('should emit an error with type assertion', () => { + expect(() => + compile(` + + `) + ).toThrowError( + '[@vue/compiler-sfc] defineOptions() cannot be used to declare props. Use defineProps() instead.' + ) + }) + + it('should emit an error with declaring props/emits/slots/expose', () => { + expect(() => + compile(` + + `) + ).toThrowError( + '[@vue/compiler-sfc] defineOptions() cannot be used to declare props. Use defineProps() instead' + ) + + expect(() => + compile(` + + `) + ).toThrowError( + '[@vue/compiler-sfc] defineOptions() cannot be used to declare emits. Use defineEmits() instead' + ) + + expect(() => + compile(` + + `) + ).toThrowError( + '[@vue/compiler-sfc] defineOptions() cannot be used to declare expose. Use defineExpose() instead' + ) + + expect(() => + compile(` + + `) + ).toThrowError( + '[@vue/compiler-sfc] defineOptions() cannot be used to declare slots. Use defineSlots() instead' + ) + }) +}) diff --git a/packages/compiler-sfc/__tests__/compileScript/defineProps.spec.ts b/packages/compiler-sfc/__tests__/compileScript/defineProps.spec.ts new file mode 100644 index 00000000000..43f54b0aa1e --- /dev/null +++ b/packages/compiler-sfc/__tests__/compileScript/defineProps.spec.ts @@ -0,0 +1,611 @@ +import { BindingTypes } from '@vue/compiler-core' +import { compileSFCScript as compile, assertCode } from '../utils' + +describe('defineProps', () => { + test('basic usage', () => { + const { content, bindings } = compile(` + + `) + // should generate working code + assertCode(content) + // should analyze bindings + expect(bindings).toStrictEqual({ + foo: BindingTypes.PROPS, + bar: BindingTypes.LITERAL_CONST, + props: BindingTypes.SETUP_REACTIVE_CONST + }) + + // should remove defineOptions import and call + expect(content).not.toMatch('defineProps') + // should generate correct setup signature + expect(content).toMatch(`setup(__props, { expose: __expose }) {`) + // should assign user identifier to it + expect(content).toMatch(`const props = __props`) + // should include context options in default export + expect(content).toMatch(`export default { + props: { + foo: String +},`) + }) + + test('w/ external definition', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`export default { + props: propsModel,`) + }) + + // #4764 + test('w/ leading code', () => { + const { content } = compile(` + + `) + // props declaration should be inside setup, not moved along with the import + expect(content).not.toMatch(`const props = __props\nimport`) + assertCode(content) + }) + + test('defineProps w/ runtime options', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`export default /*#__PURE__*/_defineComponent({ + props: { foo: String }, + setup(__props, { expose: __expose }) {`) + }) + + test('w/ type', () => { + const { content, bindings } = compile(` + `) + assertCode(content) + expect(content).toMatch(`string: { type: String, required: true }`) + expect(content).toMatch(`number: { type: Number, required: true }`) + expect(content).toMatch(`boolean: { type: Boolean, required: true }`) + expect(content).toMatch(`object: { type: Object, required: true }`) + expect(content).toMatch(`objectLiteral: { type: Object, required: true }`) + expect(content).toMatch(`fn: { type: Function, required: true }`) + expect(content).toMatch(`functionRef: { type: Function, required: true }`) + expect(content).toMatch(`objectRef: { type: Object, required: true }`) + expect(content).toMatch(`dateTime: { type: Date, required: true }`) + expect(content).toMatch(`array: { type: Array, required: true }`) + expect(content).toMatch(`arrayRef: { type: Array, required: true }`) + expect(content).toMatch(`tuple: { type: Array, required: true }`) + expect(content).toMatch(`set: { type: Set, required: true }`) + expect(content).toMatch(`literal: { type: String, required: true }`) + expect(content).toMatch(`optional: { type: null, required: false }`) + expect(content).toMatch(`recordRef: { type: Object, required: true }`) + expect(content).toMatch(`interface: { type: Object, required: true }`) + expect(content).toMatch(`alias: { type: Array, required: true }`) + expect(content).toMatch(`method: { type: Function, required: true }`) + expect(content).toMatch(`symbol: { type: Symbol, required: true }`) + expect(content).toMatch( + `objectOrFn: { type: [Function, Object], required: true },` + ) + expect(content).toMatch(`extract: { type: Number, required: true }`) + expect(content).toMatch( + `exclude: { type: [Number, Boolean], required: true }` + ) + expect(content).toMatch(`uppercase: { type: String, required: true }`) + expect(content).toMatch(`params: { type: Array, required: true }`) + expect(content).toMatch(`nonNull: { type: String, required: true }`) + expect(content).toMatch(`union: { type: [String, Number], required: true }`) + expect(content).toMatch(`literalUnion: { type: String, required: true }`) + expect(content).toMatch( + `literalUnionNumber: { type: Number, required: true }` + ) + expect(content).toMatch( + `literalUnionMixed: { type: [String, Number, Boolean], required: true }` + ) + expect(content).toMatch(`intersection: { type: Object, required: true }`) + expect(content).toMatch(`intersection2: { type: String, required: true }`) + expect(content).toMatch(`foo: { type: [Function, null], required: true }`) + expect(content).toMatch(`unknown: { type: null, required: true }`) + // uninon containing unknown type: skip check + expect(content).toMatch(`unknownUnion: { type: null, required: true }`) + // intersection containing unknown type: narrow to the known types + expect(content).toMatch( + `unknownIntersection: { type: Object, required: true },` + ) + expect(content).toMatch( + `unknownUnionWithBoolean: { type: Boolean, required: true, skipCheck: true },` + ) + expect(content).toMatch( + `unknownUnionWithFunction: { type: Function, required: true, skipCheck: true }` + ) + expect(bindings).toStrictEqual({ + string: BindingTypes.PROPS, + number: BindingTypes.PROPS, + boolean: BindingTypes.PROPS, + object: BindingTypes.PROPS, + objectLiteral: BindingTypes.PROPS, + fn: BindingTypes.PROPS, + functionRef: BindingTypes.PROPS, + objectRef: BindingTypes.PROPS, + dateTime: BindingTypes.PROPS, + array: BindingTypes.PROPS, + arrayRef: BindingTypes.PROPS, + tuple: BindingTypes.PROPS, + set: BindingTypes.PROPS, + literal: BindingTypes.PROPS, + optional: BindingTypes.PROPS, + recordRef: BindingTypes.PROPS, + interface: BindingTypes.PROPS, + alias: BindingTypes.PROPS, + method: BindingTypes.PROPS, + symbol: BindingTypes.PROPS, + objectOrFn: BindingTypes.PROPS, + extract: BindingTypes.PROPS, + exclude: BindingTypes.PROPS, + union: BindingTypes.PROPS, + literalUnion: BindingTypes.PROPS, + literalUnionNumber: BindingTypes.PROPS, + literalUnionMixed: BindingTypes.PROPS, + intersection: BindingTypes.PROPS, + intersection2: BindingTypes.PROPS, + foo: BindingTypes.PROPS, + uppercase: BindingTypes.PROPS, + params: BindingTypes.PROPS, + nonNull: BindingTypes.PROPS, + unknown: BindingTypes.PROPS, + unknownUnion: BindingTypes.PROPS, + unknownIntersection: BindingTypes.PROPS, + unknownUnionWithBoolean: BindingTypes.PROPS, + unknownUnionWithFunction: BindingTypes.PROPS + }) + }) + + test('w/ interface', () => { + const { content, bindings } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`x: { type: Number, required: false }`) + expect(bindings).toStrictEqual({ + x: BindingTypes.PROPS + }) + }) + + test('w/ extends interface', () => { + const { content, bindings } = compile(` + + + `) + assertCode(content) + expect(content).toMatch(`z: { type: Number, required: true }`) + expect(content).toMatch(`y: { type: String, required: true }`) + expect(content).toMatch(`x: { type: Number, required: false }`) + expect(bindings).toStrictEqual({ + x: BindingTypes.PROPS, + y: BindingTypes.PROPS, + z: BindingTypes.PROPS + }) + }) + + test('w/ exported interface', () => { + const { content, bindings } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`x: { type: Number, required: false }`) + expect(bindings).toStrictEqual({ + x: BindingTypes.PROPS + }) + }) + + test('w/ exported interface in normal script', () => { + const { content, bindings } = compile(` + + + `) + assertCode(content) + expect(content).toMatch(`x: { type: Number, required: false }`) + expect(bindings).toStrictEqual({ + x: BindingTypes.PROPS + }) + }) + + test('w/ type alias', () => { + const { content, bindings } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`x: { type: Number, required: false }`) + expect(bindings).toStrictEqual({ + x: BindingTypes.PROPS + }) + }) + + test('w/ exported type alias', () => { + const { content, bindings } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`x: { type: Number, required: false }`) + expect(bindings).toStrictEqual({ + x: BindingTypes.PROPS + }) + }) + + test('w/ TS assertion', () => { + const { content, bindings } = compile(` + + `) + expect(content).toMatch(`props: ['foo']`) + assertCode(content) + expect(bindings).toStrictEqual({ + foo: BindingTypes.PROPS + }) + }) + + test('withDefaults (static)', () => { + const { content, bindings } = compile(` + + `) + assertCode(content) + expect(content).toMatch( + `foo: { type: String, required: false, default: 'hi' }` + ) + expect(content).toMatch(`bar: { type: Number, required: false }`) + expect(content).toMatch(`baz: { type: Boolean, required: true }`) + expect(content).toMatch( + `qux: { type: Function, required: false, default() { return 1 } }` + ) + expect(content).toMatch( + `quux: { type: Function, required: false, default() { } }` + ) + expect(content).toMatch( + `quuxx: { type: Promise, required: false, async default() { return await Promise.resolve('hi') } }` + ) + expect(content).toMatch( + `fred: { type: String, required: false, get default() { return 'fred' } }` + ) + expect(content).toMatch(`const props = __props`) + expect(bindings).toStrictEqual({ + foo: BindingTypes.PROPS, + bar: BindingTypes.PROPS, + baz: BindingTypes.PROPS, + qux: BindingTypes.PROPS, + quux: BindingTypes.PROPS, + quuxx: BindingTypes.PROPS, + fred: BindingTypes.PROPS, + props: BindingTypes.SETUP_CONST + }) + }) + + test('withDefaults (static) + normal script', () => { + const { content } = compile(` + + + `) + assertCode(content) + }) + + // #7111 + test('withDefaults (static) w/ production mode', () => { + const { content } = compile( + ` + + `, + { isProd: true } + ) + assertCode(content) + expect(content).toMatch(`const props = __props`) + + // foo has no default value, the Function can be dropped + expect(content).toMatch(`foo: {}`) + expect(content).toMatch(`bar: { type: Boolean }`) + expect(content).toMatch(`baz: { type: [Boolean, Function], default: true }`) + expect(content).toMatch(`qux: { default: 'hi' }`) + }) + + test('withDefaults (dynamic)', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`import { mergeDefaults as _mergeDefaults`) + expect(content).toMatch( + ` + _mergeDefaults({ + foo: { type: String, required: false }, + bar: { type: Number, required: false }, + baz: { type: Boolean, required: true } + }, { ...defaults })`.trim() + ) + }) + + test('withDefaults (reference)', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`import { mergeDefaults as _mergeDefaults`) + expect(content).toMatch( + ` + _mergeDefaults({ + foo: { type: String, required: false }, + bar: { type: Number, required: false }, + baz: { type: Boolean, required: true } + }, defaults)`.trim() + ) + }) + + // #7111 + test('withDefaults (dynamic) w/ production mode', () => { + const { content } = compile( + ` + + `, + { isProd: true } + ) + assertCode(content) + expect(content).toMatch(`import { mergeDefaults as _mergeDefaults`) + expect(content).toMatch( + ` + _mergeDefaults({ + foo: { type: Function }, + bar: { type: Boolean }, + baz: { type: [Boolean, Function] }, + qux: {} + }, { ...defaults })`.trim() + ) + }) + + test('withDefaults w/ dynamic object method', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`import { mergeDefaults as _mergeDefaults`) + expect(content).toMatch( + ` + _mergeDefaults({ + foo: { type: Function, required: false } + }, { + ['fo' + 'o']() { return 'foo' } + })`.trim() + ) + }) + + test('runtime inference for Enum', () => { + expect( + compile( + ``, + { hoistStatic: true } + ).content + ).toMatch(`foo: { type: Number`) + + expect( + compile( + ``, + { hoistStatic: true } + ).content + ).toMatch(`foo: { type: String`) + + expect( + compile( + ``, + { hoistStatic: true } + ).content + ).toMatch(`foo: { type: [String, Number]`) + + expect( + compile( + ``, + { hoistStatic: true } + ).content + ).toMatch(`foo: { type: Number`) + }) + + // #8148 + test('should not override local bindings', () => { + const { bindings } = compile(` + + `) + expect(bindings).toStrictEqual({ + bar: BindingTypes.SETUP_REF, + computed: BindingTypes.SETUP_CONST + }) + }) + + // #8289 + test('destructure without enabling reactive destructure', () => { + const { content } = compile( + `` + ) + expect(content).toMatch(`const { foo } = __props`) + assertCode(content) + }) + + describe('errors', () => { + test('w/ both type and non-type args', () => { + expect(() => { + compile(``) + }).toThrow(`cannot accept both type and non-type arguments`) + }) + }) +}) diff --git a/packages/compiler-sfc/__tests__/compileScriptPropsDestructure.spec.ts b/packages/compiler-sfc/__tests__/compileScript/definePropsDestructure.spec.ts similarity index 85% rename from packages/compiler-sfc/__tests__/compileScriptPropsDestructure.spec.ts rename to packages/compiler-sfc/__tests__/compileScript/definePropsDestructure.spec.ts index e00d7d48b97..a2941872fd2 100644 --- a/packages/compiler-sfc/__tests__/compileScriptPropsDestructure.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/definePropsDestructure.spec.ts @@ -1,11 +1,12 @@ import { BindingTypes } from '@vue/compiler-core' -import { SFCScriptCompileOptions } from '../src' -import { compileSFCScript, assertCode } from './utils' +import { SFCScriptCompileOptions } from '../../src' +import { compileSFCScript, assertCode } from '../utils' -describe('sfc props transform', () => { +describe('sfc reactive props destructure', () => { function compile(src: string, options?: Partial) { return compileSFCScript(src, { inlineTemplate: true, + propsDestructure: true, ...options }) } @@ -105,6 +106,28 @@ describe('sfc props transform', () => { })`) assertCode(content) }) + test('default values w/ runtime declaration & key is string', () => { + const { content, bindings } = compile(` + + `) + expect(bindings).toStrictEqual({ + __propsAliases: { + fooBar: 'foo:bar' + }, + foo: BindingTypes.PROPS, + 'foo:bar': BindingTypes.PROPS, + fooBar: BindingTypes.PROPS_ALIASED + }) + + expect(content).toMatch(` + props: _mergeDefaults(['foo', 'foo:bar'], { + foo: 1, + "foo:bar": 'foo-bar' +}),`) + assertCode(content) + }) test('default values w/ type declaration', () => { const { content } = compile(` @@ -122,6 +145,37 @@ describe('sfc props transform', () => { assertCode(content) }) + test('default values w/ type declaration & key is string', () => { + const { content, bindings } = compile(` + + `) + expect(bindings).toStrictEqual({ + __propsAliases: { + fooBar: 'foo:bar' + }, + foo: BindingTypes.PROPS, + bar: BindingTypes.PROPS, + 'foo:bar': BindingTypes.PROPS, + fooBar: BindingTypes.PROPS_ALIASED, + 'onUpdate:modelValue': BindingTypes.PROPS + }) + expect(content).toMatch(` + props: { + foo: { type: Number, required: true, default: 1 }, + bar: { type: Number, required: true, default: 2 }, + "foo:bar": { type: String, required: true, default: 'foo-bar' }, + "onUpdate:modelValue": { type: Function, required: true } + },`) + assertCode(content) + }) + test('default values w/ type declaration, prod mode', () => { const { content } = compile( ` diff --git a/packages/compiler-sfc/__tests__/compileScript/defineSlots.spec.ts b/packages/compiler-sfc/__tests__/compileScript/defineSlots.spec.ts new file mode 100644 index 00000000000..c7becacc02a --- /dev/null +++ b/packages/compiler-sfc/__tests__/compileScript/defineSlots.spec.ts @@ -0,0 +1,40 @@ +import { compileSFCScript as compile, assertCode } from '../utils' + +describe('defineSlots()', () => { + test('basic usage', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`const slots = _useSlots()`) + expect(content).not.toMatch('defineSlots') + }) + + test('w/o return value', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).not.toMatch('defineSlots') + expect(content).not.toMatch(`_useSlots`) + }) + + test('w/o generic params', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`const slots = _useSlots()`) + expect(content).not.toMatch('defineSlots') + }) +}) diff --git a/packages/compiler-sfc/__tests__/compileScriptHoistStatic.spec.ts b/packages/compiler-sfc/__tests__/compileScript/hoistStatic.spec.ts similarity index 89% rename from packages/compiler-sfc/__tests__/compileScriptHoistStatic.spec.ts rename to packages/compiler-sfc/__tests__/compileScript/hoistStatic.spec.ts index 4879dd5f924..d2c76c9a2cc 100644 --- a/packages/compiler-sfc/__tests__/compileScriptHoistStatic.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/hoistStatic.spec.ts @@ -1,6 +1,6 @@ import { BindingTypes } from '@vue/compiler-core' -import { SFCScriptCompileOptions } from '../src' -import { compileSFCScript, assertCode } from './utils' +import { SFCScriptCompileOptions } from '../../src' +import { compileSFCScript, assertCode } from '../utils' describe('sfc hoist static', () => { function compile(src: string, options?: Partial) { @@ -19,7 +19,6 @@ describe('sfc hoist static', () => { const nil = null const bigint = 100n const template = \`str\` - const regex = /.*/g `.trim() const { content, bindings } = compile(` + + ` + ) + expect(content).toMatch('_toDisplayString(foo)') + }) }) diff --git a/packages/compiler-sfc/__tests__/compileScriptRefTransform.spec.ts b/packages/compiler-sfc/__tests__/compileScript/reactivityTransform.spec.ts similarity index 98% rename from packages/compiler-sfc/__tests__/compileScriptRefTransform.spec.ts rename to packages/compiler-sfc/__tests__/compileScript/reactivityTransform.spec.ts index 8ae5275661e..44d51c14e75 100644 --- a/packages/compiler-sfc/__tests__/compileScriptRefTransform.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/reactivityTransform.spec.ts @@ -1,5 +1,6 @@ +// TODO remove in 3.4 import { BindingTypes } from '@vue/compiler-core' -import { compileSFCScript as compile, assertCode } from './utils' +import { compileSFCScript as compile, assertCode } from '../utils' // this file only tests integration with SFC - main test case for the ref // transform can be found in /packages/reactivity-transform/__tests__ diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts new file mode 100644 index 00000000000..85d67e01cb8 --- /dev/null +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -0,0 +1,948 @@ +import { Identifier } from '@babel/types' +import { SFCScriptCompileOptions, parse } from '../../src' +import { ScriptCompileContext } from '../../src/script/context' +import { + inferRuntimeType, + invalidateTypeCache, + recordImports, + resolveTypeElements, + registerTS +} from '../../src/script/resolveType' + +import ts from 'typescript' +registerTS(ts) + +describe('resolveType', () => { + test('type literal', () => { + const { props, calls } = resolve(`defineProps<{ + foo: number // property + bar(): void // method + 'baz': string // string literal key + (e: 'foo'): void // call signature + (e: 'bar'): void + }>()`) + expect(props).toStrictEqual({ + foo: ['Number'], + bar: ['Function'], + baz: ['String'] + }) + expect(calls?.length).toBe(2) + }) + + test('reference type', () => { + expect( + resolve(` + type Aliased = { foo: number } + defineProps() + `).props + ).toStrictEqual({ + foo: ['Number'] + }) + }) + + test('reference exported type', () => { + expect( + resolve(` + export type Aliased = { foo: number } + defineProps() + `).props + ).toStrictEqual({ + foo: ['Number'] + }) + }) + + test('reference interface', () => { + expect( + resolve(` + interface Aliased { foo: number } + defineProps() + `).props + ).toStrictEqual({ + foo: ['Number'] + }) + }) + + test('reference exported interface', () => { + expect( + resolve(` + export interface Aliased { foo: number } + defineProps() + `).props + ).toStrictEqual({ + foo: ['Number'] + }) + }) + + test('reference interface extends', () => { + expect( + resolve(` + export interface A { a(): void } + export interface B extends A { b: boolean } + interface C { c: string } + interface Aliased extends B, C { foo: number } + defineProps() + `).props + ).toStrictEqual({ + a: ['Function'], + b: ['Boolean'], + c: ['String'], + foo: ['Number'] + }) + }) + + test('reference class', () => { + expect( + resolve(` + class Foo {} + defineProps<{ foo: Foo }>() + `).props + ).toStrictEqual({ + foo: ['Object'] + }) + }) + + test('function type', () => { + expect( + resolve(` + defineProps<(e: 'foo') => void>() + `).calls?.length + ).toBe(1) + }) + + test('reference function type', () => { + expect( + resolve(` + type Fn = (e: 'foo') => void + defineProps() + `).calls?.length + ).toBe(1) + }) + + test('intersection type', () => { + expect( + resolve(` + type Foo = { foo: number } + type Bar = { bar: string } + type Baz = { bar: string | boolean } + defineProps<{ self: any } & Foo & Bar & Baz>() + `).props + ).toStrictEqual({ + self: ['Unknown'], + foo: ['Number'], + // both Bar & Baz has 'bar', but Baz['bar] is wider so it should be + // preferred + bar: ['String', 'Boolean'] + }) + }) + + // #7553 + test('union type', () => { + expect( + resolve(` + interface CommonProps { + size?: 'xl' | 'l' | 'm' | 's' | 'xs' + } + + type ConditionalProps = + | { + color: 'normal' | 'primary' | 'secondary' + appearance: 'normal' | 'outline' | 'text' + } + | { + color: number + appearance: 'outline' + note: string + } + + defineProps() + `).props + ).toStrictEqual({ + size: ['String'], + color: ['String', 'Number'], + appearance: ['String'], + note: ['String'] + }) + }) + + test('template string type', () => { + expect( + resolve(` + type T = 'foo' | 'bar' + type S = 'x' | 'y' + defineProps<{ + [\`_\${T}_\${S}_\`]: string + }>() + `).props + ).toStrictEqual({ + _foo_x_: ['String'], + _foo_y_: ['String'], + _bar_x_: ['String'], + _bar_y_: ['String'] + }) + }) + + test('mapped types w/ string manipulation', () => { + expect( + resolve(` + type T = 'foo' | 'bar' + defineProps<{ [K in T]: string | number } & { + [K in 'optional']?: boolean + } & { + [K in Capitalize]: string + } & { + [K in Uppercase>]: string + } & { + [K in \`x\${T}\`]: string + }>() + `).props + ).toStrictEqual({ + foo: ['String', 'Number'], + bar: ['String', 'Number'], + Foo: ['String'], + Bar: ['String'], + FOO: ['String'], + xfoo: ['String'], + xbar: ['String'], + optional: ['Boolean'] + }) + }) + + test('utility type: Partial', () => { + expect( + resolve(` + type T = { foo: number, bar: string } + defineProps>() + `).raw.props + ).toMatchObject({ + foo: { + optional: true + }, + bar: { + optional: true + } + }) + }) + + test('utility type: Required', () => { + expect( + resolve(` + type T = { foo?: number, bar?: string } + defineProps>() + `).raw.props + ).toMatchObject({ + foo: { + optional: false + }, + bar: { + optional: false + } + }) + }) + + test('utility type: Pick', () => { + expect( + resolve(` + type T = { foo: number, bar: string, baz: boolean } + type K = 'foo' | 'bar' + defineProps>() + `).props + ).toStrictEqual({ + foo: ['Number'], + bar: ['String'] + }) + }) + + test('utility type: Omit', () => { + expect( + resolve(` + type T = { foo: number, bar: string, baz: boolean } + type K = 'foo' | 'bar' + defineProps>() + `).props + ).toStrictEqual({ + baz: ['Boolean'] + }) + }) + + test('indexed access type (literal)', () => { + expect( + resolve(` + type T = { bar: number } + type S = { nested: { foo: T['bar'] }} + defineProps() + `).props + ).toStrictEqual({ + foo: ['Number'] + }) + }) + + test('indexed access type (advanced)', () => { + expect( + resolve(` + type K = 'foo' | 'bar' + type T = { foo: string, bar: number } + type S = { foo: { foo: T[string] }, bar: { bar: string } } + defineProps() + `).props + ).toStrictEqual({ + foo: ['String', 'Number'], + bar: ['String'] + }) + }) + + test('indexed access type (number)', () => { + expect( + resolve(` + type A = (string | number)[] + type AA = Array + type T = [1, 'foo'] + type TT = [foo: 1, bar: 'foo'] + defineProps<{ foo: A[number], bar: AA[number], tuple: T[number], namedTuple: TT[number] }>() + `).props + ).toStrictEqual({ + foo: ['String', 'Number'], + bar: ['String'], + tuple: ['Number', 'String'], + namedTuple: ['Number', 'String'] + }) + }) + + test('namespace', () => { + expect( + resolve(` + type X = string + namespace Foo { + type X = number + export namespace Bar { + export type A = { + foo: X + } + } + } + defineProps() + `).props + ).toStrictEqual({ + foo: ['Number'] + }) + }) + + test('interface merging', () => { + expect( + resolve(` + interface Foo { + a: string + } + interface Foo { + b: number + } + defineProps<{ + foo: Foo['a'], + bar: Foo['b'] + }>() + `).props + ).toStrictEqual({ + foo: ['String'], + bar: ['Number'] + }) + }) + + test('namespace merging', () => { + expect( + resolve(` + namespace Foo { + export type A = string + } + namespace Foo { + export type B = number + } + defineProps<{ + foo: Foo.A, + bar: Foo.B + }>() + `).props + ).toStrictEqual({ + foo: ['String'], + bar: ['Number'] + }) + }) + + test('namespace merging with other types', () => { + expect( + resolve(` + namespace Foo { + export type A = string + } + interface Foo { + b: number + } + defineProps<{ + foo: Foo.A, + bar: Foo['b'] + }>() + `).props + ).toStrictEqual({ + foo: ['String'], + bar: ['Number'] + }) + }) + + test('enum merging', () => { + expect( + resolve(` + enum Foo { + A = 1 + } + enum Foo { + B = 'hi' + } + defineProps<{ + foo: Foo + }>() + `).props + ).toStrictEqual({ + foo: ['Number', 'String'] + }) + }) + + test('typeof', () => { + expect( + resolve(` + declare const a: string + defineProps<{ foo: typeof a }>() + `).props + ).toStrictEqual({ + foo: ['String'] + }) + }) + + test('ExtractPropTypes (element-plus)', () => { + const { props, raw } = resolve( + ` + import { ExtractPropTypes } from 'vue' + declare const props: { + foo: StringConstructor, + bar: { + type: import('foo').EpPropFinalized, + required: true + } + } + type Props = ExtractPropTypes + defineProps() + ` + ) + expect(props).toStrictEqual({ + foo: ['String'], + bar: ['Boolean'] + }) + expect(raw.props.bar.optional).toBe(false) + }) + + test('ExtractPropTypes (antd)', () => { + const { props } = resolve( + ` + declare const props: () => { + foo: StringConstructor, + bar: { type: PropType } + } + type Props = Partial>> + defineProps() + ` + ) + expect(props).toStrictEqual({ + foo: ['String'], + bar: ['Boolean'] + }) + }) + + describe('external type imports', () => { + test('relative ts', () => { + const files = { + '/foo.ts': 'export type P = { foo: number }', + '/bar.d.ts': + 'type X = { bar: string }; export { X as Y };' + + // verify that we can parse syntax that is only valid in d.ts + 'export const baz: boolean' + } + const { props, deps } = resolve( + ` + import { P } from './foo' + import { Y as PP } from './bar' + defineProps

() + `, + files + ) + expect(props).toStrictEqual({ + foo: ['Number'], + bar: ['String'] + }) + expect(deps && [...deps]).toStrictEqual(Object.keys(files)) + }) + + // #8244 + test('utility type in external file', () => { + const files = { + '/foo.ts': 'type A = { n?: number }; export type B = Required' + } + const { props } = resolve( + ` + import { B } from './foo' + defineProps() + `, + files + ) + expect(props).toStrictEqual({ + n: ['Number'] + }) + }) + + test('relative vue', () => { + const files = { + '/foo.vue': + '', + '/bar.vue': + '' + } + const { props, deps } = resolve( + ` + import { P } from './foo.vue' + import { P as PP } from './bar.vue' + defineProps

() + `, + files + ) + expect(props).toStrictEqual({ + foo: ['Number'], + bar: ['String'] + }) + expect(deps && [...deps]).toStrictEqual(Object.keys(files)) + }) + + test('relative (chained)', () => { + const files = { + '/foo.ts': `import type { P as PP } from './nested/bar.vue' + export type P = { foo: number } & PP`, + '/nested/bar.vue': + '' + } + const { props, deps } = resolve( + ` + import { P } from './foo' + defineProps

() + `, + files + ) + expect(props).toStrictEqual({ + foo: ['Number'], + bar: ['String'] + }) + expect(deps && [...deps]).toStrictEqual(Object.keys(files)) + }) + + test('relative (chained, re-export)', () => { + const files = { + '/foo.ts': `export { P as PP } from './bar'`, + '/bar.ts': 'export type P = { bar: string }' + } + const { props, deps } = resolve( + ` + import { PP as P } from './foo' + defineProps

() + `, + files + ) + expect(props).toStrictEqual({ + bar: ['String'] + }) + expect(deps && [...deps]).toStrictEqual(Object.keys(files)) + }) + + test('relative (chained, export *)', () => { + const files = { + '/foo.ts': `export * from './bar'`, + '/bar.ts': 'export type P = { bar: string }' + } + const { props, deps } = resolve( + ` + import { P } from './foo' + defineProps

() + `, + files + ) + expect(props).toStrictEqual({ + bar: ['String'] + }) + expect(deps && [...deps]).toStrictEqual(Object.keys(files)) + }) + + test('relative (default export)', () => { + const files = { + '/foo.ts': `export default interface P { foo: string }`, + '/bar.ts': `type X = { bar: string }; export default X` + } + const { props, deps } = resolve( + ` + import P from './foo' + import X from './bar' + defineProps

() + `, + files + ) + expect(props).toStrictEqual({ + foo: ['String'], + bar: ['String'] + }) + expect(deps && [...deps]).toStrictEqual(Object.keys(files)) + }) + + test('relative (default re-export)', () => { + const files = { + '/bar.ts': `export { default } from './foo'`, + '/foo.ts': `export default interface P { foo: string }; export interface PP { bar: number }`, + '/baz.ts': `export { PP as default } from './foo'` + } + const { props, deps } = resolve( + ` + import P from './bar' + import PP from './baz' + defineProps

() + `, + files + ) + expect(props).toStrictEqual({ + foo: ['String'], + bar: ['Number'] + }) + expect(deps && [...deps]).toStrictEqual(Object.keys(files)) + }) + + test('relative (re-export /w same source type name)', () => { + const files = { + '/foo.ts': `export default interface P { foo: string }`, + '/bar.ts': `export default interface PP { bar: number }`, + '/baz.ts': `export { default as X } from './foo'; export { default as XX } from './bar'; ` + } + const { props, deps } = resolve( + `import { X, XX } from './baz' + defineProps() + `, + files + ) + expect(props).toStrictEqual({ + foo: ['String'], + bar: ['Number'] + }) + expect(deps && [...deps]).toStrictEqual(['/baz.ts', '/foo.ts', '/bar.ts']) + }) + + test('relative (dynamic import)', () => { + const files = { + '/foo.ts': `export type P = { foo: string, bar: import('./bar').N }`, + '/bar.ts': 'export type N = number' + } + const { props, deps } = resolve( + ` + defineProps() + `, + files + ) + expect(props).toStrictEqual({ + foo: ['String'], + bar: ['Number'] + }) + expect(deps && [...deps]).toStrictEqual(Object.keys(files)) + }) + + // #8339 + test('relative, .js import', () => { + const files = { + '/foo.d.ts': + 'import { PP } from "./bar.js"; export type P = { foo: PP }', + '/bar.d.ts': 'export type PP = "foo" | "bar"' + } + const { props, deps } = resolve( + ` + import { P } from './foo' + defineProps

() + `, + files + ) + expect(props).toStrictEqual({ + foo: ['String'] + }) + expect(deps && [...deps]).toStrictEqual(Object.keys(files)) + }) + + test('ts module resolve', () => { + const files = { + '/node_modules/foo/package.json': JSON.stringify({ + types: 'index.d.ts' + }), + '/node_modules/foo/index.d.ts': 'export type P = { foo: number }', + '/tsconfig.json': JSON.stringify({ + compilerOptions: { + paths: { + bar: ['./pp.ts'] + } + } + }), + '/pp.ts': 'export type PP = { bar: string }' + } + + const { props, deps } = resolve( + ` + import { P } from 'foo' + import { PP } from 'bar' + defineProps

() + `, + files + ) + + expect(props).toStrictEqual({ + foo: ['Number'], + bar: ['String'] + }) + expect(deps && [...deps]).toStrictEqual([ + '/node_modules/foo/index.d.ts', + '/pp.ts' + ]) + }) + + test('ts module resolve w/ project reference & extends', () => { + const files = { + '/tsconfig.json': JSON.stringify({ + references: [ + { + path: './tsconfig.app.json' + } + ] + }), + '/tsconfig.app.json': JSON.stringify({ + include: ['**/*.ts', '**/*.vue'], + extends: './tsconfig.web.json' + }), + '/tsconfig.web.json': JSON.stringify({ + compilerOptions: { + composite: true, + paths: { + bar: ['./user.ts'] + } + } + }), + '/user.ts': 'export type User = { bar: string }' + } + + const { props, deps } = resolve( + ` + import { User } from 'bar' + defineProps() + `, + files + ) + + expect(props).toStrictEqual({ + bar: ['String'] + }) + expect(deps && [...deps]).toStrictEqual(['/user.ts']) + }) + + test('ts module resolve w/ path aliased vue file', () => { + const files = { + '/tsconfig.json': JSON.stringify({ + compilerOptions: { + include: ['**/*.ts', '**/*.vue'], + paths: { + '@/*': ['./src/*'] + } + } + }), + '/src/Foo.vue': + '' + } + + const { props, deps } = resolve( + ` + import { P } from '@/Foo.vue' + defineProps

() + `, + files + ) + + expect(props).toStrictEqual({ + bar: ['String'] + }) + expect(deps && [...deps]).toStrictEqual(['/src/Foo.vue']) + }) + + test('global types', () => { + const files = { + // ambient + '/app.d.ts': + 'declare namespace App { interface User { name: string } }', + // module - should only respect the declare global block + '/global.d.ts': ` + declare type PP = { bar: number } + declare global { + type PP = { bar: string } + } + export {} + ` + } + + const { props, deps } = resolve(`defineProps()`, files, { + globalTypeFiles: Object.keys(files) + }) + + expect(props).toStrictEqual({ + name: ['String'], + bar: ['String'] + }) + expect(deps && [...deps]).toStrictEqual(Object.keys(files)) + }) + + test('global types with ambient references', () => { + const files = { + // with references + '/backend.d.ts': ` + declare namespace App.Data { + export type AircraftData = { + id: string + manufacturer: App.Data.Listings.ManufacturerData + } + } + declare namespace App.Data.Listings { + export type ManufacturerData = { + id: string + } + } + ` + } + + const { props } = resolve(`defineProps()`, files, { + globalTypeFiles: Object.keys(files) + }) + + expect(props).toStrictEqual({ + id: ['String'], + manufacturer: ['Object'] + }) + }) + }) + + describe('errors', () => { + test('failed type reference', () => { + expect(() => resolve(`defineProps()`)).toThrow( + `Unresolvable type reference` + ) + }) + + test('unsupported computed keys', () => { + expect(() => resolve(`defineProps<{ [Foo]: string }>()`)).toThrow( + `Unsupported computed key in type referenced by a macro` + ) + }) + + test('unsupported index type', () => { + expect(() => resolve(`defineProps()`)).toThrow( + `Unsupported type when resolving index type` + ) + }) + + test('failed import source resolve', () => { + expect(() => + resolve(`import { X } from './foo'; defineProps()`) + ).toThrow(`Failed to resolve import source "./foo"`) + }) + + test('should not error on unresolved type when inferring runtime type', () => { + expect(() => resolve(`defineProps<{ foo: T }>()`)).not.toThrow() + expect(() => resolve(`defineProps<{ foo: T['bar'] }>()`)).not.toThrow() + expect(() => + resolve(` + import type P from 'unknown' + defineProps<{ foo: P }>() + `) + ).not.toThrow() + }) + + test('error against failed extends', () => { + expect(() => + resolve(` + import type Base from 'unknown' + interface Props extends Base {} + defineProps() + `) + ).toThrow(`@vue-ignore`) + }) + + test('allow ignoring failed extends', () => { + let res: any + + expect( + () => + (res = resolve(` + import type Base from 'unknown' + interface Props extends /*@vue-ignore*/ Base { + foo: string + } + defineProps() + `)) + ).not.toThrow(`@vue-ignore`) + + expect(res.props).toStrictEqual({ + foo: ['String'] + }) + }) + }) +}) + +function resolve( + code: string, + files: Record = {}, + options?: Partial +) { + const { descriptor } = parse(``, { + filename: '/Test.vue' + }) + const ctx = new ScriptCompileContext(descriptor, { + id: 'test', + fs: { + fileExists(file) { + return !!files[file] + }, + readFile(file) { + return files[file] + } + }, + ...options + }) + + for (const file in files) { + invalidateTypeCache(file) + } + + // ctx.userImports is collected when calling compileScript(), but we are + // skipping that here, so need to manually register imports + ctx.userImports = recordImports(ctx.scriptSetupAst!.body) as any + + let target: any + for (const s of ctx.scriptSetupAst!.body) { + if ( + s.type === 'ExpressionStatement' && + s.expression.type === 'CallExpression' && + (s.expression.callee as Identifier).name === 'defineProps' + ) { + target = s.expression.typeParameters!.params[0] + } + } + const raw = resolveTypeElements(ctx, target) + const props: Record = {} + for (const key in raw.props) { + props[key] = inferRuntimeType(ctx, raw.props[key]) + } + return { + props, + calls: raw.calls, + deps: ctx.deps, + raw + } +} diff --git a/packages/compiler-sfc/__tests__/parse.spec.ts b/packages/compiler-sfc/__tests__/parse.spec.ts index 5f1db5e2499..c7a17ab1739 100644 --- a/packages/compiler-sfc/__tests__/parse.spec.ts +++ b/packages/compiler-sfc/__tests__/parse.spec.ts @@ -1,6 +1,6 @@ import { parse } from '../src' import { baseParse, baseCompile } from '@vue/compiler-core' -import { SourceMapConsumer } from 'source-map' +import { SourceMapConsumer } from 'source-map-js' describe('compiler:sfc', () => { describe('source map', () => { diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json index 8c97d3b3b9b..33a8c40d185 100644 --- a/packages/compiler-sfc/package.json +++ b/packages/compiler-sfc/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-sfc", - "version": "3.3.0-alpha.9", + "version": "3.3.4", "description": "@vue/compiler-sfc", "main": "dist/compiler-sfc.cjs.js", "module": "dist/compiler-sfc.esm-browser.js", @@ -33,15 +33,15 @@ "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-sfc#readme", "dependencies": { "@babel/parser": "^7.20.15", - "@vue/compiler-core": "3.3.0-alpha.9", - "@vue/compiler-dom": "3.3.0-alpha.9", - "@vue/compiler-ssr": "3.3.0-alpha.9", - "@vue/reactivity-transform": "3.3.0-alpha.9", - "@vue/shared": "3.3.0-alpha.9", + "@vue/compiler-core": "3.3.4", + "@vue/compiler-dom": "3.3.4", + "@vue/compiler-ssr": "3.3.4", + "@vue/reactivity-transform": "3.3.4", + "@vue/shared": "3.3.4", "estree-walker": "^2.0.2", "magic-string": "^0.30.0", "postcss": "^8.1.10", - "source-map": "^0.6.1" + "source-map-js": "^1.0.2" }, "devDependencies": { "@babel/types": "^7.21.3", @@ -51,6 +51,7 @@ "hash-sum": "^2.0.0", "lru-cache": "^5.1.1", "merge-source-map": "^1.1.0", + "minimatch": "^9.0.0", "postcss-modules": "^4.0.0", "postcss-selector-parser": "^6.0.4", "pug": "^3.0.1", diff --git a/packages/compiler-sfc/src/cache.ts b/packages/compiler-sfc/src/cache.ts index 510dfee3547..36d240810c7 100644 --- a/packages/compiler-sfc/src/cache.ts +++ b/packages/compiler-sfc/src/cache.ts @@ -1,7 +1,11 @@ import LRU from 'lru-cache' -export function createCache(size = 500) { - return __GLOBAL__ || __ESM_BROWSER__ - ? new Map() - : (new LRU(size) as any as Map) +export function createCache(size = 500): Map & { max?: number } { + if (__GLOBAL__ || __ESM_BROWSER__) { + return new Map() + } + const cache = new LRU(size) + // @ts-expect-error + cache.delete = cache.del.bind(cache) + return cache as any as Map } diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index 35e690bb00e..6f096ff3e04 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -1,78 +1,58 @@ -import MagicString from 'magic-string' import { - BindingMetadata, BindingTypes, - createRoot, - NodeTypes, - transform, - parserOptions, UNREF, - SimpleExpressionNode, isFunctionType, - walkIdentifiers, - getImportedName, - unwrapTSNode, - isCallOf + walkIdentifiers } from '@vue/compiler-dom' import { DEFAULT_FILENAME, SFCDescriptor, SFCScriptBlock } from './parse' -import { - parse as _parse, - parseExpression, - ParserOptions, - ParserPlugin -} from '@babel/parser' -import { camelize, capitalize, generateCodeFrame, makeMap } from '@vue/shared' +import { ParserPlugin } from '@babel/parser' +import { generateCodeFrame } from '@vue/shared' import { Node, Declaration, ObjectPattern, - ObjectExpression, ArrayPattern, Identifier, ExportSpecifier, - TSType, - TSTypeLiteral, - TSFunctionType, - ObjectProperty, - ArrayExpression, Statement, - CallExpression, - RestElement, - TSInterfaceBody, - TSTypeElement, - AwaitExpression, - Program, - ObjectMethod, - LVal, - Expression, - TSEnumDeclaration + CallExpression } from '@babel/types' import { walk } from 'estree-walker' -import { RawSourceMap } from 'source-map' +import { RawSourceMap } from 'source-map-js' import { - CSS_VARS_HELPER, - genCssVarsCode, - genNormalScriptCssVarsCode -} from './style/cssVars' + processNormalScript, + normalScriptDefaultVar +} from './script/normalScript' +import { CSS_VARS_HELPER, genCssVarsCode } from './style/cssVars' import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate' import { warnOnce } from './warn' -import { rewriteDefaultAST } from './rewriteDefault' -import { createCache } from './cache' import { shouldTransform, transformAST } from '@vue/reactivity-transform' -import { transformDestructuredProps } from './script/propsDestructure' - -// Special compiler macros -const DEFINE_PROPS = 'defineProps' -const DEFINE_EMITS = 'defineEmits' -const DEFINE_EXPOSE = 'defineExpose' -const WITH_DEFAULTS = 'withDefaults' -const DEFINE_OPTIONS = 'defineOptions' -const DEFINE_SLOTS = 'defineSlots' -const DEFINE_MODEL = 'defineModel' - -const isBuiltInDir = makeMap( - `once,memo,if,for,else,else-if,slot,text,html,on,bind,model,show,cloak,is` -) +import { transformDestructuredProps } from './script/definePropsDestructure' +import { ScriptCompileContext } from './script/context' +import { + processDefineProps, + genRuntimeProps, + DEFINE_PROPS, + WITH_DEFAULTS +} from './script/defineProps' +import { + processDefineEmits, + genRuntimeEmits, + DEFINE_EMITS +} from './script/defineEmits' +import { DEFINE_EXPOSE, processDefineExpose } from './script/defineExpose' +import { DEFINE_OPTIONS, processDefineOptions } from './script/defineOptions' +import { processDefineSlots } from './script/defineSlots' +import { DEFINE_MODEL, processDefineModel } from './script/defineModel' +import { + isLiteralNode, + unwrapTSNode, + isCallOf, + getImportedName +} from './script/utils' +import { analyzeScriptBindings } from './script/analyzeScriptBindings' +import { isImportUsed } from './script/importUsageCheck' +import { processAwait } from './script/topLevelAwait' export interface SFCScriptCompileOptions { /** @@ -93,13 +73,10 @@ export interface SFCScriptCompileOptions { */ babelParserPlugins?: ParserPlugin[] /** - * (Experimental) Enable syntax transform for using refs without `.value` and - * using destructured props with reactivity - * @deprecated the Reactivity Transform proposal has been dropped. This - * feature will be removed from Vue core in 3.4. If you intend to continue - * using it, disable this and switch to the [Vue Macros implementation](https://vue-macros.sxzz.moe/features/reactivity-transform.html). + * A list of files to parse for global types to be made available for type + * resolving in SFC macros. The list must be fully resolved file system paths. */ - reactivityTransform?: boolean + globalTypeFiles?: string[] /** * Compile the template and inline the resulting render function * directly inside setup(). @@ -128,8 +105,31 @@ export interface SFCScriptCompileOptions { hoistStatic?: boolean /** * (**Experimental**) Enable macro `defineModel` + * @default false */ defineModel?: boolean + /** + * (**Experimental**) Enable reactive destructure for `defineProps` + * @default false + */ + propsDestructure?: boolean + /** + * File system access methods to be used when resolving types + * imported in SFC macros. Defaults to ts.sys in Node.js, can be overwritten + * to use a virtual file system for use in browsers (e.g. in REPLs) + */ + fs?: { + fileExists(file: string): boolean + readFile(file: string): string | undefined + } + /** + * (Experimental) Enable syntax transform for using refs without `.value` and + * using destructured props with reactivity + * @deprecated the Reactivity Transform proposal has been dropped. This + * feature will be removed from Vue core in 3.4. If you intend to continue + * using it, disable this and switch to the [Vue Macros implementation](https://vue-macros.sxzz.moe/features/reactivity-transform.html). + */ + reactivityTransform?: boolean } export interface ImportBinding { @@ -141,25 +141,6 @@ export interface ImportBinding { isUsedInTemplate: boolean } -export type PropsDestructureBindings = Record< - string, // public prop key - { - local: string // local identifier, may be different - default?: Expression - } -> - -type FromNormalScript = T & { __fromNormalScript?: boolean | null } -type PropsDeclType = FromNormalScript -type EmitsDeclType = FromNormalScript< - TSFunctionType | TSTypeLiteral | TSInterfaceBody -> -interface ModelDecl { - type: TSType | undefined - options: string | undefined - identifier: string | undefined -} - /** * Compile `