diff --git a/.eslintrc.js b/.eslintrc.cjs similarity index 69% rename from .eslintrc.js rename to .eslintrc.cjs index 4c667f90216..ec05a113113 100644 --- a/.eslintrc.js +++ b/.eslintrc.cjs @@ -6,7 +6,9 @@ module.exports = { parserOptions: { sourceType: 'module' }, + plugins: ['jest'], rules: { + 'no-debugger': 'error', 'no-unused-vars': [ 'error', // we are only using this rule to check for unused arguments since TS @@ -15,22 +17,27 @@ module.exports = { ], // most of the codebase are expected to be env agnostic 'no-restricted-globals': ['error', ...DOMGlobals, ...NodeGlobals], - // since we target ES2015 for baseline support, we need to forbid object - // rest spread usage (both assign and destructure) + 'no-restricted-syntax': [ 'error', - 'ObjectExpression > SpreadElement', + // since we target ES2015 for baseline support, we need to forbid object + // rest spread usage in destructure as it compiles into a verbose helper. 'ObjectPattern > RestElement', + // tsc compiles assignment spread into Object.assign() calls, but esbuild + // still generates verbose helpers, so spread assignment is also prohiboted + 'ObjectExpression > SpreadElement', 'AwaitExpression' ] }, overrides: [ // tests, no restrictions (runs in Node / jest with jsdom) { - files: ['**/__tests__/**', 'test-dts/**'], + files: ['**/__tests__/**', 'packages/dts-test/**'], rules: { 'no-restricted-globals': 'off', - 'no-restricted-syntax': 'off' + 'no-restricted-syntax': 'off', + 'jest/no-disabled-tests': 'error', + 'jest/no-focused-tests': 'error' } }, // shared, may be used in any env @@ -64,6 +71,19 @@ module.exports = { 'no-restricted-globals': ['error', ...NodeGlobals], 'no-restricted-syntax': 'off' } + }, + // Node scripts + { + files: [ + 'scripts/**', + '*.{js,ts}', + 'packages/**/index.js', + 'packages/size-check/**' + ], + rules: { + 'no-restricted-globals': 'off', + 'no-restricted-syntax': 'off' + } } ] } diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 4a8f6fd424f..9288efdb9ac 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,2 @@ -open_collective: vuejs -patreon: evanyou github: yyx990803 +open_collective: vuejs diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000000..95e0ca79c07 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,74 @@ +name: "\U0001F41E Bug report" +description: Create a report to help us improve +body: + - type: markdown + attributes: + value: | + **Before You Start...** + + This form is only for submitting bug reports. If you have a usage question + or are unsure if this is really a bug, make sure to: + + - Read the [docs](https://vuejs.org/) + - Ask on [Discord Chat](https://chat.vuejs.org/) + - Ask on [GitHub Discussions](https://github.com/vuejs/core/discussions) + - Look for / ask questions on [Stack Overflow](https://stackoverflow.com/questions/ask?tags=vue.js) + + Also try to search for your issue - it may have already been answered or even fixed in the development branch. + However, if you find that an old, closed issue still persists in the latest version, + you should open a new issue using the form below instead of commenting on the old issue. + - type: input + id: version + attributes: + label: Vue version + validations: + required: true + - type: input + id: reproduction-link + attributes: + label: Link to minimal reproduction + description: | + The easiest way to provide a reproduction is by showing the bug in [The SFC Playground](https://play.vuejs.org/). + If it cannot be reproduced in the playground and requires a proper build setup, try [StackBlitz](https://vite.new/vue). + If neither of these are suitable, you can always provide a GitHub repository. + + The reproduction should be **minimal** - i.e. it should contain only the bare minimum amount of code needed + to show the bug. See [Bug Reproduction Guidelines](https://github.com/vuejs/core/blob/main/.github/bug-repro-guidelines.md) for more details. + + Please do not just fill in a random link. The issue will be closed if no valid reproduction is provided. + placeholder: Reproduction Link + validations: + required: true + - type: textarea + id: steps-to-reproduce + attributes: + label: Steps to reproduce + description: | + What do we need to do after opening your repro in order to make the bug happen? Clear and concise reproduction instructions are important for us to be able to triage your issue in a timely manner. Note that you can use [Markdown](https://guides.github.com/features/mastering-markdown/) to format lists and code. + placeholder: Steps to reproduce + validations: + required: true + - type: textarea + id: expected + attributes: + label: What is expected? + validations: + required: true + - type: textarea + id: actually-happening + attributes: + label: What is actually happening? + validations: + required: true + - type: textarea + id: system-info + attributes: + label: System Info + description: Output of `npx envinfo --system --npmPackages vue --binaries --browsers` + render: shell + placeholder: System, Binaries, Browsers + - type: textarea + id: additional-comments + attributes: + label: Any additional comments? + description: e.g. some background/context of how you ran into this bug. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index d331b5312a9..af3782c8422 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,11 @@ blank_issues_enabled: false contact_links: - - name: Create new issue - url: https://new-issue.vuejs.org/?repo=vuejs/core - about: Please use the following link to create a new issue. + - name: Discord Chat + url: https://chat.vuejs.org + about: Ask questions and discuss with other Vue users in real time. + - name: Questions & Discussions + url: https://github.com/vuejs/core/discussions + about: Use GitHub discussions for message-board style questions and discussions. - name: Patreon url: https://www.patreon.com/evanyou about: Love Vue.js? Please consider supporting us via Patreon. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000000..6861fc26d86 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,39 @@ +name: "\U0001F680 New feature proposal" +description: Suggest an idea for this project +labels: [":sparkles: feature request"] +body: + - type: markdown + attributes: + value: | + **Before You Start...** + + This form is only for submitting feature requests. If you have a usage question + or are unsure if this is really a bug, make sure to: + + - Read the [docs](https://vuejs.org/) + - Ask on [Discord Chat](https://chat.vuejs.org/) + - Ask on [GitHub Discussions](https://github.com/vuejs/core/discussions) + - Look for / ask questions on [Stack Overflow](https://stackoverflow.com/questions/ask?tags=vue.js) + + Also try to search for your issue - another user may have already requested something similar! + + - type: textarea + id: problem-description + attributes: + label: What problem does this feature solve? + description: | + Explain your use case, context, and rationale behind this feature request. More importantly, what is the **end user experience** you are trying to build that led to the need for this feature? + + An important design goal of Vue is keeping the API surface small and straightforward. In general, we only consider adding new features that solve a problem that cannot be easily dealt with using existing APIs (i.e. not just an alternative way of doing things that can already be done). The problem should also be common enough to justify the addition. + placeholder: Problem description + validations: + required: true + - type: textarea + id: proposed-API + attributes: + label: What does the proposed API look like? + description: | + Describe how you propose to solve the problem and provide code samples of how the API would work once implemented. Note that you can use [Markdown](https://guides.github.com/features/mastering-markdown/) to format your code blocks. + placeholder: Assumed API + validations: + required: true diff --git a/.github/bug-repro-guidelines.md b/.github/bug-repro-guidelines.md new file mode 100644 index 00000000000..90458b30741 --- /dev/null +++ b/.github/bug-repro-guidelines.md @@ -0,0 +1,29 @@ +## About Bug Reproductions + +A bug reproduction is a piece of code that can run and demonstrate how a bug can happen. + +### Text is not enough + +It's impossible to fix a bug from mere text descriptions. First, it's very difficult to precisely describe a technical problem while keeping it easy to follow; Second, the real cause may very well be something that you forgot to even mention. A reproduction is the only way that can reliably help us understand what is going on, so please provide one. + +### A repro must be runnable + +Screenshots or videos are NOT reproductions! They only show that the bug exists, but do not provide enough information on why it happens. Only runnable code provides the most complete context and allows us to properly debug the scenario. That said, in some cases videos/gifs can help explain interaction issues that are hard to describe in text. + +### A repro should be minimal + +Some users would give us a link to a real project and hope we can help them figure out what is wrong. We generally do not accept such requests because: + +You are already familiar with your codebase, but we are not. It is extremely time-consuming to hunt a bug in a big and unfamiliar codebase. + +The problematic behavior may very well be caused by your code rather than by a bug in Vue. + +A minimal reproduction means it demonstrates the bug, and the bug only. It should only contain the bare minimum amount of code that can reliably cause the bug. Try your best to get rid of anything that aren't directly related to the problem. + +### How to create a repro + +For Vue 3 core reproductions, try reproducing it in [The SFC Playground](https://play.vuejs.org/). + +If it cannot be reproduced in the playground and requires a proper build setup, try [StackBlitz](https://vite.new/vue). + +If neither of these are suitable, you can always provide a GitHub repository. diff --git a/.github/contributing.md b/.github/contributing.md index b85198c3d1b..c535aa7f4e6 100644 --- a/.github/contributing.md +++ b/.github/contributing.md @@ -17,7 +17,9 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before ## Pull Request Guidelines -- Checkout a topic branch from a base branch, e.g. `master`, and merge back against that branch. +- Checkout a topic branch from a base branch, e.g. `main`, and merge back against that branch. + +- [Make sure to tick the "Allow edits from maintainers" box](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork). This allows us to directly make minor edits / refactors and saves a lot of time. - If adding a new feature: @@ -28,19 +30,34 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before - If you are resolving a special issue, add `(fix #xxxx[,#xxxx])` (#xxxx is the issue id) in your PR title for a better release log, e.g. `update entities encoding/decoding (fix #3899)`. - Provide a detailed description of the bug in the PR. Live demo preferred. - - Add appropriate test coverage if applicable. You can check the coverage of your code addition by running `npm test -- --coverage`. + - Add appropriate test coverage if applicable. You can check the coverage of your code addition by running `nr test-coverage`. - It's OK to have multiple small commits as you work on the PR - GitHub can automatically squash them before merging. - Make sure tests pass! -- Commit messages must follow the [commit message convention](./commit-convention.md) so that changelogs can be automatically generated. Commit messages are automatically validated before commit (by invoking [Git Hooks](https://git-scm.com/docs/githooks) via [yorkie](https://github.com/yyx990803/yorkie)). +- Commit messages must follow the [commit message convention](./commit-convention.md) so that changelogs can be automatically generated. Commit messages are automatically validated before commit (by invoking [Git Hooks](https://git-scm.com/docs/githooks) via [simple-git-hooks](https://github.com/toplenboren/simple-git-hooks)). + +- No need to worry about code style as long as you have installed the dev dependencies - modified files are automatically formatted with Prettier on commit (by invoking [Git Hooks](https://git-scm.com/docs/githooks) via [simple-git-hooks](https://github.com/toplenboren/simple-git-hooks)). + +### Advanced Pull Request Tips + +- The PR should fix the intended bug **only** and not introduce unrelated changes. This includes unnecessary refactors - a PR should focus on the fix and not code style, this makes it easier to trace changes in the future. -- No need to worry about code style as long as you have installed the dev dependencies - modified files are automatically formatted with Prettier on commit (by invoking [Git Hooks](https://git-scm.com/docs/githooks) via [yorkie](https://github.com/yyx990803/yorkie)). +- Consider the performance / size impact of the changes, and whether the bug being fixes justifies the cost. If the bug being fixed is a very niche edge case, we should try to minimize the size / perf cost to make it worthwhile. + + - Is the code perf-sensitive (e.g. in "hot paths" like component updates or the vdom patch function?) + + - If the branch is dev-only, performance is less of a concern. + + - Check how much extra bundle size the change introduces. + - Make sure to put dev-only code in `__DEV__` branches so they are tree-shakable. + - Runtime code is more sensitive to size increase than compiler code. + - Make sure it doesn't accidentally cause dev-only or compiler-only code branches to be included in the runtime build. Notable case is that some functions in `@vue/shared` are compiler-only and should not be used in runtime code, e.g. `isHTMLTag` and `isSVGTag`. ## Development Setup -You will need [Node.js](https://nodejs.org) **version 10+**, and [PNPM](https://pnpm.io). +You will need [Node.js](https://nodejs.org) **version 16+**, and [PNPM](https://pnpm.io) **version 7+**. We also recommend installing [ni](https://github.com/antfu/ni) to help switching between repos using different package managers. `ni` also provides the handy `nr` command which running npm scripts easier. @@ -53,14 +70,36 @@ $ pnpm i # install the dependencies of the project A high level overview of tools used: - [TypeScript](https://www.typescriptlang.org/) as the development language -- [Rollup](https://rollupjs.org) for bundling -- [Jest](https://jestjs.io/) for unit testing +- [Vite](https://vitejs.dev/) and [ESBuild](https://esbuild.github.io/) for development bundling +- [Rollup](https://rollupjs.org) for production bundling +- [Vitest](https://vitest.dev/) for unit testing - [Prettier](https://prettier.io/) for code formatting +- [ESLint](https://eslint.org/) for static error prevention (outside of types) + +## Git Hooks + +The project uses [simple-git-hooks](https://github.com/toplenboren/simple-git-hooks) to enforce the following on each commit: + +- Type check the entire project +- Automatically format changed files using Prettier +- Verify commit message format (logic in `scripts/verifyCommit.js`) ## Scripts **The examples below will be using the `nr` command from the [ni](https://github.com/antfu/ni) package.** You can also use plain `npm run`, but you will need to pass all additional arguments after the command after an extra `--`. For example, `nr build runtime --all` is equivalent to `npm run build -- runtime --all`. +The `run-s` and `run-p` commands found in some scripts are from [npm-run-all](https://github.com/mysticatea/npm-run-all) for orchestrating multiple scripts. `run-s` means "run in sequence" while `run-p` means "run in parallel". + +- [`nr build`](#nr-build) +- [`nr build-dts`](#nr-build-dts) +- [`nr check`](#nr-check) +- [`nr dev`](#nr-dev) +- [`nr dev-sfc`](#nr-dev-sfc) +- [`nr dev-esm`](#nr-dev-esm) +- [`nr dev-compiler`](#nr-dev-compiler) +- [`nr test`](#nr-test) +- [`nr test-dts`](#nr-test-dts) + ### `nr build` The `build` script builds all public packages (packages without `private: true` in their `package.json`). @@ -75,6 +114,8 @@ nr build runtime-core nr build runtime --all ``` +Note that `nr build` uses `rollup-plugin-esbuild` for transpiling typescript and **does not perform type checking**. To run type check on the entire codebase, run `nr check`. Type checks are also automatically run on each commit. + #### Build Formats By default, each package will be built in multiple distribution formats as specified in the `buildOptions.formats` field in its `package.json`. These can be overwritten via the `-f` flag. The following formats are supported: @@ -108,13 +149,11 @@ nr build runtime-core -f esm-browser,cjs Use the `--sourcemap` or `-s` flag to build with source maps. Note this will make the build much slower. -#### Build with Type Declarations +### `nr build-dts` -The `--types` or `-t` flag will generate type declarations during the build and in addition: +This command builds the type declarations for all packages. It first generates the raw `.d.ts` files in the `temp` directory, then uses [rollup-plugin-dts](https://github.com/Swatinem/rollup-plugin-dts) to roll the types into a single `.d.ts` file for each package. -- Roll the declarations into a single `.d.ts` file for each package; -- Generate an API report in `/temp/.api.md`. This report contains potential warnings emitted by [api-extractor](https://api-extractor.com/). -- Generate an API model json in `/temp/.api.json`. This file can be used to generate a Markdown version of the exported APIs. +### `nr check` ### `nr dev` @@ -123,7 +162,7 @@ The `dev` script bundles a target package (default: `vue`) in a specified format ```bash $ nr dev -> watching: packages/vue/dist/vue.global.js +> built: packages/vue/dist/vue.global.js ``` - **Important:** output of the `dev` script is for development and debugging only. While it has the same runtime behavior, the generated code should never be published to npm. @@ -136,29 +175,44 @@ $ nr dev - The `dev` script supports the `-i` flag for inlining all deps. This is useful when debugging `esm-bundler` builds which externalizes deps by default. +### `nr dev-sfc` + +Shortcut for starting the SFC Playground in local dev mode. This provides the fastest feedback loop when debugging issues that can be reproduced in the SFC Playground. + +### `nr dev-esm` + +Builds and watches `vue/dist/vue-runtime.esm-bundler.js` with all deps inlined using esbuild. This is useful when debugging the ESM build in a reproductions that require real build setups: link `packages/vue` globally, then link it into the project being debugged. + ### `nr dev-compiler` -The `dev-compiler` script builds, watches and serves the [Template Explorer](https://github.com/vuejs/core/tree/main/packages/template-explorer) at `http://localhost:5000`. This is extremely useful when working on the compiler. +The `dev-compiler` script builds, watches and serves the [Template Explorer](https://github.com/vuejs/core/tree/main/packages/template-explorer) at `http://localhost:5000`. This is useful when working on pure compiler issues. ### `nr test` -The `test` script simply calls the `jest` binary, so all [Jest CLI Options](https://jestjs.io/docs/en/cli) can be used. Some examples: +The `test` script simply calls the `vitest` binary, so all [Vitest CLI Options](https://vitest.dev/guide/cli.html#options) can be used. Some examples: ```bash -# run all tests +# run all tests in watch mode $ nr test +# run once and exit (equivalent to `vitest run`) +$ nr test run + # run all tests under the runtime-core package $ nr test runtime-core -# run tests in a specific file -$ nr test fileName +# run tests in files matching the pattern +$ nr test -# run a specific test in a specific file -$ nr test fileName -t 'test name' +# run a specific test in specific files +$ nr test -t 'test name' ``` -The default `test` script includes the `--runInBand` jest flag to improve test stability, especially for the CSS transition related tests. When you are testing specific test specs, you can also run `npx jest` with flags directly to speed up tests (jest runs them in parallel by default). +Tests that test against source code are grouped under `nr test-unit`, while tests that test against built files that run in real browsers are grouped under `nr test-e2e`. + +### `nr test-dts` + +Runs `nr build-dts` first, then verify the type tests in `packages/dts-test` are working correctly against the actual built type declarations. ## Project Structure @@ -182,14 +236,20 @@ This repository employs a [monorepo](https://en.wikipedia.org/wiki/Monorepo) set - `compiler-ssr`: Compiler that produces render functions optimized for server-side rendering. -- `template-explorer`: A development tool for debugging compiler output. You can run `nr dev template-explorer` and open its `index.html` to get a repl of template compilation based on current source code. - - A [live version](https://vue-next-template-explorer.netlify.com) of the template explorer is also available, which can be used for providing reproductions for compiler bugs. You can also pick the deployment for a specific commit from the [deploy logs](https://app.netlify.com/sites/vue-next-template-explorer/deploys). - - `shared`: Internal utilities shared across multiple packages (especially environment-agnostic utils used by both runtime and compiler packages). - `vue`: The public facing "full build" which includes both the runtime AND the compiler. +- Private utility packages: + + - `dts-test`: Contains type-only tests against generated dts files. + + - `sfc-playground`: The playground continuously deployed at https://play.vuejs.org. To run the playground locally, use [`nr dev-sfc`](#nr-dev-sfc). + + - `template-explorer`: A development tool for debugging compiler output, continuously deployed at https://template-explorer.vuejs.org/. To run it locally, run [`nr dev-compiler`](#nr-dev-compiler). + + - `size-check`: Used for checking built bundle sizes on CI. + ### Importing Packages The packages can import each other directly using their package names. Note that when importing a package, the name listed in its `package.json` should be used. Most of the time the `@vue/` prefix is needed: @@ -201,32 +261,34 @@ import { h } from '@vue/runtime-core' This is made possible via several configurations: - For TypeScript, `compilerOptions.paths` in `tsconfig.json` -- For Jest, `moduleNameMapper` in `jest.config.js` +- Vitest and Rollup share the sae set of aliases from `scripts/aliases.js` - For plain Node.js, they are linked using [PNPM Workspaces](https://pnpm.io/workspaces). ### Package Dependencies -``` - +---------------------+ - | | - | @vue/compiler-sfc | - | | - +-----+--------+------+ - | | - v v - +---------------------+ +----------------------+ - | | | | - +------------>| @vue/compiler-dom +--->| @vue/compiler-core | - | | | | | - +----+----+ +---------------------+ +----------------------+ - | | - | vue | - | | - +----+----+ +---------------------+ +----------------------+ +-------------------+ - | | | | | | | - +------------>| @vue/runtime-dom +--->| @vue/runtime-core +--->| @vue/reactivity | - | | | | | | - +---------------------+ +----------------------+ +-------------------+ +```mermaid + flowchart LR + compiler-sfc["@vue/compiler-sfc"] + compiler-dom["@vue/compiler-dom"] + compiler-core["@vue/compiler-core"] + vue["vue"] + runtime-dom["@vue/runtime-dom"] + runtime-core["@vue/runtime-core"] + reactivity["@vue/reactivity"] + + subgraph "Runtime Packages" + runtime-dom --> runtime-core + runtime-core --> reactivity + end + + subgraph "Compiler Packages" + compiler-sfc --> compiler-core + compiler-sfc --> compiler-dom + compiler-dom --> compiler-core + end + + vue ---> compiler-dom + vue --> runtime-dom ``` There are some rules to follow when importing across package boundaries: @@ -239,7 +301,7 @@ There are some rules to follow when importing across package boundaries: ## Contributing Tests -Unit tests are collocated with the code being tested in each package, inside directories named `__tests__`. Consult the [Jest docs](https://jestjs.io/docs/en/using-matchers) and existing test cases for how to write new test specs. Here are some additional guidelines: +Unit tests are collocated with the code being tested in each package, inside directories named `__tests__`. Consult the [Vitest docs](https://vitest.dev/api/) and existing test cases for how to write new test specs. Here are some additional guidelines: - Use the minimal API needed for a test case. For example, if a test can be written without involving the reactivity system or a component, it should be written so. This limits the test's exposure to changes in unrelated parts and makes it more stable. @@ -247,13 +309,11 @@ Unit tests are collocated with the code being tested in each package, inside dir - Only use platform-specific runtimes if the test is asserting platform-specific behavior. -Test coverage is continuously deployed at https://vue-next-coverage.netlify.app/. PRs that improve test coverage are welcome, but in general the test coverage should be used as a guidance for finding API use cases that are not covered by tests. We don't recommend adding tests that only improve coverage but not actually test a meaning use case. +Test coverage is continuously deployed at https://coverage.vuejs.org. PRs that improve test coverage are welcome, but in general the test coverage should be used as a guidance for finding API use cases that are not covered by tests. We don't recommend adding tests that only improve coverage but not actually test a meaning use case. ### Testing Type Definition Correctness -This project uses [tsd](https://github.com/SamVerschueren/tsd) to test the built definition files (`*.d.ts`). - -Type tests are located in the `test-dts` directory. To run the dts tests, run `nr test-dts`. Note that the type test requires all relevant `*.d.ts` files to be built first (and the script does it for you). Once the `d.ts` files are built and up-to-date, the tests can be re-run by simply running `nr test-dts`. +Type tests are located in the `packages/dts-test` directory. To run the dts tests, run `nr test-dts`. Note that the type test requires all relevant `*.d.ts` files to be built first (and the script does it for you). Once the `d.ts` files are built and up-to-date, the tests can be re-run by running `nr test-dts-only`. ## Financial Contribution diff --git a/.github/dependabot.yml b/.github/dependabot.yml index af713d27a13..206deefc560 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -62,3 +62,9 @@ updates: - dependency-name: node-notifier versions: - 8.0.1 +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: monthly + open-pull-requests-limit: 10 + versioning-strategy: lockfile-only diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml new file mode 100644 index 00000000000..cabd601a8ef --- /dev/null +++ b/.github/workflows/canary.yml @@ -0,0 +1,31 @@ +name: canary release +on: + # Runs every Monday at 1 AM UTC (9:00 AM in Singapore) + schedule: + - cron: 0 1 * * MON + workflow_dispatch: + +jobs: + canary: + # prevents this action from running on forks + if: github.repository == 'vuejs/core' + runs-on: ubuntu-latest + environment: Release + 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 + registry-url: 'https://registry.npmjs.org' + cache: 'pnpm' + + - run: pnpm install + + - run: pnpm release --canary + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e02bfc6627..c52bbc06970 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,73 +6,131 @@ on: pull_request: branches: - main + +permissions: + contents: read # to fetch code (actions/checkout) + jobs: - test: + unit-test: runs-on: ubuntu-latest + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install pnpm - uses: pnpm/action-setup@v2.0.1 - with: - version: 6.15.1 + uses: pnpm/action-setup@v2 - - name: Set node version to 16 - uses: actions/setup-node@v2 + - name: Set node version to 18 + uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 cache: 'pnpm' + - name: Skip Puppeteer download + run: echo "PUPPETEER_SKIP_DOWNLOAD=1" >> $GITHUB_ENV + - run: pnpm install - name: Run unit tests - run: pnpm run test + run: pnpm run test-unit - test-dts: + 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 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 + + - name: Setup cache for Chromium binary + uses: actions/cache@v3 + with: + path: ~/.cache/puppeteer/chrome + key: chromium-${{ hashFiles('pnpm-lock.yaml') }} - name: Install pnpm - uses: pnpm/action-setup@v2.0.1 + uses: pnpm/action-setup@v2 + + - name: Set node version to 18 + uses: actions/setup-node@v3 with: - version: 6.15.1 + node-version: 18 + cache: 'pnpm' + + - run: pnpm install + + - name: Run e2e tests + run: pnpm run test-e2e + + lint-and-test-dts: + runs-on: ubuntu-latest + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository + steps: + - uses: actions/checkout@v3 - - name: Set node version to 16 - uses: actions/setup-node@v2 + - name: Install pnpm + uses: pnpm/action-setup@v2 + + - name: Set node version to 18 + uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 cache: 'pnpm' + - name: Skip Puppeteer download + run: echo "PUPPETEER_SKIP_DOWNLOAD=1" >> $GITHUB_ENV + - run: pnpm install + - name: Run eslint + run: pnpm run lint + + # - name: Run prettier + # run: pnpm run format-check + - name: Run type declaration tests run: pnpm run test-dts size: runs-on: ubuntu-latest + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository env: CI_JOB_NUMBER: 1 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install pnpm - uses: pnpm/action-setup@v2.0.1 - with: - version: 6.15.1 + uses: pnpm/action-setup@v2 - - name: Set node version to 16 - uses: actions/setup-node@v2 + - name: Set node version to 18 + uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 cache: 'pnpm' - - run: pnpm install + - run: PUPPETEER_SKIP_DOWNLOAD=1 pnpm install - run: pnpm run size - - # - name: Check build size - # uses: posva/size-check-action@v1.1.2 - # with: - # github_token: ${{ secrets.GITHUB_TOKEN }} - # build_script: size - # files: packages/vue/dist/vue.global.prod.js packages/runtime-dom/dist/runtime-dom.global.prod.js packages/size-check/dist/index.js diff --git a/.github/workflows/ecosystem-ci-trigger.yml b/.github/workflows/ecosystem-ci-trigger.yml new file mode 100644 index 00000000000..bd3a2749431 --- /dev/null +++ b/.github/workflows/ecosystem-ci-trigger.yml @@ -0,0 +1,85 @@ +name: ecosystem-ci trigger + +on: + issue_comment: + types: [created] + +jobs: + trigger: + runs-on: ubuntu-latest + if: github.repository == 'vuejs/core' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run') + steps: + - uses: actions/github-script@v6 + with: + script: | + const user = context.payload.sender.login + console.log(`Validate user: ${user}`) + + let isVuejsMember = false + try { + const { status } = await github.rest.orgs.checkMembershipForUser({ + org: 'vuejs', + username: user + }); + + isVuejsMember = (status === 204) + } catch (e) {} + + if (isVuejsMember) { + console.log('Allowed') + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: '+1', + }) + } else { + console.log('Not allowed') + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: '-1', + }) + throw new Error('not allowed') + } + - uses: actions/github-script@v6 + id: get-pr-data + with: + script: | + console.log(`Get PR info: ${context.repo.owner}/${context.repo.repo}#${context.issue.number}`) + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number + }) + return { + num: context.issue.number, + branchName: pr.head.ref, + repo: pr.head.repo.full_name + } + - uses: actions/github-script@v6 + id: trigger + env: + COMMENT: ${{ github.event.comment.body }} + with: + github-token: ${{ secrets.ECOSYSTEM_CI_ACCESS_TOKEN }} + result-encoding: string + script: | + const comment = process.env.COMMENT.trim() + const prData = ${{ steps.get-pr-data.outputs.result }} + + const suite = comment.replace(/^\/ecosystem-ci run/, '').trim() + + await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: 'ecosystem-ci', + workflow_id: 'ecosystem-ci-from-pr.yml', + ref: 'main', + inputs: { + prNumber: '' + prData.num, + branchName: prData.branchName, + repo: prData.repo, + suite: suite === '' ? '-' : suite + } + }) diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index d9ea7a07f72..16c6c9c5c10 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -5,8 +5,12 @@ on: name: Create Release +permissions: {} jobs: build: + permissions: + contents: write # to create release (yyx990803/release-tag) + name: Create Release runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index 1487e3b7c21..810f8852690 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ explorations TODOs.md *.log .idea +.eslintcache +dts-build/packages +*.tsbuildinfo diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000000..1521c8b7652 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +dist diff --git a/BACKERS.md b/BACKERS.md index fa66d206698..631bcb91120 100644 --- a/BACKERS.md +++ b/BACKERS.md @@ -1,6 +1,6 @@

Sponsors & Backers

-Vue.js is an MIT-licensed open source project with its ongoing development made possible entirely by the support of the awesome sponsors and backers listed in this file. If you'd like to join them, please consider [ sponsor Vue's development](https://vuejs.org/sponsor/). +Vue.js is an MIT-licensed open source project with its ongoing development made possible entirely by the support of the awesome sponsors and backers listed in this file. If you'd like to join them, please consider [ sponsoring Vue's development](https://vuejs.org/sponsor/).

diff --git a/CHANGELOG.md b/CHANGELOG.md index 111f3e200fa..f2659ed589a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2619 +1,412 @@ -## [3.2.31](https://github.com/vuejs/core/compare/v3.2.30...v3.2.31) (2022-02-12) +## [3.3.4](https://github.com/vuejs/core/compare/v3.3.3...v3.3.4) (2023-05-18) ### Bug Fixes -* **compiler-ssr:** no need to inject resolveDirective calls for setup custom directives ([436c500](https://github.com/vuejs/core/commit/436c500d2c418930652fededc4882540dcd0c987)) -* **runtime-core:** allow spying on proxy methods ([#4216](https://github.com/vuejs/core/issues/4216)) ([8457d8b](https://github.com/vuejs/core/commit/8457d8b980674b09547edb2dae28091306fe6aa8)) -* **ssr:** always hydrate children for HMR ([#5406](https://github.com/vuejs/core/issues/5406)) ([0342fae](https://github.com/vuejs/core/commit/0342fae8ad0e71866e9b9725a1f9c471db775c76)), closes [#5405](https://github.com/vuejs/core/issues/5405) +* **build:** ensure correct typing for node esm ([d621d4c](https://github.com/vuejs/core/commit/d621d4c646b2d7b190fbd44ad1fd04512b3de300)) +* **build:** fix __DEV__ flag replacement edge case ([8b7c04b](https://github.com/vuejs/core/commit/8b7c04b18f73aad9a08dd57eba90101b5b2aef28)), closes [#8353](https://github.com/vuejs/core/issues/8353) +* **compiler-sfc:** handle imported types from default exports ([5aec717](https://github.com/vuejs/core/commit/5aec717a2402652306085f58432ba3ab91848a74)), closes [#8355](https://github.com/vuejs/core/issues/8355) -## [3.2.30](https://github.com/vuejs/core/compare/v3.2.29...v3.2.30) (2022-02-07) +## [3.3.3](https://github.com/vuejs/core/compare/v3.3.2...v3.3.3) (2023-05-18) -### Features - -* **ssr:** support custom directive getSSRProps in optimized compilation ([60cf175](https://github.com/vuejs/core/commit/60cf175d88236db2c2a4a02900c92e26ceea0073)), closes [#5304](https://github.com/vuejs/core/issues/5304) - - -### Performance Improvements - -* **reactivity:** optimize effect/effectScope active state tracking ([2993a24](https://github.com/vuejs/core/commit/2993a246181df12e367b7abdfce0954244e8f7ec)) - - - -## [3.2.29](https://github.com/vuejs/vue-next/compare/v3.2.28...v3.2.29) (2022-01-23) - - -### Bug Fixes - -* **compiler-sfc:** fix css v-bind inside other css functions ([16fa18d](https://github.com/vuejs/vue-next/commit/16fa18da6dbbc52c89f9ea729816e1e70ab0d388)), closes [#5302](https://github.com/vuejs/vue-next/issues/5302) [#5306](https://github.com/vuejs/vue-next/issues/5306) -* **reactivity:** ensure readonly refs can be replaced with new refs in reactive objects ([#5310](https://github.com/vuejs/vue-next/issues/5310)) ([4be1037](https://github.com/vuejs/vue-next/commit/4be1037f31e169d667059c44364fc3e43803accb)), closes [#5307](https://github.com/vuejs/vue-next/issues/5307) -* **runtime-dom:** fix static content re-insertion ([9aa5dfd](https://github.com/vuejs/vue-next/commit/9aa5dfd4bb8efac0041e33ef5fdbebab59cc6516)), closes [#5308](https://github.com/vuejs/vue-next/issues/5308) - - - -## 3.2.28 (2022-01-21) - -* build: fix build script ([3d80b15](https://github.com/vuejs/vue-next/commit/3d80b15)) -* fix(compat): convertLegacyVModelProps should merge model option in mixins (#5251) ([72130ac](https://github.com/vuejs/vue-next/commit/72130ac)), closes [#5251](https://github.com/vuejs/vue-next/issues/5251) -* fix(compat): ensure fallthrough *Native events are not dropped during props update (#5228) ([97f6bd9](https://github.com/vuejs/vue-next/commit/97f6bd9)), closes [#5228](https://github.com/vuejs/vue-next/issues/5228) -* fix(compat): simulate Vue 2.6.14 version in compat build (#5293) ([d0b9708](https://github.com/vuejs/vue-next/commit/d0b9708)), closes [#5293](https://github.com/vuejs/vue-next/issues/5293) -* fix(compiler-core): handle v-memo in template v-for (#5291) ([9f55e6f](https://github.com/vuejs/vue-next/commit/9f55e6f)), closes [#5291](https://github.com/vuejs/vue-next/issues/5291) [#5288](https://github.com/vuejs/vue-next/issues/5288) -* fix(compiler-sfc): support complex expression in CSS v-bind() (#5114) ([95d49bf](https://github.com/vuejs/vue-next/commit/95d49bf)), closes [#5114](https://github.com/vuejs/vue-next/issues/5114) [#5109](https://github.com/vuejs/vue-next/issues/5109) -* fix(compiler-sfc/reactivity-transform): fix edge case where normal script has ref macros but script ([4768f26](https://github.com/vuejs/vue-next/commit/4768f26)) -* fix(reactivity-transform): apply transform for labelled variable declarations ([a05b000](https://github.com/vuejs/vue-next/commit/a05b000)), closes [/github.com/vuejs/core/issues/5298#issuecomment-1017970061](https://github.com//github.com/vuejs/core/issues/5298/issues/issuecomment-1017970061) -* fix(reactivity-transform): apply transform on exported variable declarations ([a81a992](https://github.com/vuejs/vue-next/commit/a81a992)), closes [#5298](https://github.com/vuejs/vue-next/issues/5298) -* fix(reactivity): differentiate shallow/deep proxies of same target when nested in reactive ([9c304bf](https://github.com/vuejs/vue-next/commit/9c304bf)), closes [#5271](https://github.com/vuejs/vue-next/issues/5271) -* fix(reactivity): mutating a readonly ref nested in a reactive object should fail. (#5048) ([171f5e9](https://github.com/vuejs/vue-next/commit/171f5e9)), closes [#5048](https://github.com/vuejs/vue-next/issues/5048) [#5042](https://github.com/vuejs/vue-next/issues/5042) -* fix(runtime-core): ensure mergeProps skips undefined event handlers (#5299) ([c35ec47](https://github.com/vuejs/vue-next/commit/c35ec47)), closes [#5299](https://github.com/vuejs/vue-next/issues/5299) [#5296](https://github.com/vuejs/vue-next/issues/5296) -* fix(ssr): only cache computed getters during render phase ([2f91872](https://github.com/vuejs/vue-next/commit/2f91872)), closes [#5300](https://github.com/vuejs/vue-next/issues/5300) -* fix(types): calling readonly() with ref() should return Readonly> (#5212) ([c64907d](https://github.com/vuejs/vue-next/commit/c64907d)), closes [#5212](https://github.com/vuejs/vue-next/issues/5212) -* refactor: includes instead of indexOf (#5117) ([63210fe](https://github.com/vuejs/vue-next/commit/63210fe)), closes [#5117](https://github.com/vuejs/vue-next/issues/5117) -* chore: bump marked ([0c06c74](https://github.com/vuejs/vue-next/commit/0c06c74)) -* chore: comment dom tag config usage [ci skip] ([b2bac9f](https://github.com/vuejs/vue-next/commit/b2bac9f)) -* chore: fix typo (#5261) [ci skip] ([e603fd2](https://github.com/vuejs/vue-next/commit/e603fd2)), closes [#5261](https://github.com/vuejs/vue-next/issues/5261) -* chore: fix typo (#5282) [ci skip] ([e802275](https://github.com/vuejs/vue-next/commit/e802275)), closes [#5282](https://github.com/vuejs/vue-next/issues/5282) -* chore: type improvements (#5264) ([92e04a6](https://github.com/vuejs/vue-next/commit/92e04a6)), closes [#5264](https://github.com/vuejs/vue-next/issues/5264) -* chore: update repo references ([ae4b078](https://github.com/vuejs/vue-next/commit/ae4b078)) -* perf(reactivity): optimize effect run condition ([25bc654](https://github.com/vuejs/vue-next/commit/25bc654)) -* feat(reactivity): add isShallow api ([9fda941](https://github.com/vuejs/vue-next/commit/9fda941)) -* docs(contributing): missing structure info for compiler-sfc (#3559) [ci skip] ([8cbfe09](https://github.com/vuejs/vue-next/commit/8cbfe09)), closes [#3559](https://github.com/vuejs/vue-next/issues/3559) - - - -## [3.2.27](https://github.com/vuejs/core/compare/v3.2.26...v3.2.27) (2022-01-16) - - -### Bug Fixes - -* **KeepAlive:** remove cached VNode properly ([#5260](https://github.com/vuejs/core/issues/5260)) ([2e3e183](https://github.com/vuejs/core/commit/2e3e183b4f19c9e25865e35438653cbc9bf01afc)), closes [#5258](https://github.com/vuejs/core/issues/5258) -* **reactivity-transform:** should not rewrite for...in / for...of scope variables ([7007ffb](https://github.com/vuejs/core/commit/7007ffb2c796d6d56b9c8e278c54dc1cefd7b58f)) -* **sfc-playground:** hide title to avoid overlap ([#5099](https://github.com/vuejs/core/issues/5099)) ([44b9527](https://github.com/vuejs/core/commit/44b95276f5c086e1d88fa3c686a5f39eb5bb7821)) -* **ssr:** make computed inactive during ssr, fix memory leak ([f4f0966](https://github.com/vuejs/core/commit/f4f0966b33863ac0fca6a20cf9e8ddfbb311ae87)), closes [#5208](https://github.com/vuejs/core/issues/5208) -* **ssr:** remove missing ssr directive transform error ([55cc4af](https://github.com/vuejs/core/commit/55cc4af25e6f4924b267620bd965e496f260d41a)) -* **types/tsx:** allow ref_for type on tsx elements ([78df8c7](https://github.com/vuejs/core/commit/78df8c78c4539d2408278d1a11612b6bbc47d22f)) -* **types:** fix shallowReadonly type ([92f11d6](https://github.com/vuejs/core/commit/92f11d6740929f5b591740e30ae5fba50940ec82)) -* **types:** handle ToRef ([5ac7030](https://github.com/vuejs/core/commit/5ac703055fa83cb1e8a173bbd6a4d6c33707a3c3)), closes [#5188](https://github.com/vuejs/core/issues/5188) -* **types:** KeepAlive match pattern should allow mixed array ([3007d5b](https://github.com/vuejs/core/commit/3007d5b4cafed1da445bc498f771bd2c79eda6fc)) - - -### Features - -* **types:** simplify `ExtractPropTypes` to avoid props JSDocs being removed ([#5166](https://github.com/vuejs/core/issues/5166)) ([a570b38](https://github.com/vuejs/core/commit/a570b38741a7dc259772c5ccce7ea8a1638eb0bd)) - - -### Performance Improvements - -* improve memory usage for static vnodes ([ed9eb62](https://github.com/vuejs/core/commit/ed9eb62e5992bd575d999c4197330d8bad622cfb)) - - - -## [3.2.26](https://github.com/vuejs/core/compare/v3.2.25...v3.2.26) (2021-12-12) - - - -## [3.2.25](https://github.com/vuejs/core/compare/v3.2.24...v3.2.25) (2021-12-12) - - -### Bug Fixes - -* **compiler-sfc:** generate valid TS in script and script setup co-usage with TS ([7e4f0a8](https://github.com/vuejs/core/commit/7e4f0a869498e7dce601e7c150f402045ea2e79b)), closes [#5094](https://github.com/vuejs/core/issues/5094) -* **compiler:** force block for custom dirs and inline beforeUpdate hooks ([1c9a481](https://github.com/vuejs/core/commit/1c9a4810fcdd2b6c1c6c3be077aebbecbfcbcf1e)) -* **runtime-core:** disallow recurse in vnode/directive beforeUpdate hooks ([a1167c5](https://github.com/vuejs/core/commit/a1167c57e5514be57505f4bce8d163aa1f92cf14)) - - -### Features - -* **compiler-core:** support aliasing vue: prefixed events to inline vnode hooks ([4b0ca87](https://github.com/vuejs/core/commit/4b0ca8709a7e2652f4b02665f378d47ba4dbe969)) -* **experimental:** allow const for ref sugar declarations ([9823bd9](https://github.com/vuejs/core/commit/9823bd95d11f22f0ae53f5e0b705a21b6e6e8859)) -* **reactivity-transform/types:** restructure macro types + export types for all shorthand methods ([db729ce](https://github.com/vuejs/core/commit/db729ce99eb13cd18dad600055239c63edd9cfb8)) -* **reactivity-transform:** $$() escape for destructured prop bindings ([198ca14](https://github.com/vuejs/core/commit/198ca14f192f9eb80028153f3d36600e636de3f0)) -* **reactivity-transform:** rename @vue/ref-transform to @vue/reactivity-transform ([d70fd8d](https://github.com/vuejs/core/commit/d70fd8d36b23c987f2ebe3280da785f4d2e7d2ef)) -* **reactivity-transform:** support $-shorthands for all ref-creating APIs ([179fc05](https://github.com/vuejs/core/commit/179fc05a8406eac525c8450153b42fcb5af7d6bb)) -* **reactivity-transform:** support optionally importing macros ([fbd0fe9](https://github.com/vuejs/core/commit/fbd0fe97595f759e12e445c713b732775589fabf)) -* **reactivity-transform:** use toRef() for $() destructure codegen ([93ba6b9](https://github.com/vuejs/core/commit/93ba6b974e4a2ff4ba004fef47ef69cfe980c654)) -* **reactivity:** support default value in toRef() ([2db9c90](https://github.com/vuejs/core/commit/2db9c909c2cf3845f57b2c930c05cd6c17abe3b0)) -* **sfc-playground:** add github link ([#5067](https://github.com/vuejs/core/issues/5067)) ([9ac0dde](https://github.com/vuejs/core/commit/9ac0ddea4beec1a1c4471463d3476ccd019bd84e)) -* **sfc-playground:** prevent ctrl+s default behavior ([#5066](https://github.com/vuejs/core/issues/5066)) ([b027507](https://github.com/vuejs/core/commit/b0275070e4824c5efa868528f610eaced83d8fbc)) -* support ref in v-for, remove compat deprecation warnings ([41c18ef](https://github.com/vuejs/core/commit/41c18effea9dd32ab899b5de3bb0513abdb52ee4)) - - - -## [3.2.24](https://github.com/vuejs/core/compare/v3.2.23...v3.2.24) (2021-12-06) - - -### Bug Fixes - -* **compat:** maintain compatConfig option in legacy functional comp ([#4974](https://github.com/vuejs/core/issues/4974)) ([ee97cf5](https://github.com/vuejs/core/commit/ee97cf5a4db9e4f135d8eb25aff725eb37363675)) -* **compiler-dom:** avoid bailing stringification on setup const bindings ([29beda7](https://github.com/vuejs/core/commit/29beda7c6f69f79e65f0111cb2d2b8d57d8257bb)) -* **compiler-sfc:** make asset url imports stringifiable ([87c73e9](https://github.com/vuejs/core/commit/87c73e99d6aed0771f8c955ca9d5188ec22c90e7)) -* **package:** ensure ref-macros export is recognized by vue-tsc ([#5003](https://github.com/vuejs/core/issues/5003)) ([f855269](https://github.com/vuejs/core/commit/f8552697fbbdbd444d8322c6b6adeb48cc0b5617)) -* **runtime-core:** handle initial undefined attrs ([#5017](https://github.com/vuejs/core/issues/5017)) ([6d887aa](https://github.com/vuejs/core/commit/6d887aaf591cfa05d5fea978bbd87e3e502bfa86)), closes [#5016](https://github.com/vuejs/core/issues/5016) -* **types/reactivity:** export ShallowRef type ([#5026](https://github.com/vuejs/core/issues/5026)) ([523b4b7](https://github.com/vuejs/core/commit/523b4b78f5d2e11f1822e09c324a854c790a7863)), closes [#5205](https://github.com/vuejs/core/issues/5205) - - -### Features - -* **types/script-setup:** add generic type to defineExpose ([#5035](https://github.com/vuejs/core/issues/5035)) ([34985fe](https://github.com/vuejs/core/commit/34985fee6b23018b6eb6322239db6165c1b0e273)) - - - -## [3.2.23](https://github.com/vuejs/core/compare/v3.2.22...v3.2.23) (2021-11-26) - - -### Bug Fixes - -* **reactivity:** retain readonly proxies when setting as reactive property ([d145128](https://github.com/vuejs/core/commit/d145128ab400f4563eb3727626d0942ea5f4980a)), closes [#4986](https://github.com/vuejs/core/issues/4986) -* **runtime-core:** fix component public instance has check for accessed non-existent properties ([aac0466](https://github.com/vuejs/core/commit/aac0466cb8819fd132fbcc9c4d3e1014c14e2ad8)), closes [#4962](https://github.com/vuejs/core/issues/4962) -* **runtime-core:** handle error in async KeepAlive hooks ([#4978](https://github.com/vuejs/core/issues/4978)) ([820a143](https://github.com/vuejs/core/commit/820a14345798edc0ab673bae8ce3181e479d9cca)) -* **runtime-dom:** fix option element value patching edge case ([#4959](https://github.com/vuejs/core/issues/4959)) ([89b2f92](https://github.com/vuejs/core/commit/89b2f924fc82d7f71dcb8ffbacb386fd5cf9ade2)), closes [#4956](https://github.com/vuejs/core/issues/4956) -* **runtime-dom:** patchDOMProps should not set _value if element is custom element ([#4839](https://github.com/vuejs/core/issues/4839)) ([1701bf3](https://github.com/vuejs/core/commit/1701bf3968f001dd3a2bc9f41e3e7e0f1b13e922)) -* **types:** export ref-macros.d.ts ([1245709](https://github.com/vuejs/core/commit/124570973df4ddfdd38e43bf1e92b9710321e5d9)) -* **types:** fix propType type inference ([#4985](https://github.com/vuejs/core/issues/4985)) ([3c449cd](https://github.com/vuejs/core/commit/3c449cd408840d35987fb32b39737fbf093809d6)), closes [#4983](https://github.com/vuejs/core/issues/4983) -* **types:** scrip-setup+ts: ensure proper handling of `null` as default prop value. ([#4979](https://github.com/vuejs/core/issues/4979)) ([f2d2d7b](https://github.com/vuejs/core/commit/f2d2d7b2d236f256531ae9ad2048bd939c92d834)), closes [#4868](https://github.com/vuejs/core/issues/4868) - - -### Features - -* **compiler-sfc:** export resolveTemplateUsageCheckString for HMR plugin use ([#4908](https://github.com/vuejs/core/issues/4908)) ([c61baac](https://github.com/vuejs/core/commit/c61baac75a03b938bc728a8de961ba93736a0ff6)) -* **compiler-sfc:** expose properties for more accurate HMR ([68c45e7](https://github.com/vuejs/core/commit/68c45e73da902e715df9614800a7ab43d6579198)), closes [#4358](https://github.com/vuejs/core/issues/4358) [#4908](https://github.com/vuejs/core/issues/4908) - - - -## [3.2.22](https://github.com/vuejs/core/compare/v3.2.21...v3.2.22) (2021-11-15) - - -### Bug Fixes - -* **compiler-sfc:** add type for props include Function in prod mode ([#4938](https://github.com/vuejs/core/issues/4938)) ([9c42a1e](https://github.com/vuejs/core/commit/9c42a1e2a3385f3b33faed5cdcc430bf8c1fc4b2)) -* **compiler-sfc:** add type for props's properties in prod mode ([#4790](https://github.com/vuejs/core/issues/4790)) ([090df08](https://github.com/vuejs/core/commit/090df0837eb0aedd8a02fd0107b7668ca5c136a1)), closes [#4783](https://github.com/vuejs/core/issues/4783) -* **compiler-sfc:** externalRE support automatic http/https prefix url pattern ([#4922](https://github.com/vuejs/core/issues/4922)) ([574070f](https://github.com/vuejs/core/commit/574070f43f804fd855f4ee319936ec770a56cef0)), closes [#4920](https://github.com/vuejs/core/issues/4920) -* **compiler-sfc:** fix expose codegen edge case ([#4919](https://github.com/vuejs/core/issues/4919)) ([31fd590](https://github.com/vuejs/core/commit/31fd590fd47e2dc89b84687ffe26a5c6f05fea34)), closes [#4917](https://github.com/vuejs/core/issues/4917) -* **devtool:** improve devtools late injection browser env detection ([#4890](https://github.com/vuejs/core/issues/4890)) ([fa2237f](https://github.com/vuejs/core/commit/fa2237f1d824eac511c4246135318594c48dc121)) -* **runtime-core:** improve dedupe listeners when attr fallthrough ([#4912](https://github.com/vuejs/core/issues/4912)) ([b4eb7e3](https://github.com/vuejs/core/commit/b4eb7e3866d7dc722d93a48f4faae1696d4e7023)), closes [#4859](https://github.com/vuejs/core/issues/4859) -* **types/sfc:** fix withDefaults type inference when using union types ([#4925](https://github.com/vuejs/core/issues/4925)) ([04e5835](https://github.com/vuejs/core/commit/04e58351965caf489ac68e4961ef70448d954912)) - - - -## [3.2.21](https://github.com/vuejs/core/compare/v3.2.20...v3.2.21) (2021-11-02) - - -### Bug Fixes - -* **custom-element:** fix custom element props access on initial render ([4b7f76e](https://github.com/vuejs/core/commit/4b7f76e36a7fc650986a20eca258f7a5d912424f)), closes [#4792](https://github.com/vuejs/core/issues/4792) -* **custom-element:** fix initial attr type casting for programmtically created elements ([3ca8317](https://github.com/vuejs/core/commit/3ca83179d1a798f65e4e70215c511e2f1b64adb6)), closes [#4772](https://github.com/vuejs/core/issues/4772) -* **devtools:** avoid open handle in non-browser env ([6916d72](https://github.com/vuejs/core/commit/6916d725a06a57e92ff9d046ccf132c305cd0a51)), closes [#4815](https://github.com/vuejs/core/issues/4815) -* **devtools:** fix memory leak when devtools is not installed ([#4833](https://github.com/vuejs/core/issues/4833)) ([6b32f0d](https://github.com/vuejs/core/commit/6b32f0d976c0aac8bb2c1b78fedd03e76fb391eb)), closes [#4829](https://github.com/vuejs/core/issues/4829) -* **runtime-core:** add `v-memo` to built-in directives check ([#4787](https://github.com/vuejs/core/issues/4787)) ([5eb7263](https://github.com/vuejs/core/commit/5eb72630a53a8dd82c2b8a9705c21a8075161a3d)) -* **runtime-dom:** fix behavior regression for v-show + style display binding ([3f38d59](https://github.com/vuejs/core/commit/3f38d599f5aacdd3eeaa9475251a24f74e7ae3b4)), closes [#4768](https://github.com/vuejs/core/issues/4768) -* **types:** fix ref unwrapping type inference for nested shallowReactive & shallowRef ([20a3615](https://github.com/vuejs/core/commit/20a361541cc5faffa82cbf3f2d49639a97b3b678)), closes [#4771](https://github.com/vuejs/core/issues/4771) - - - -## [3.2.20](https://github.com/vuejs/core/compare/v3.2.19...v3.2.20) (2021-10-08) - - -### Bug Fixes - -* **compiler-sfc:** fix props codegen w/ leading import ([d4c04e9](https://github.com/vuejs/core/commit/d4c04e979934b81a30467aa4b1e717175b9b2d80)), closes [#4764](https://github.com/vuejs/core/issues/4764) -* **compiler-sfc:** support runtime Enum in normal script ([#4698](https://github.com/vuejs/core/issues/4698)) ([f66d456](https://github.com/vuejs/core/commit/f66d456b7a39db9dae7e70c28bb431ff293d8fef)) -* **devtools:** clear devtools buffer after timeout ([f4639e0](https://github.com/vuejs/core/commit/f4639e0a36abe16828b202d7297e1486653b1217)), closes [#4738](https://github.com/vuejs/core/issues/4738) -* **hmr:** fix hmr for components with no active instance yet ([9e3d773](https://github.com/vuejs/core/commit/9e3d7731c7839638f49157123c6b372fec9e4d46)), closes [#4757](https://github.com/vuejs/core/issues/4757) -* **types:** ensure that DeepReadonly handles Ref type properly ([#4714](https://github.com/vuejs/core/issues/4714)) ([ed0071a](https://github.com/vuejs/core/commit/ed0071ac1a6d18439f3212711c6901fbb7193288)) -* **types:** make `toRef` return correct type(fix [#4732](https://github.com/vuejs/core/issues/4732)) ([#4734](https://github.com/vuejs/core/issues/4734)) ([925bc34](https://github.com/vuejs/core/commit/925bc346fe85091467fcd2e40d6c1ff07f3b51c4)) - - -### Features - -* **compiler-sfc:** ` + `) + expect(content).toMatch(`return { a, b }`) + assertCode(content) + }) + test('should expose top level declarations', () => { const { content, bindings } = compile(` `) - expect(content).toMatch('return { aa, bb, cc, dd, a, b, c, d, xx, x }') + expect(content).toMatch( + `return { get aa() { return aa }, set aa(v) { aa = v }, ` + + `bb, cc, dd, get a() { return a }, set a(v) { a = v }, b, c, d, ` + + `get xx() { return xx }, get x() { return x } }` + ) expect(bindings).toStrictEqual({ x: BindingTypes.SETUP_MAYBE_REF, a: BindingTypes.SETUP_LET, @@ -29,7 +44,7 @@ describe('SFC compile - `) - // should generate working code - assertCode(content) - // should anayze bindings - expect(bindings).toStrictEqual({ - foo: BindingTypes.PROPS, - bar: BindingTypes.SETUP_CONST, - props: BindingTypes.SETUP_CONST - }) - - // should remove defineOptions import and call - expect(content).not.toMatch('defineProps') - // should generate correct setup signature - expect(content).toMatch(`setup(__props, { 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', () => { + test('defineProps/defineEmits in multi-variable declaration', () => { const { content } = compile(` - `) + `) assertCode(content) - expect(content).toMatch(`export default { - props: propsModel,`) + expect(content).toMatch(`const a = 1;`) // test correct removal + expect(content).toMatch(`props: ['item'],`) + expect(content).toMatch(`emits: ['a'],`) }) - // #4764 - test('defineProps w/ leading code', () => { + // #6757 + test('defineProps/defineEmits in multi-variable declaration fix #6757 ', () => { 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 defineOptions import and call - expect(content).not.toMatch('defineEmits') - // should generate correct setup signature - expect(content).toMatch(`setup(__props, { expose, emit: myEmit }) {`) - // should include context options in default export - expect(content).toMatch(`export default { - emits: ['foo', 'bar'],`) + expect(content).toMatch(`const a = 1;`) // test correct removal + expect(content).toMatch(`props: ['item'],`) + expect(content).toMatch(`emits: ['a'],`) }) - test('defineProps/defineEmits in multi-variable declaration', () => { + // #7422 + test('defineProps/defineEmits in multi-variable declaration fix #7422', () => { const { content } = compile(` `) assertCode(content) - expect(content).toMatch(`const a = 1;`) // test correct removal expect(content).toMatch(`props: ['item'],`) - expect(content).toMatch(`emits: ['a'],`) + expect(content).toMatch(`emits: ['foo'],`) + expect(content).toMatch(`const a = 0,`) + expect(content).toMatch(`b = 0;`) }) test('defineProps/defineEmits in multi-variable declaration (full removal)', () => { @@ -153,21 +126,6 @@ const myEmit = defineEmits(['foo', 'bar']) expect(content).toMatch(`emits: ['a'],`) }) - 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 }) {`) - // should replace callee - expect(content).toMatch(/\bexpose\(\{ foo: 123 \}\)/) - }) - describe(' + + + `) + assertCode(content) + }) }) describe('imports', () => { @@ -331,6 +305,70 @@ defineExpose({ foo: 123 }) content.lastIndexOf(`import { x }`) ) }) + + describe('import ref/reactive function from other place', () => { + test('import directly', () => { + const { bindings } = compile(` + + `) + expect(bindings).toStrictEqual({ + ref: BindingTypes.SETUP_MAYBE_REF, + reactive: BindingTypes.SETUP_MAYBE_REF, + foo: BindingTypes.SETUP_MAYBE_REF, + bar: BindingTypes.SETUP_MAYBE_REF + }) + }) + + test('import w/ alias', () => { + const { bindings } = compile(` + + `) + expect(bindings).toStrictEqual({ + _reactive: BindingTypes.SETUP_MAYBE_REF, + _ref: BindingTypes.SETUP_MAYBE_REF, + foo: BindingTypes.SETUP_MAYBE_REF, + bar: BindingTypes.SETUP_MAYBE_REF + }) + }) + + test('aliased usage before import site', () => { + const { bindings } = compile(` + + `) + expect(bindings).toStrictEqual({ + bar: BindingTypes.SETUP_REACTIVE_CONST, + x: BindingTypes.SETUP_CONST + }) + }) + }) + + test('should support module string names syntax', () => { + const { content, bindings } = compile(` + + + `) + assertCode(content) + expect(bindings).toStrictEqual({ + foo: BindingTypes.SETUP_MAYBE_REF + }) + }) }) // in dev mode, declared bindings are returned as an object from setup() @@ -355,7 +393,10 @@ defineExpose({ foo: 123 }) // FooBaz: used as PascalCase component // FooQux: used as kebab-case component // foo: lowercase component - expect(content).toMatch(`return { fooBar, FooBaz, FooQux, foo }`) + expect(content).toMatch( + `return { fooBar, get FooBaz() { return FooBaz }, ` + + `get FooQux() { return FooQux }, get foo() { return foo } }` + ) assertCode(content) }) @@ -368,7 +409,7 @@ defineExpose({ foo: 123 })

`) - expect(content).toMatch(`return { vMyDir }`) + expect(content).toMatch(`return { get vMyDir() { return vMyDir } }`) assertCode(content) }) @@ -383,7 +424,9 @@ defineExpose({ foo: 123 })
`) - expect(content).toMatch(`return { cond, bar, baz }`) + expect(content).toMatch( + `return { cond, get bar() { return bar }, get baz() { return baz } }` + ) assertCode(content) }) @@ -399,11 +442,13 @@ defineExpose({ foo: 123 }) // x: used in interpolation // y: should not be matched by {{ yy }} or 'y' in binding exps // x$y: #4274 should escape special chars when creating Regex - expect(content).toMatch(`return { x, z, x$y }`) + expect(content).toMatch( + `return { get x() { return x }, get z() { return z }, get x$y() { return x$y } }` + ) assertCode(content) }) - // #4340 interpolations in tempalte strings + // #4340 interpolations in template strings test('js template string interpolations', () => { const { content } = compile(` + + `) + expect(content).toMatch(`return { a, b, get Baz() { return Baz } }`) + assertCode(content) + }) + + // vuejs/vue#12591 + test('v-on inline statement', () => { + // should not error + compile(` + + + `) + }) }) describe('inlineTemplate mode', () => { @@ -468,7 +549,7 @@ defineExpose({ foo: 123 }) { inlineTemplate: true } ) assertCode(content) - expect(content).toMatch(`setup(__props, { expose })`) + expect(content).toMatch(`setup(__props, { expose: __expose })`) expect(content).toMatch(`expose({ count })`) }) @@ -502,6 +583,7 @@ defineExpose({ foo: 123 }) import { ref } from 'vue' import Foo, { bar } from './Foo.vue' import other from './util' + import * as tree from './tree' const count = ref(0) const constant = {} const maybe = foo() @@ -511,6 +593,7 @@ defineExpose({ foo: 123 }) `, { inlineTemplate: true } @@ -529,6 +612,8 @@ defineExpose({ foo: 123 }) expect(content).toMatch(`unref(maybe)`) // should unref() on let bindings expect(content).toMatch(`unref(lett)`) + // no need to unref namespace import (this also preserves tree-shaking) + expect(content).toMatch(`tree.foo()`) // no need to unref function declarations expect(content).toMatch(`{ onClick: fn }`) // no need to mark constant fns in patch flag @@ -563,6 +648,26 @@ defineExpose({ foo: 123 }) assertCode(content) }) + test('v-model should not generate ref assignment code for non-setup bindings', () => { + const { content } = compile( + ` + + + `, + { inlineTemplate: true } + ) + expect(content).not.toMatch(`_isRef(foo)`) + }) + test('template assignment expression codegen', () => { const { content } = compile( ` + + `, + { + inlineTemplate: false + } + ) + ).not.toThrowError() + }) }) describe('with TypeScript', () => { @@ -709,357 +835,6 @@ defineExpose({ foo: 123 }) assertCode(content) }) - test('defineProps/Emit w/ runtime options', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`export default /*#__PURE__*/_defineComponent({ - props: { foo: String }, - emits: ['a', 'b'], - setup(__props, { expose, emit }) {`) - }) - - 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( - `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(`foo: { type: [Function, null], required: 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, - union: BindingTypes.PROPS, - literalUnion: BindingTypes.PROPS, - literalUnionNumber: BindingTypes.PROPS, - literalUnionMixed: BindingTypes.PROPS, - intersection: BindingTypes.PROPS, - foo: BindingTypes.PROPS - }) - }) - - test('defineProps w/ 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', () => { - 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('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( - `{ foo: string, bar?: number, baz: boolean, qux(): number }` - ) - expect(content).toMatch(`const props = __props`) - expect(bindings).toStrictEqual({ - foo: BindingTypes.PROPS, - bar: BindingTypes.PROPS, - baz: BindingTypes.PROPS, - qux: BindingTypes.PROPS, - props: BindingTypes.SETUP_CONST - }) - }) - - 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('defineEmits w/ type', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`emit: ((e: 'foo' | 'bar') => void),`) - 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(`emit: (${type}),`) - expect(content).toMatch(`emits: ["foo", "bar", "baz"]`) - }) - - test('defineEmits w/ type (interface)', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`emit: ({ (e: 'foo' | 'bar'): void }),`) - expect(content).toMatch(`emits: ["foo", "bar"]`) - }) - - test('defineEmits w/ type (exported interface)', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`emit: ({ (e: 'foo' | 'bar'): void }),`) - expect(content).toMatch(`emits: ["foo", "bar"]`) - }) - - test('defineEmits w/ type (type alias)', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`emit: ({ (e: 'foo' | 'bar'): void }),`) - expect(content).toMatch(`emits: ["foo", "bar"]`) - }) - - test('defineEmits w/ type (exported type alias)', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`emit: ({ (e: 'foo' | 'bar'): void }),`) - expect(content).toMatch(`emits: ["foo", "bar"]`) - }) - - test('defineEmits w/ type (referenced function type)', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`emit: ((e: 'foo' | 'bar') => void),`) - expect(content).toMatch(`emits: ["foo", "bar"]`) - }) - - test('defineEmits w/ type (referenced exported function type)', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`emit: ((e: 'foo' | 'bar') => void),`) - expect(content).toMatch(`emits: ["foo", "bar"]`) - }) - test('runtime Enum', () => { const { content, bindings } = compile( `` + `, + { hoistStatic: true } ) assertCode(content) expect(bindings).toStrictEqual({ - Foo: BindingTypes.SETUP_CONST + Foo: BindingTypes.LITERAL_CONST }) }) + + test('import type', () => { + const { content } = compile( + `` + ) + expect(content).toMatch(`return { get Baz() { return Baz } }`) + assertCode(content) + }) }) describe('async/await detection', () => { @@ -1153,6 +940,59 @@ const emit = defineEmits(['a', 'b']) assertAwaitDetection(`if (ok) { await foo } else { await bar }`) }) + test('multiple `if` nested statements', () => { + assertAwaitDetection(`if (ok) { + let a = 'foo' + await 0 + await 1 + await 2 + } else if (a) { + await 10 + if (b) { + await 0 + await 1 + } else { + let a = 'foo' + await 2 + } + if (b) { + await 3 + await 4 + } + } else { + await 5 + }`) + }) + + test('multiple `if while` nested statements', () => { + assertAwaitDetection(`if (ok) { + while (d) { + await 5 + } + while (d) { + await 5 + await 6 + if (c) { + let f = 10 + 10 + await 7 + } else { + await 8 + await 9 + } + } + }`) + }) + + test('multiple `if for` nested statements', () => { + assertAwaitDetection(`if (ok) { + for (let a of [1,2,3]) { + await a + } + for (let a of [1,2,3]) { + await a + await a + } + }`) + }) + test('should ignore await inside functions', () => { // function declaration assertAwaitDetection(`async function foo() { await bar }`, false) @@ -1198,24 +1038,10 @@ const emit = defineEmits(['a', 'b']) ).toThrow(moduleErrorMsg) }) - test('defineProps/Emit() w/ both type and non-type args', () => { - expect(() => { - 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(``) ).toThrow(`cannot reference locally declared variables`) @@ -1367,7 +1193,7 @@ describe('SFC analyze `) expect(bindings).toStrictEqual({ - foo: BindingTypes.SETUP_CONST + foo: BindingTypes.LITERAL_CONST }) }) @@ -1533,10 +1359,199 @@ describe('SFC analyze + `, + undefined, + { + filename: 'FooBar.vue' + } + ) + expect(content).toMatch(`export default { + __name: 'FooBar'`) + assertCode(content) + }) + + test('do not overwrite manual name (object)', () => { + const { content } = compile( + ` + + `, + undefined, + { + filename: 'FooBar.vue' + } + ) + expect(content).not.toMatch(`name: 'FooBar'`) + expect(content).toMatch(`name: 'Baz'`) + assertCode(content) + }) + + test('do not overwrite manual name (call)', () => { + const { content } = compile( + ` + + `, + undefined, + { + filename: 'FooBar.vue' + } + ) + expect(content).not.toMatch(`name: 'FooBar'`) + expect(content).toMatch(`name: 'Baz'`) + assertCode(content) + }) + }) +}) + +describe('SFC genDefaultAs', () => { + test('normal `, + { + genDefaultAs: '_sfc_' + } + ) + expect(content).not.toMatch('export default') + expect(content).toMatch(`const _sfc_ = {}`) + assertCode(content) + }) + + test('normal + `, + { + genDefaultAs: '_sfc_' + } + ) + expect(content).not.toMatch('export default') + expect(content).not.toMatch('__default__') + expect(content).toMatch(`const _sfc_ = {}`) + assertCode(content) + }) + + test(' + `, + { + genDefaultAs: '_sfc_' + } + ) + expect(content).not.toMatch('export default') + expect(content).toMatch( + `const _sfc_ = /*#__PURE__*/Object.assign(__default__` + ) + assertCode(content) + }) + + test(' + `, + { + genDefaultAs: '_sfc_' + } + ) + expect(content).not.toMatch('export default') + expect(content).toMatch( + `const _sfc_ = /*#__PURE__*/Object.assign(__default__` + ) + assertCode(content) + }) + + test('`, + { + genDefaultAs: '_sfc_' + } + ) + expect(content).not.toMatch('export default') + expect(content).toMatch(`const _sfc_ = {\n setup`) + assertCode(content) + }) + + test('`, + { + genDefaultAs: '_sfc_' + } + ) + expect(content).not.toMatch('export default') + expect(content).toMatch(`const _sfc_ = /*#__PURE__*/_defineComponent(`) + assertCode(content) + }) + + test(' + `, + { + genDefaultAs: '_sfc_' + } + ) + expect(content).not.toMatch('export default') + expect(content).toMatch( + `const _sfc_ = /*#__PURE__*/_defineComponent({\n ...__default__` + ) + 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__/compileScript/definePropsDestructure.spec.ts b/packages/compiler-sfc/__tests__/compileScript/definePropsDestructure.spec.ts new file mode 100644 index 00000000000..a2941872fd2 --- /dev/null +++ b/packages/compiler-sfc/__tests__/compileScript/definePropsDestructure.spec.ts @@ -0,0 +1,427 @@ +import { BindingTypes } from '@vue/compiler-core' +import { SFCScriptCompileOptions } from '../../src' +import { compileSFCScript, assertCode } from '../utils' + +describe('sfc reactive props destructure', () => { + function compile(src: string, options?: Partial) { + return compileSFCScript(src, { + inlineTemplate: true, + propsDestructure: true, + ...options + }) + } + + test('basic usage', () => { + const { content, bindings } = compile(` + + + `) + expect(content).not.toMatch(`const { foo } =`) + expect(content).toMatch(`console.log(__props.foo)`) + expect(content).toMatch(`_toDisplayString(__props.foo)`) + assertCode(content) + expect(bindings).toStrictEqual({ + foo: BindingTypes.PROPS + }) + }) + + test('multiple variable declarations', () => { + const { content, bindings } = compile(` + + + `) + expect(content).not.toMatch(`const { foo } =`) + expect(content).toMatch(`const bar = 'fish', hello = 'world'`) + expect(content).toMatch(`_toDisplayString(hello)`) + expect(content).toMatch(`_toDisplayString(bar)`) + expect(content).toMatch(`_toDisplayString(__props.foo)`) + assertCode(content) + expect(bindings).toStrictEqual({ + foo: BindingTypes.PROPS, + bar: BindingTypes.LITERAL_CONST, + hello: BindingTypes.LITERAL_CONST + }) + }) + + test('nested scope', () => { + const { content, bindings } = compile(` + + `) + expect(content).not.toMatch(`const { foo, bar } =`) + expect(content).toMatch(`console.log(foo)`) + expect(content).toMatch(`console.log(__props.bar)`) + assertCode(content) + expect(bindings).toStrictEqual({ + foo: BindingTypes.PROPS, + bar: BindingTypes.PROPS, + test: BindingTypes.SETUP_CONST + }) + }) + + test('default values w/ array runtime declaration', () => { + const { content } = compile(` + + `) + // literals can be used as-is, non-literals are always returned from a + // function + // functions need to be marked with a skip marker + expect(content).toMatch(`props: _mergeDefaults(['foo', 'bar', 'baz'], { + foo: 1, + bar: () => ({}), + func: () => {}, __skip_func: true +})`) + assertCode(content) + }) + + test('default values w/ object runtime declaration', () => { + const { content } = compile(` + + `) + // literals can be used as-is, non-literals are always returned from a + // function + // functions need to be marked with a skip marker since we cannot always + // safely infer whether runtime type is Function (e.g. if the runtime decl + // is imported, or spreads another object) + expect(content) + .toMatch(`props: _mergeDefaults({ foo: Number, bar: Object, func: Function, ext: null }, { + foo: 1, + bar: () => ({}), + func: () => {}, __skip_func: true, + ext: x, __skip_ext: true +})`) + 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(` + + `) + // literals can be used as-is, non-literals are always returned from a + // function + expect(content).toMatch(`props: { + foo: { type: Number, required: false, default: 1 }, + bar: { type: Object, required: false, default: () => ({}) }, + func: { type: Function, required: false, default: () => {} } + }`) + 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( + ` + + `, + { isProd: true } + ) + assertCode(content) + // literals can be used as-is, non-literals are always returned from a + // function + expect(content).toMatch(`props: { + foo: { default: 1 }, + bar: { default: () => ({}) }, + baz: {}, + boola: { type: Boolean }, + boolb: { type: [Boolean, Number] }, + func: { type: Function, default: () => {} } + }`) + }) + + test('aliasing', () => { + const { content, bindings } = compile(` + + + `) + expect(content).not.toMatch(`const { foo: bar } =`) + expect(content).toMatch(`let x = foo`) // should not process + expect(content).toMatch(`let y = __props.foo`) + // should convert bar to __props.foo in template expressions + expect(content).toMatch(`_toDisplayString(__props.foo + __props.foo)`) + assertCode(content) + expect(bindings).toStrictEqual({ + x: BindingTypes.SETUP_LET, + y: BindingTypes.SETUP_LET, + foo: BindingTypes.PROPS, + bar: BindingTypes.PROPS_ALIASED, + __propsAliases: { + bar: 'foo' + } + }) + }) + + // #5425 + test('non-identifier prop names', () => { + const { content, bindings } = compile(` + + + `) + expect(content).toMatch(`x = __props["foo.bar"]`) + expect(content).toMatch(`toDisplayString(__props["foo.bar"])`) + assertCode(content) + expect(bindings).toStrictEqual({ + x: BindingTypes.SETUP_LET, + 'foo.bar': BindingTypes.PROPS, + fooBar: BindingTypes.PROPS_ALIASED, + __propsAliases: { + fooBar: 'foo.bar' + } + }) + }) + + test('rest spread', () => { + const { content, bindings } = compile(` + + `) + expect(content).toMatch( + `const rest = _createPropsRestProxy(__props, ["foo","bar"])` + ) + assertCode(content) + expect(bindings).toStrictEqual({ + foo: BindingTypes.PROPS, + bar: BindingTypes.PROPS, + baz: BindingTypes.PROPS, + rest: BindingTypes.SETUP_REACTIVE_CONST + }) + }) + + // #6960 + test('computed static key', () => { + const { content, bindings } = compile(` + + + `) + expect(content).not.toMatch(`const { foo } =`) + expect(content).toMatch(`console.log(__props.foo)`) + expect(content).toMatch(`_toDisplayString(__props.foo)`) + assertCode(content) + expect(bindings).toStrictEqual({ + foo: BindingTypes.PROPS + }) + }) + + describe('errors', () => { + test('should error on deep destructure', () => { + expect(() => + compile( + `` + ) + ).toThrow(`destructure does not support nested patterns`) + + expect(() => + compile( + `` + ) + ).toThrow(`destructure does not support nested patterns`) + }) + + test('should error on computed key', () => { + expect(() => + compile( + `` + ) + ).toThrow(`destructure cannot use computed key`) + }) + + test('should error when used with withDefaults', () => { + expect(() => + compile( + `` + ) + ).toThrow(`withDefaults() is unnecessary when using destructure`) + }) + + test('should error if destructure reference local vars', () => { + expect(() => + compile( + `` + ) + ).toThrow(`cannot reference locally declared variables`) + }) + + test('should error if assignment to destructured prop binding', () => { + expect(() => + compile( + `` + ) + ).toThrow(`Cannot assign to destructured props`) + + expect(() => + compile( + `` + ) + ).toThrow(`Cannot assign to destructured props`) + }) + + test('should error when passing destructured prop into certain methods', () => { + expect(() => + compile( + `` + ) + ).toThrow( + `"foo" is a destructured prop and should not be passed directly to watch().` + ) + + expect(() => + compile( + `` + ) + ).toThrow( + `"foo" is a destructured prop and should not be passed directly to watch().` + ) + + expect(() => + compile( + `` + ) + ).toThrow( + `"foo" is a destructured prop and should not be passed directly to toRef().` + ) + + expect(() => + compile( + `` + ) + ).toThrow( + `"foo" is a destructured prop and should not be passed directly to toRef().` + ) + }) + + // not comprehensive, but should help for most common cases + test('should error if default value type does not match declared type', () => { + expect(() => + compile( + `` + ) + ).toThrow(`Default value of prop "foo" does not match declared type.`) + }) + + // #8017 + test('should not throw an error if the variable is not a props', () => { + expect(() => + compile( + `` + ) + ).not.toThrowError() + }) + }) +}) 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__/compileScript/hoistStatic.spec.ts b/packages/compiler-sfc/__tests__/compileScript/hoistStatic.spec.ts new file mode 100644 index 00000000000..d2c76c9a2cc --- /dev/null +++ b/packages/compiler-sfc/__tests__/compileScript/hoistStatic.spec.ts @@ -0,0 +1,219 @@ +import { BindingTypes } from '@vue/compiler-core' +import { SFCScriptCompileOptions } from '../../src' +import { compileSFCScript, assertCode } from '../utils' + +describe('sfc hoist static', () => { + function compile(src: string, options?: Partial) { + return compileSFCScript(src, { + inlineTemplate: true, + hoistStatic: true, + ...options + }) + } + + test('should hoist literal value', () => { + const code = ` + const string = 'default value' + const number = 123 + const boolean = false + const nil = null + const bigint = 100n + const template = \`str\` + `.trim() + const { content, bindings } = compile(` + + `) + + // should hoist to first line + expect(content.startsWith(code)).toBe(true) + expect(bindings).toStrictEqual({ + string: BindingTypes.LITERAL_CONST, + number: BindingTypes.LITERAL_CONST, + boolean: BindingTypes.LITERAL_CONST, + nil: BindingTypes.LITERAL_CONST, + bigint: BindingTypes.LITERAL_CONST, + template: BindingTypes.LITERAL_CONST + }) + assertCode(content) + }) + + test('should hoist expressions', () => { + const code = ` + const unary = !false + const binary = 1 + 2 + const conditional = 1 ? 2 : 3 + const sequence = (1, true, 'foo', 1) + `.trim() + const { content, bindings } = compile(` + + `) + // should hoist to first line + expect(content.startsWith(code)).toBe(true) + expect(bindings).toStrictEqual({ + binary: BindingTypes.LITERAL_CONST, + conditional: BindingTypes.LITERAL_CONST, + unary: BindingTypes.LITERAL_CONST, + sequence: BindingTypes.LITERAL_CONST + }) + assertCode(content) + }) + + test('should hoist w/ defineProps/Emits', () => { + const hoistCode = `const defaultValue = 'default value'` + const { content, bindings } = compile(` + + `) + + // should hoist to first line + expect(content.startsWith(hoistCode)).toBe(true) + expect(bindings).toStrictEqual({ + foo: BindingTypes.PROPS, + defaultValue: BindingTypes.LITERAL_CONST + }) + assertCode(content) + }) + + test('should not hoist a variable', () => { + const code = ` + let KEY1 = 'default value' + var KEY2 = 123 + const regex = /.*/g + const undef = undefined + `.trim() + const { content, bindings } = compile(` + + `) + expect(bindings).toStrictEqual({ + KEY1: BindingTypes.SETUP_LET, + KEY2: BindingTypes.SETUP_LET, + regex: BindingTypes.SETUP_CONST, + undef: BindingTypes.SETUP_MAYBE_REF + }) + expect(content).toMatch(`setup(__props) {\n\n ${code}`) + assertCode(content) + }) + + test('should not hoist a constant initialized to a reference value', () => { + const code = ` + const KEY1 = Boolean + const KEY2 = [Boolean] + const KEY3 = [getCurrentInstance()] + let i = 0; + const KEY4 = (i++, 'foo') + enum KEY5 { + FOO = 1, + BAR = getCurrentInstance(), + } + const KEY6 = \`template\${i}\` + `.trim() + const { content, bindings } = compile(` + + `) + expect(bindings).toStrictEqual({ + KEY1: BindingTypes.SETUP_MAYBE_REF, + KEY2: BindingTypes.SETUP_CONST, + KEY3: BindingTypes.SETUP_CONST, + KEY4: BindingTypes.SETUP_CONST, + KEY5: BindingTypes.SETUP_CONST, + KEY6: BindingTypes.SETUP_CONST, + i: BindingTypes.SETUP_LET + }) + expect(content).toMatch(`setup(__props) {\n\n ${code}`) + assertCode(content) + }) + + test('should not hoist a object or array', () => { + const code = ` + const obj = { foo: 'bar' } + const arr = [1, 2, 3] + `.trim() + const { content, bindings } = compile(` + + `) + expect(bindings).toStrictEqual({ + arr: BindingTypes.SETUP_CONST, + obj: BindingTypes.SETUP_CONST + }) + expect(content).toMatch(`setup(__props) {\n\n ${code}`) + assertCode(content) + }) + + test('should not hoist a function or class', () => { + const code = ` + const fn = () => {} + function fn2() {} + class Foo {} + `.trim() + const { content, bindings } = compile(` + + `) + expect(bindings).toStrictEqual({ + Foo: BindingTypes.SETUP_CONST, + fn: BindingTypes.SETUP_CONST, + fn2: BindingTypes.SETUP_CONST + }) + expect(content).toMatch(`setup(__props) {\n\n ${code}`) + assertCode(content) + }) + + test('should enable when only script setup', () => { + const { content, bindings } = compile(` + + + `) + expect(bindings).toStrictEqual({ + foo: BindingTypes.SETUP_CONST + }) + assertCode(content) + }) + + test('should not hoist when disabled', () => { + const { content, bindings } = compile( + ` + + `, + { hoistStatic: false } + ) + expect(bindings).toStrictEqual({ + foo: BindingTypes.SETUP_CONST + }) + assertCode(content) + }) + + test('template binding access in inline mode', () => { + const { content } = 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 95% rename from packages/compiler-sfc/__tests__/compileScriptRefTransform.spec.ts rename to packages/compiler-sfc/__tests__/compileScript/reactivityTransform.spec.ts index 88d62f2b478..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__ @@ -32,7 +33,10 @@ describe('sfc ref transform', () => { // normal declarations left untouched expect(content).toMatch(`let c = () => {}`) expect(content).toMatch(`let d`) - expect(content).toMatch(`return { foo, a, b, c, d, ref, shallowRef }`) + expect(content).toMatch( + `return { foo, a, b, get c() { return c }, set c(v) { c = v }, ` + + `get d() { return d }, set d(v) { d = v }, ref, shallowRef }` + ) assertCode(content) expect(bindings).toStrictEqual({ foo: BindingTypes.SETUP_REF, 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..f671541977b --- /dev/null +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -0,0 +1,929 @@ +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 (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__/compileScriptPropsTransform.spec.ts b/packages/compiler-sfc/__tests__/compileScriptPropsTransform.spec.ts deleted file mode 100644 index 140dbec2e6b..00000000000 --- a/packages/compiler-sfc/__tests__/compileScriptPropsTransform.spec.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { BindingTypes } from '@vue/compiler-core' -import { SFCScriptCompileOptions } from '../src' -import { compileSFCScript, assertCode } from './utils' - -describe('sfc props transform', () => { - function compile(src: string, options?: Partial) { - return compileSFCScript(src, { - inlineTemplate: true, - reactivityTransform: true, - ...options - }) - } - - test('basic usage', () => { - const { content, bindings } = compile(` - - - `) - expect(content).not.toMatch(`const { foo } =`) - expect(content).toMatch(`console.log(__props.foo)`) - expect(content).toMatch(`_toDisplayString(__props.foo)`) - assertCode(content) - expect(bindings).toStrictEqual({ - foo: BindingTypes.PROPS - }) - }) - - test('nested scope', () => { - const { content, bindings } = compile(` - - `) - expect(content).not.toMatch(`const { foo, bar } =`) - expect(content).toMatch(`console.log(foo)`) - expect(content).toMatch(`console.log(__props.bar)`) - assertCode(content) - expect(bindings).toStrictEqual({ - foo: BindingTypes.PROPS, - bar: BindingTypes.PROPS, - test: BindingTypes.SETUP_CONST - }) - }) - - test('default values w/ runtime declaration', () => { - const { content } = compile(` - - `) - // literals can be used as-is, non-literals are always returned from a - // function - expect(content).toMatch(`props: _mergeDefaults(['foo', 'bar'], { - foo: 1, - bar: () => {} -})`) - assertCode(content) - }) - - test('default values w/ type declaration', () => { - const { content } = compile(` - - `) - // literals can be used as-is, non-literals are always returned from a - // function - expect(content).toMatch(`props: { - foo: { type: Number, required: false, default: 1 }, - bar: { type: Object, required: false, default: () => {} } - }`) - assertCode(content) - }) - - test('default values w/ type declaration, prod mode', () => { - const { content } = compile( - ` - - `, - { isProd: true } - ) - // literals can be used as-is, non-literals are always returned from a - // function - expect(content).toMatch(`props: { - foo: { default: 1 }, - bar: { default: () => {} }, - baz: null, - boola: { type: Boolean }, - boolb: { type: [Boolean, Number] }, - func: { type: Function, default: () => () => {} } - }`) - assertCode(content) - }) - - test('aliasing', () => { - const { content, bindings } = compile(` - - - `) - expect(content).not.toMatch(`const { foo: bar } =`) - expect(content).toMatch(`let x = foo`) // should not process - expect(content).toMatch(`let y = __props.foo`) - // should convert bar to __props.foo in template expressions - expect(content).toMatch(`_toDisplayString(__props.foo + __props.foo)`) - assertCode(content) - expect(bindings).toStrictEqual({ - x: BindingTypes.SETUP_LET, - y: BindingTypes.SETUP_LET, - foo: BindingTypes.PROPS, - bar: BindingTypes.PROPS_ALIASED, - __propsAliases: { - bar: 'foo' - } - }) - }) - - test('rest spread', () => { - const { content, bindings } = compile(` - - `) - expect(content).toMatch( - `const rest = _createPropsRestProxy(__props, ["foo","bar"])` - ) - assertCode(content) - expect(bindings).toStrictEqual({ - foo: BindingTypes.PROPS, - bar: BindingTypes.PROPS, - baz: BindingTypes.PROPS, - rest: BindingTypes.SETUP_CONST - }) - }) - - test('$$() escape', () => { - const { content } = compile(` - - `) - expect(content).toMatch(`const __props_foo = _toRef(__props, 'foo')`) - expect(content).toMatch(`const __props_bar = _toRef(__props, 'bar')`) - expect(content).toMatch(`console.log((__props_foo))`) - expect(content).toMatch(`console.log((__props_bar))`) - expect(content).toMatch(`({ foo: __props_foo, baz: __props_bar })`) - assertCode(content) - }) - - describe('errors', () => { - test('should error on deep destructure', () => { - expect(() => - compile( - `` - ) - ).toThrow(`destructure does not support nested patterns`) - - expect(() => - compile( - `` - ) - ).toThrow(`destructure does not support nested patterns`) - }) - - test('should error on computed key', () => { - expect(() => - compile( - `` - ) - ).toThrow(`destructure cannot use computed key`) - }) - - test('should error when used with withDefaults', () => { - expect(() => - compile( - `` - ) - ).toThrow(`withDefaults() is unnecessary when using destructure`) - }) - - test('should error if destructure reference local vars', () => { - expect(() => - compile( - `` - ) - ).toThrow(`cannot reference locally declared variables`) - }) - }) -}) diff --git a/packages/compiler-sfc/__tests__/compileStyle.spec.ts b/packages/compiler-sfc/__tests__/compileStyle.spec.ts index a343fe6b6fa..b33dabfd2ce 100644 --- a/packages/compiler-sfc/__tests__/compileStyle.spec.ts +++ b/packages/compiler-sfc/__tests__/compileStyle.spec.ts @@ -1,7 +1,3 @@ -/** - * @jest-environment node - */ - import { compileStyle, compileStyleAsync, diff --git a/packages/compiler-sfc/__tests__/compileTemplate.spec.ts b/packages/compiler-sfc/__tests__/compileTemplate.spec.ts index af0f66007c7..b471b67c9ca 100644 --- a/packages/compiler-sfc/__tests__/compileTemplate.spec.ts +++ b/packages/compiler-sfc/__tests__/compileTemplate.spec.ts @@ -22,6 +22,22 @@ test('should work', () => { expect(result.code).toMatch(`export function render(`) }) +// #6807 +test('should work with style comment', () => { + const source = ` +

+ ` + + const result = compile({ filename: 'example.vue', source }) + expect(result.errors.length).toBe(0) + expect(result.source).toBe(source) + expect(result.code).toMatch(`{"width":"300px","height":"100px"}`) +}) + test('preprocess pug', () => { const template = parse( ` @@ -153,3 +169,33 @@ test('should generate the correct imports expression', () => { expect(code).toMatch(`_ssrRenderAttr(\"src\", _imports_1)`) expect(code).toMatch(`_createVNode(\"img\", { src: _imports_1 })`) }) + +// #3874 +test('should not hoist srcset URLs in SSR mode', () => { + const { code } = compile({ + filename: 'example.vue', + source: ` + + + + + + + + + + + `, + ssr: true + }) + expect(code).toMatchSnapshot() +}) + +// #6742 +test('dynamic v-on + static v-on should merged', () => { + const source = `` + + const result = compile({ filename: 'example.vue', source }) + + expect(result.code).toMatchSnapshot() +}) diff --git a/packages/compiler-sfc/__tests__/cssVars.spec.ts b/packages/compiler-sfc/__tests__/cssVars.spec.ts index d9912f44b51..5b01d73d772 100644 --- a/packages/compiler-sfc/__tests__/cssVars.spec.ts +++ b/packages/compiler-sfc/__tests__/cssVars.spec.ts @@ -1,4 +1,4 @@ -import { compileStyle } from '../src' +import { compileStyle, parse } from '../src' import { mockId, compileSFCScript, assertCode } from './utils' describe('CSS vars injection', () => { @@ -12,7 +12,7 @@ describe('CSS vars injection', () => { ) expect(content).toMatch(`_useCssVars(_ctx => ({ "${mockId}-color": (_ctx.color), - "${mockId}-font_size": (_ctx.font.size) + "${mockId}-font\\.size": (_ctx.font.size) })`) assertCode(content) }) @@ -79,6 +79,10 @@ describe('CSS vars injection', () => { source: `.foo { color: v-bind(color); font-size: v-bind('font.size'); + + font-weight: v-bind(_φ); + font-size: v-bind(1-字号); + font-family: v-bind(フォント); }`, filename: 'test.css', id: 'data-v-test' @@ -86,7 +90,11 @@ describe('CSS vars injection', () => { expect(code).toMatchInlineSnapshot(` ".foo { color: var(--test-color); - font-size: var(--test-font_size); + font-size: var(--test-font\\\\.size); + + font-weight: var(--test-_φ); + font-size: var(--test-1-字号); + font-family: var(--test-フォント); }" `) }) @@ -225,11 +233,44 @@ describe('CSS vars injection', () => { ) expect(content).toMatch(`_useCssVars(_ctx => ({ "${mockId}-foo": (_unref(foo)), - "${mockId}-foo____px_": (_unref(foo) + 'px'), - "${mockId}-_a___b____2____px_": ((_unref(a) + _unref(b)) / 2 + 'px'), - "${mockId}-__a___b______2___a_": (((_unref(a) + _unref(b))) / (2 * _unref(a))) -})`) + "${mockId}-foo\\ \\+\\ \\'px\\'": (_unref(foo) + 'px'), + "${mockId}-\\(a\\ \\+\\ b\\)\\ \\/\\ 2\\ \\+\\ \\'px\\'": ((_unref(a) + _unref(b)) / 2 + 'px'), + "${mockId}-\\(\\(a\\ \\+\\ b\\)\\)\\ \\/\\ \\(2\\ \\*\\ a\\)": (((_unref(a) + _unref(b))) / (2 * _unref(a))) +}))`) assertCode(content) }) + + // #6022 + test('should be able to parse incomplete expressions', () => { + const { + descriptor: { cssVars } + } = parse( + ` + ` + ) + expect(cssVars).toMatchObject([`count.toString(`, `xxx`]) + }) + + // #7759 + test('It should correctly parse the case where there is no space after the script tag', () => { + const { content } = compileSFCScript( + ` + ` + ) + expect(content).toMatch( + `export default {\n setup(__props, { expose: __expose }) {\n __expose();\n\n_useCssVars(_ctx => ({\n "xxxxxxxx-background": (_unref(background))\n}))` + ) + }) }) }) diff --git a/packages/compiler-sfc/__tests__/parse.spec.ts b/packages/compiler-sfc/__tests__/parse.spec.ts index 9db9bb7f3e6..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', () => { @@ -268,7 +268,9 @@ h1 { color: red } }) test('treat custom blocks as raw text', () => { - const { errors, descriptor } = parse(` <-& `) + const { errors, descriptor } = parse( + ` <-& ` + ) expect(errors.length).toBe(0) expect(descriptor.customBlocks[0].content).toBe(` <-& `) }) @@ -309,5 +311,13 @@ h1 { color: red } ).errors.length ).toBe(0) }) + + // # 6676 + test('should throw error if no diff --git a/packages/sfc-playground/src/main.ts b/packages/sfc-playground/src/main.ts index e645bb2bd0f..713251fd81c 100644 --- a/packages/sfc-playground/src/main.ts +++ b/packages/sfc-playground/src/main.ts @@ -4,7 +4,7 @@ import '@vue/repl/style.css' // @ts-expect-error Custom window property window.VUE_DEVTOOLS_CONFIG = { - defaultSelectedAppId: 'id:repl' + defaultSelectedAppId: 'repl' } createApp(App).mount('#app') diff --git a/packages/sfc-playground/src/vue-server-renderer-dev-proxy.ts b/packages/sfc-playground/src/vue-server-renderer-dev-proxy.ts new file mode 100644 index 00000000000..f2ceb265609 --- /dev/null +++ b/packages/sfc-playground/src/vue-server-renderer-dev-proxy.ts @@ -0,0 +1,2 @@ +// serve vue/server-renderer to the iframe sandbox during dev. +export * from 'vue/server-renderer' diff --git a/packages/sfc-playground/vercel.json b/packages/sfc-playground/vercel.json new file mode 100644 index 00000000000..4511eb79d49 --- /dev/null +++ b/packages/sfc-playground/vercel.json @@ -0,0 +1,16 @@ +{ + "github": { + "silent": true + }, + "headers": [ + { + "source": "/assets/(.*)", + "headers": [ + { + "key": "Cache-Control", + "value": "max-age=31536000, immutable" + } + ] + } + ] +} diff --git a/packages/sfc-playground/vite.config.ts b/packages/sfc-playground/vite.config.ts index 4184c791817..ed76f69dcf9 100644 --- a/packages/sfc-playground/vite.config.ts +++ b/packages/sfc-playground/vite.config.ts @@ -7,7 +7,17 @@ import execa from 'execa' const commit = execa.sync('git', ['rev-parse', 'HEAD']).stdout.slice(0, 7) export default defineConfig({ - plugins: [vue(), copyVuePlugin()], + plugins: [ + vue({ + script: { + fs: { + fileExists: fs.existsSync, + readFile: file => fs.readFileSync(file, 'utf-8') + } + } + }), + copyVuePlugin() + ], define: { __COMMIT__: JSON.stringify(commit), __VUE_PROD_DEVTOOLS__: JSON.stringify(true) @@ -21,21 +31,24 @@ function copyVuePlugin(): Plugin { return { name: 'copy-vue', generateBundle() { - const filePath = path.resolve( - __dirname, - '../vue/dist/vue.runtime.esm-browser.js' - ) - if (!fs.existsSync(filePath)) { - throw new Error( - `vue.runtime.esm-browser.js not built. ` + - `Run "nr build vue -f esm-browser" first.` - ) + const copyFile = (file: string) => { + const filePath = path.resolve(__dirname, file) + const basename = path.basename(file) + if (!fs.existsSync(filePath)) { + throw new Error( + `${basename} not built. ` + + `Run "nr build vue -f esm-browser" first.` + ) + } + this.emitFile({ + type: 'asset', + fileName: basename, + source: fs.readFileSync(filePath, 'utf-8') + }) } - this.emitFile({ - type: 'asset', - fileName: 'vue.runtime.esm-browser.js', - source: fs.readFileSync(filePath, 'utf-8') - }) + + copyFile(`../vue/dist/vue.runtime.esm-browser.js`) + copyFile(`../server-renderer/dist/server-renderer.esm-browser.js`) } } } diff --git a/packages/shared/__tests__/__snapshots__/codeframe.spec.ts.snap b/packages/shared/__tests__/__snapshots__/codeframe.spec.ts.snap index 579a4507d6e..762e32694d3 100644 --- a/packages/shared/__tests__/__snapshots__/codeframe.spec.ts.snap +++ b/packages/shared/__tests__/__snapshots__/codeframe.spec.ts.snap @@ -1,6 +1,6 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`compiler: codeframe line in middle 1`] = ` +exports[`compiler: codeframe > line in middle 1`] = ` "2 | 3 |
    4 |
  • hi
  • @@ -9,7 +9,7 @@ exports[`compiler: codeframe line in middle 1`] = ` 6 | " `; -exports[`compiler: codeframe line near bottom 1`] = ` +exports[`compiler: codeframe > line near bottom 1`] = ` "4 |
  • hi
  • 5 |
6 | @@ -17,7 +17,7 @@ exports[`compiler: codeframe line near bottom 1`] = ` 7 | " `; -exports[`compiler: codeframe line near top 1`] = ` +exports[`compiler: codeframe > line near top 1`] = ` "1 |
2 | | ^^^^^^^^^ @@ -25,7 +25,7 @@ exports[`compiler: codeframe line near top 1`] = ` 4 |
  • hi
  • " `; -exports[`compiler: codeframe multi-line highlights 1`] = ` +exports[`compiler: codeframe > multi-line highlights 1`] = ` "1 |
    newline sequences - unix 1`] = ` "8 | 9 |
    10 |
    @@ -49,7 +49,7 @@ exports[`compiler: codeframe newline sequences - unix 1`] = ` | ^^^^^^^^^^^^" `; -exports[`compiler: codeframe newline sequences - windows 1`] = ` +exports[`compiler: codeframe > newline sequences - windows 1`] = ` "8 | 9 |
    10 |
    diff --git a/packages/shared/__tests__/looseEqual.spec.ts b/packages/shared/__tests__/looseEqual.spec.ts index 75bb25058b7..67b69dd76d4 100644 --- a/packages/shared/__tests__/looseEqual.spec.ts +++ b/packages/shared/__tests__/looseEqual.spec.ts @@ -1,3 +1,6 @@ +/** + * @vitest-environment jsdom + */ import { looseEqual } from '../src' describe('utils/looseEqual', () => { @@ -49,6 +52,18 @@ describe('utils/looseEqual', () => { expect(looseEqual(date1, date4)).toBe(false) }) + test('compares symbols correctly', () => { + const symbol1 = Symbol('a') + const symbol2 = Symbol('a') + const symbol3 = Symbol('b') + const notSymbol = 0 + + expect(looseEqual(symbol1, symbol1)).toBe(true) + expect(looseEqual(symbol1, symbol2)).toBe(false) + expect(looseEqual(symbol1, symbol3)).toBe(false) + expect(looseEqual(symbol1, notSymbol)).toBe(false) + }) + test('compares files correctly', () => { const date1 = new Date(2019, 1, 2, 3, 4, 5, 6) const date2 = new Date(2019, 1, 2, 3, 4, 5, 7) diff --git a/packages/shared/__tests__/normalizeProp.spec.ts b/packages/shared/__tests__/normalizeProp.spec.ts index c884d9e7281..a3cb104c003 100644 --- a/packages/shared/__tests__/normalizeProp.spec.ts +++ b/packages/shared/__tests__/normalizeProp.spec.ts @@ -1,4 +1,4 @@ -import { normalizeClass } from '../src' +import { normalizeClass, parseStringStyle } from '../src' describe('normalizeClass', () => { test('handles string correctly', () => { @@ -16,4 +16,31 @@ describe('normalizeClass', () => { 'foo baz' ) }) + + // #6777 + test('parse multi-line inline style', () => { + expect( + parseStringStyle(`border: 1px solid transparent; + background: linear-gradient(white, white) padding-box, + repeating-linear-gradient( + -45deg, + #ccc 0, + #ccc 0.5em, + white 0, + white 0.75em + );`) + ).toMatchInlineSnapshot(` + { + "background": "linear-gradient(white, white) padding-box, + repeating-linear-gradient( + -45deg, + #ccc 0, + #ccc 0.5em, + white 0, + white 0.75em + )", + "border": "1px solid transparent", + } + `) + }) }) diff --git a/packages/shared/__tests__/toDisplayString.spec.ts b/packages/shared/__tests__/toDisplayString.spec.ts index 53cfcb87a1f..5255c0e400b 100644 --- a/packages/shared/__tests__/toDisplayString.spec.ts +++ b/packages/shared/__tests__/toDisplayString.spec.ts @@ -1,3 +1,6 @@ +/** + * @vitest-environment jsdom + */ import { computed, ref } from '@vue/reactivity' import { toDisplayString } from '../src' @@ -28,11 +31,11 @@ describe('toDisplayString', () => { } expect(toDisplayString(objWithToStringOverride)).toBe('override') - const objWithNonInvokeableToString = { + const objWithNonInvokableToString = { foo: 555, toString: null } - expect(toDisplayString(objWithNonInvokeableToString)).toBe( + expect(toDisplayString(objWithNonInvokableToString)).toBe( `{ "foo": 555, "toString": null @@ -86,7 +89,7 @@ describe('toDisplayString', () => { test('native objects', () => { const div = document.createElement('div') - expect(toDisplayString(div)).toBe('[object HTMLDivElement]') + expect(toDisplayString(div)).toMatch('[object HTMLDivElement]') expect(toDisplayString({ div })).toMatchInlineSnapshot(` "{ \\"div\\": \\"[object HTMLDivElement]\\" diff --git a/packages/shared/api-extractor.json b/packages/shared/api-extractor.json deleted file mode 100644 index 5602b3a6fd2..00000000000 --- a/packages/shared/api-extractor.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../api-extractor.json", - "mainEntryPointFilePath": "./dist/packages//src/index.d.ts", - "dtsRollup": { - "publicTrimmedFilePath": "./dist/.d.ts" - } -} diff --git a/packages/shared/package.json b/packages/shared/package.json index ac9bcbf6707..836f78dc1f1 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@vue/shared", - "version": "3.2.31", + "version": "3.3.4", "description": "internal utils shared across @vue packages", "main": "index.js", "module": "dist/shared.esm-bundler.js", @@ -9,6 +9,7 @@ "index.js", "dist" ], + "sideEffects": false, "buildOptions": { "formats": [ "esm-bundler", diff --git a/packages/shared/src/domAttrConfig.ts b/packages/shared/src/domAttrConfig.ts index fb4f29a46fc..5f7f851b0df 100644 --- a/packages/shared/src/domAttrConfig.ts +++ b/packages/shared/src/domAttrConfig.ts @@ -20,7 +20,7 @@ export const isSpecialBooleanAttr = /*#__PURE__*/ makeMap(specialBooleanAttrs) export const isBooleanAttr = /*#__PURE__*/ makeMap( specialBooleanAttrs + `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,` + - `loop,open,required,reversed,scoped,seamless,` + + `inert,loop,open,required,reversed,scoped,seamless,` + `checked,muted,multiple,selected` ) @@ -53,21 +53,6 @@ export const propsToAttrMap: Record = { httpEquiv: 'http-equiv' } -/** - * CSS properties that accept plain numbers - */ -export const isNoUnitNumericStyleProp = /*#__PURE__*/ makeMap( - `animation-iteration-count,border-image-outset,border-image-slice,` + - `border-image-width,box-flex,box-flex-group,box-ordinal-group,column-count,` + - `columns,flex,flex-grow,flex-positive,flex-shrink,flex-negative,flex-order,` + - `grid-row,grid-row-end,grid-row-span,grid-row-start,grid-column,` + - `grid-column-end,grid-column-span,grid-column-start,font-weight,line-clamp,` + - `line-height,opacity,order,orphans,tab-size,widows,z-index,zoom,` + - // SVG - `fill-opacity,flood-opacity,stop-opacity,stroke-dasharray,stroke-dashoffset,` + - `stroke-miterlimit,stroke-opacity,stroke-width` -) - /** * Known attributes, this is used for stringification of runtime static nodes * so that we don't stringify bindings that cannot be set from HTML. @@ -82,7 +67,7 @@ export const isKnownHtmlAttr = /*#__PURE__*/ makeMap( `coords,crossorigin,csp,data,datetime,decoding,default,defer,dir,dirname,` + `disabled,download,draggable,dropzone,enctype,enterkeyhint,for,form,` + `formaction,formenctype,formmethod,formnovalidate,formtarget,headers,` + - `height,hidden,high,href,hreflang,http-equiv,icon,id,importance,integrity,` + + `height,hidden,high,href,hreflang,http-equiv,icon,id,importance,inert,integrity,` + `ismap,itemprop,keytype,kind,label,lang,language,loading,list,loop,low,` + `manifest,max,maxlength,minlength,media,min,multiple,muted,name,novalidate,` + `open,optimum,pattern,ping,placeholder,poster,preload,radiogroup,readonly,` + diff --git a/packages/shared/src/domTagConfig.ts b/packages/shared/src/domTagConfig.ts index 1f8605d9fc4..535aa6be718 100644 --- a/packages/shared/src/domTagConfig.ts +++ b/packages/shared/src/domTagConfig.ts @@ -5,7 +5,7 @@ import { makeMap } from './makeMap' // https://developer.mozilla.org/en-US/docs/Web/HTML/Element const HTML_TAGS = 'html,body,base,head,link,meta,style,title,address,article,aside,footer,' + - 'header,h1,h2,h3,h4,h5,h6,nav,section,div,dd,dl,dt,figcaption,' + + 'header,hgroup,h1,h2,h3,h4,h5,h6,nav,section,div,dd,dl,dt,figcaption,' + 'figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,' + 'data,dfn,em,i,kbd,mark,q,rp,rt,ruby,s,samp,small,span,strong,sub,sup,' + 'time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,' + @@ -19,7 +19,7 @@ const SVG_TAGS = 'svg,animate,animateMotion,animateTransform,circle,clipPath,color-profile,' + 'defs,desc,discard,ellipse,feBlend,feColorMatrix,feComponentTransfer,' + 'feComposite,feConvolveMatrix,feDiffuseLighting,feDisplacementMap,' + - 'feDistanceLight,feDropShadow,feFlood,feFuncA,feFuncB,feFuncG,feFuncR,' + + 'feDistantLight,feDropShadow,feFlood,feFuncA,feFuncB,feFuncG,feFuncR,' + 'feGaussianBlur,feImage,feMerge,feMergeNode,feMorphology,feOffset,' + 'fePointLight,feSpecularLighting,feSpotLight,feTile,feTurbulence,filter,' + 'foreignObject,g,hatch,hatchpath,image,line,linearGradient,marker,mask,' + diff --git a/packages/shared/src/general.ts b/packages/shared/src/general.ts new file mode 100644 index 00000000000..6efaf52524f --- /dev/null +++ b/packages/shared/src/general.ts @@ -0,0 +1,183 @@ +import { makeMap } from './makeMap' + +export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__ + ? Object.freeze({}) + : {} +export const EMPTY_ARR = __DEV__ ? Object.freeze([]) : [] + +export const NOOP = () => {} + +/** + * Always return false. + */ +export const NO = () => false + +const onRE = /^on[^a-z]/ +export const isOn = (key: string) => onRE.test(key) + +export const isModelListener = (key: string) => key.startsWith('onUpdate:') + +export const extend = Object.assign + +export const remove = (arr: T[], el: T) => { + const i = arr.indexOf(el) + if (i > -1) { + arr.splice(i, 1) + } +} + +const hasOwnProperty = Object.prototype.hasOwnProperty +export const hasOwn = ( + val: object, + key: string | symbol +): key is keyof typeof val => hasOwnProperty.call(val, key) + +export const isArray = Array.isArray +export const isMap = (val: unknown): val is Map => + toTypeString(val) === '[object Map]' +export const isSet = (val: unknown): val is Set => + toTypeString(val) === '[object Set]' + +export const isDate = (val: unknown): val is Date => + toTypeString(val) === '[object Date]' +export const isRegExp = (val: unknown): val is RegExp => + toTypeString(val) === '[object RegExp]' +export const isFunction = (val: unknown): val is Function => + typeof val === 'function' +export const isString = (val: unknown): val is string => typeof val === 'string' +export const isSymbol = (val: unknown): val is symbol => typeof val === 'symbol' +export const isObject = (val: unknown): val is Record => + val !== null && typeof val === 'object' + +export const isPromise = (val: unknown): val is Promise => { + return isObject(val) && isFunction(val.then) && isFunction(val.catch) +} + +export const objectToString = Object.prototype.toString +export const toTypeString = (value: unknown): string => + objectToString.call(value) + +export const toRawType = (value: unknown): string => { + // extract "RawType" from strings like "[object RawType]" + return toTypeString(value).slice(8, -1) +} + +export const isPlainObject = (val: unknown): val is object => + toTypeString(val) === '[object Object]' + +export const isIntegerKey = (key: unknown) => + isString(key) && + key !== 'NaN' && + key[0] !== '-' && + '' + parseInt(key, 10) === key + +export const isReservedProp = /*#__PURE__*/ makeMap( + // the leading comma is intentional so empty string "" is also included + ',key,ref,ref_for,ref_key,' + + 'onVnodeBeforeMount,onVnodeMounted,' + + 'onVnodeBeforeUpdate,onVnodeUpdated,' + + 'onVnodeBeforeUnmount,onVnodeUnmounted' +) + +export const isBuiltInDirective = /*#__PURE__*/ makeMap( + 'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo' +) + +const cacheStringFunction = string>(fn: T): T => { + const cache: Record = Object.create(null) + return ((str: string) => { + const hit = cache[str] + return hit || (cache[str] = fn(str)) + }) as T +} + +const camelizeRE = /-(\w)/g +/** + * @private + */ +export const camelize = cacheStringFunction((str: string): string => { + return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : '')) +}) + +const hyphenateRE = /\B([A-Z])/g +/** + * @private + */ +export const hyphenate = cacheStringFunction((str: string) => + str.replace(hyphenateRE, '-$1').toLowerCase() +) + +/** + * @private + */ +export const capitalize = cacheStringFunction( + (str: string) => str.charAt(0).toUpperCase() + str.slice(1) +) + +/** + * @private + */ +export const toHandlerKey = cacheStringFunction((str: string) => + str ? `on${capitalize(str)}` : `` +) + +// compare whether a value has changed, accounting for NaN. +export const hasChanged = (value: any, oldValue: any): boolean => + !Object.is(value, oldValue) + +export const invokeArrayFns = (fns: Function[], arg?: any) => { + for (let i = 0; i < fns.length; i++) { + fns[i](arg) + } +} + +export const def = (obj: object, key: string | symbol, value: any) => { + Object.defineProperty(obj, key, { + configurable: true, + enumerable: false, + value + }) +} + +/** + * "123-foo" will be parsed to 123 + * This is used for the .number modifier in v-model + */ +export const looseToNumber = (val: any): any => { + const n = parseFloat(val) + return isNaN(n) ? val : n +} + +/** + * Only conerces number-like strings + * "123-foo" will be returned as-is + */ +export const toNumber = (val: any): any => { + const n = isString(val) ? Number(val) : NaN + return isNaN(n) ? val : n +} + +let _globalThis: any +export const getGlobalThis = (): any => { + return ( + _globalThis || + (_globalThis = + typeof globalThis !== 'undefined' + ? globalThis + : typeof self !== 'undefined' + ? self + : typeof window !== 'undefined' + ? window + : typeof global !== 'undefined' + ? global + : {}) + ) +} + +const identRE = /^[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*$/ + +export function genPropsAccessExp(name: string) { + return identRE.test(name) + ? `__props.${name}` + : `__props[${JSON.stringify(name)}]` +} diff --git a/packages/shared/src/globalsWhitelist.ts b/packages/shared/src/globalsWhitelist.ts index f383450c88d..9485a41dafa 100644 --- a/packages/shared/src/globalsWhitelist.ts +++ b/packages/shared/src/globalsWhitelist.ts @@ -3,6 +3,6 @@ import { makeMap } from './makeMap' const GLOBALS_WHITE_LISTED = 'Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,' + 'decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,' + - 'Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt' + 'Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console' export const isGloballyWhitelisted = /*#__PURE__*/ makeMap(GLOBALS_WHITE_LISTED) diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index e47cb5cd673..2e7292f0eac 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,6 +1,5 @@ -import { makeMap } from './makeMap' - -export { makeMap } +export { makeMap } from './makeMap' +export * from './general' export * from './patchFlags' export * from './shapeFlags' export * from './slotFlags' @@ -13,161 +12,3 @@ export * from './escapeHtml' export * from './looseEqual' export * from './toDisplayString' export * from './typeUtils' - -export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__ - ? Object.freeze({}) - : {} -export const EMPTY_ARR = __DEV__ ? Object.freeze([]) : [] - -export const NOOP = () => {} - -/** - * Always return false. - */ -export const NO = () => false - -const onRE = /^on[^a-z]/ -export const isOn = (key: string) => onRE.test(key) - -export const isModelListener = (key: string) => key.startsWith('onUpdate:') - -export const extend = Object.assign - -export const remove = (arr: T[], el: T) => { - const i = arr.indexOf(el) - if (i > -1) { - arr.splice(i, 1) - } -} - -const hasOwnProperty = Object.prototype.hasOwnProperty -export const hasOwn = ( - val: object, - key: string | symbol -): key is keyof typeof val => hasOwnProperty.call(val, key) - -export const isArray = Array.isArray -export const isMap = (val: unknown): val is Map => - toTypeString(val) === '[object Map]' -export const isSet = (val: unknown): val is Set => - toTypeString(val) === '[object Set]' - -export const isDate = (val: unknown): val is Date => val instanceof Date -export const isFunction = (val: unknown): val is Function => - typeof val === 'function' -export const isString = (val: unknown): val is string => typeof val === 'string' -export const isSymbol = (val: unknown): val is symbol => typeof val === 'symbol' -export const isObject = (val: unknown): val is Record => - val !== null && typeof val === 'object' - -export const isPromise = (val: unknown): val is Promise => { - return isObject(val) && isFunction(val.then) && isFunction(val.catch) -} - -export const objectToString = Object.prototype.toString -export const toTypeString = (value: unknown): string => - objectToString.call(value) - -export const toRawType = (value: unknown): string => { - // extract "RawType" from strings like "[object RawType]" - return toTypeString(value).slice(8, -1) -} - -export const isPlainObject = (val: unknown): val is object => - toTypeString(val) === '[object Object]' - -export const isIntegerKey = (key: unknown) => - isString(key) && - key !== 'NaN' && - key[0] !== '-' && - '' + parseInt(key, 10) === key - -export const isReservedProp = /*#__PURE__*/ makeMap( - // the leading comma is intentional so empty string "" is also included - ',key,ref,ref_for,ref_key,' + - 'onVnodeBeforeMount,onVnodeMounted,' + - 'onVnodeBeforeUpdate,onVnodeUpdated,' + - 'onVnodeBeforeUnmount,onVnodeUnmounted' -) - -export const isBuiltInDirective = /*#__PURE__*/ makeMap( - 'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo' -) - -const cacheStringFunction = string>(fn: T): T => { - const cache: Record = Object.create(null) - return ((str: string) => { - const hit = cache[str] - return hit || (cache[str] = fn(str)) - }) as any -} - -const camelizeRE = /-(\w)/g -/** - * @private - */ -export const camelize = cacheStringFunction((str: string): string => { - return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : '')) -}) - -const hyphenateRE = /\B([A-Z])/g -/** - * @private - */ -export const hyphenate = cacheStringFunction((str: string) => - str.replace(hyphenateRE, '-$1').toLowerCase() -) - -/** - * @private - */ -export const capitalize = cacheStringFunction( - (str: string) => str.charAt(0).toUpperCase() + str.slice(1) -) - -/** - * @private - */ -export const toHandlerKey = cacheStringFunction((str: string) => - str ? `on${capitalize(str)}` : `` -) - -// compare whether a value has changed, accounting for NaN. -export const hasChanged = (value: any, oldValue: any): boolean => - !Object.is(value, oldValue) - -export const invokeArrayFns = (fns: Function[], arg?: any) => { - for (let i = 0; i < fns.length; i++) { - fns[i](arg) - } -} - -export const def = (obj: object, key: string | symbol, value: any) => { - Object.defineProperty(obj, key, { - configurable: true, - enumerable: false, - value - }) -} - -export const toNumber = (val: any): any => { - const n = parseFloat(val) - return isNaN(n) ? val : n -} - -let _globalThis: any -export const getGlobalThis = (): any => { - return ( - _globalThis || - (_globalThis = - typeof globalThis !== 'undefined' - ? globalThis - : typeof self !== 'undefined' - ? self - : typeof window !== 'undefined' - ? window - : typeof global !== 'undefined' - ? global - : {}) - ) -} diff --git a/packages/shared/src/looseEqual.ts b/packages/shared/src/looseEqual.ts index 030f0338b30..db869922fc1 100644 --- a/packages/shared/src/looseEqual.ts +++ b/packages/shared/src/looseEqual.ts @@ -1,4 +1,4 @@ -import { isArray, isDate, isObject } from './' +import { isArray, isDate, isObject, isSymbol } from './general' function looseCompareArrays(a: any[], b: any[]) { if (a.length !== b.length) return false @@ -16,6 +16,11 @@ export function looseEqual(a: any, b: any): boolean { if (aValidType || bValidType) { return aValidType && bValidType ? a.getTime() === b.getTime() : false } + aValidType = isSymbol(a) + bValidType = isSymbol(b) + if (aValidType || bValidType) { + return a === b + } aValidType = isArray(a) bValidType = isArray(b) if (aValidType || bValidType) { diff --git a/packages/shared/src/normalizeProp.ts b/packages/shared/src/normalizeProp.ts index cab8090692e..6a1dd20e393 100644 --- a/packages/shared/src/normalizeProp.ts +++ b/packages/shared/src/normalizeProp.ts @@ -1,5 +1,4 @@ -import { isArray, isString, isObject, hyphenate } from './' -import { isNoUnitNumericStyleProp } from './domAttrConfig' +import { isArray, isString, isObject, hyphenate } from './general' export type NormalizedStyle = Record @@ -28,16 +27,20 @@ export function normalizeStyle( } const listDelimiterRE = /;(?![^(]*\))/g -const propertyDelimiterRE = /:(.+)/ +const propertyDelimiterRE = /:([^]+)/ +const styleCommentRE = /\/\*[^]*?\*\//g export function parseStringStyle(cssText: string): NormalizedStyle { const ret: NormalizedStyle = {} - cssText.split(listDelimiterRE).forEach(item => { - if (item) { - const tmp = item.split(propertyDelimiterRE) - tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim()) - } - }) + cssText + .replace(styleCommentRE, '') + .split(listDelimiterRE) + .forEach(item => { + if (item) { + const tmp = item.split(propertyDelimiterRE) + tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim()) + } + }) return ret } @@ -51,10 +54,7 @@ export function stringifyStyle( for (const key in styles) { const value = styles[key] const normalizedKey = key.startsWith(`--`) ? key : hyphenate(key) - if ( - isString(value) || - (typeof value === 'number' && isNoUnitNumericStyleProp(normalizedKey)) - ) { + if (isString(value) || typeof value === 'number') { // only render valid values ret += `${normalizedKey}:${value};` } diff --git a/packages/shared/src/patchFlags.ts b/packages/shared/src/patchFlags.ts index 8a5ca2faada..58e8935aeb8 100644 --- a/packages/shared/src/patchFlags.ts +++ b/packages/shared/src/patchFlags.ts @@ -125,7 +125,7 @@ export const enum PatchFlags { /** * dev only flag -> name mapping */ -export const PatchFlagNames = { +export const PatchFlagNames: Record = { [PatchFlags.TEXT]: `TEXT`, [PatchFlags.CLASS]: `CLASS`, [PatchFlags.STYLE]: `STYLE`, diff --git a/packages/shared/src/toDisplayString.ts b/packages/shared/src/toDisplayString.ts index efe3b7cc0e8..7f5818d9491 100644 --- a/packages/shared/src/toDisplayString.ts +++ b/packages/shared/src/toDisplayString.ts @@ -7,7 +7,7 @@ import { isSet, objectToString, isString -} from './index' +} from './general' /** * For converting {{ interpolation }} values to displayed strings. diff --git a/packages/shared/src/typeUtils.ts b/packages/shared/src/typeUtils.ts index 8caba54c6ca..67fb47c23b3 100644 --- a/packages/shared/src/typeUtils.ts +++ b/packages/shared/src/typeUtils.ts @@ -1,3 +1,5 @@ +export type Prettify = { [K in keyof T]: T[K] } & {} + export type UnionToIntersection = ( U extends any ? (k: U) => void : never ) extends (k: infer I) => void @@ -5,6 +7,8 @@ export type UnionToIntersection = ( : never // make keys required but keep undefined values -export type LooseRequired = { [P in string & keyof T]: T[P] } +export type LooseRequired = { [P in keyof (T & Required)]: T[P] } -export type IfAny = 0 extends (1 & T) ? Y : N +// If the type T accepts type "any", output type Y, otherwise output type N. +// https://stackoverflow.com/questions/49927523/disallow-call-with-any/49928360#49928360 +export type IfAny = 0 extends 1 & T ? Y : N diff --git a/packages/size-check/brotli.js b/packages/size-check/brotli.js index 1e7ea0c774b..f9dedac0b1c 100644 --- a/packages/size-check/brotli.js +++ b/packages/size-check/brotli.js @@ -1,6 +1,6 @@ -const { compress } = require('brotli') +const { brotliCompressSync } = require('zlib') const file = require('fs').readFileSync('dist/index.js') -const compressed = compress(file) +const compressed = brotliCompressSync(file) const compressedSize = (compressed.length / 1024).toFixed(2) + 'kb' console.log(`brotli: ${compressedSize}`) diff --git a/packages/size-check/package.json b/packages/size-check/package.json index 01ad0812074..1f9fba88594 100644 --- a/packages/size-check/package.json +++ b/packages/size-check/package.json @@ -1,8 +1,11 @@ { "name": "@vue/size-check", - "version": "3.2.31", + "version": "3.3.4", "private": true, "scripts": { "build": "vite build" + }, + "dependencies": { + "vue": "workspace:*" } } diff --git a/packages/size-check/src/index.ts b/packages/size-check/src/index.ts index 3c9d23c278a..ad3b68a5cc1 100644 --- a/packages/size-check/src/index.ts +++ b/packages/size-check/src/index.ts @@ -1,4 +1,4 @@ -import { h, createApp } from '@vue/runtime-dom' +import { h, createApp } from 'vue' // The bare minimum code required for rendering something to the screen createApp({ diff --git a/packages/size-check/vite.config.js b/packages/size-check/vite.config.js index b8d714e0f2a..73721f95910 100644 --- a/packages/size-check/vite.config.js +++ b/packages/size-check/vite.config.js @@ -1,4 +1,8 @@ export default { + define: { + __VUE_PROD_DEVTOOLS__: false, + __VUE_OPTIONS_API__: true + }, build: { rollupOptions: { input: ['src/index.ts'], diff --git a/packages/template-explorer/_redirects b/packages/template-explorer/_redirects new file mode 100644 index 00000000000..9d570fb8259 --- /dev/null +++ b/packages/template-explorer/_redirects @@ -0,0 +1,2 @@ +https://vue-next-template-explorer.netlify.app https://template-explorer.vuejs.org 301! +https://vue-next-template-explorer.netlify.app/* https://template-explorer.vuejs.org/:splat 301! diff --git a/packages/template-explorer/package.json b/packages/template-explorer/package.json index 35eadc58af7..320b4c05ed3 100644 --- a/packages/template-explorer/package.json +++ b/packages/template-explorer/package.json @@ -1,6 +1,6 @@ { "name": "@vue/template-explorer", - "version": "3.2.31", + "version": "3.3.4", "private": true, "buildOptions": { "formats": [ @@ -12,6 +12,6 @@ }, "dependencies": { "monaco-editor": "^0.20.0", - "source-map": "^0.6.1" + "source-map-js": "^1.0.2" } } diff --git a/packages/template-explorer/src/index.ts b/packages/template-explorer/src/index.ts index d676c717373..bace011ed89 100644 --- a/packages/template-explorer/src/index.ts +++ b/packages/template-explorer/src/index.ts @@ -8,7 +8,7 @@ import { ssrMode } from './options' import { toRaw, watchEffect } from '@vue/runtime-dom' -import { SourceMapConsumer } from 'source-map' +import { SourceMapConsumer } from 'source-map-js' import theme from './theme' declare global { @@ -275,5 +275,5 @@ function debounce any>( fn(...args) prevTimer = null }, delay) - }) as any + }) as T } diff --git a/packages/vue-compat/README.md b/packages/vue-compat/README.md index 01bd71643a8..23e4ce9ebcc 100644 --- a/packages/vue-compat/README.md +++ b/packages/vue-compat/README.md @@ -281,7 +281,7 @@ Features that start with `COMPILER_` are compiler-specific: if you are using the | ID | Type | Description | Docs | | ------------------ | ---- | ------------------------------------- | ---------------------------------------- | -| TRANSITION_CLASSES | ⭘ | Transtion enter/leave classes changed | [link](https://v3-migration.vuejs.org/breaking-changes/transition.html) | +| TRANSITION_CLASSES | ⭘ | Transition enter/leave classes changed | [link](https://v3-migration.vuejs.org/breaking-changes/transition.html) | ### Fully Compatible @@ -308,11 +308,10 @@ Features that start with `COMPILER_` are compiler-specific: if you are using the | OPTIONS_BEFORE_DESTROY | ✔ | `beforeDestroy` -> `beforeUnmount` | | | OPTIONS_DESTROYED | ✔ | `destroyed` -> `unmounted` | | | WATCH_ARRAY | ✔ | watching an array no longer triggers on mutation unless deep | [link](https://v3-migration.vuejs.org/breaking-changes/watch.html) | -| V_FOR_REF | ✔ | `ref` inside `v-for` no longer registers array of refs | [link](https://v3-migration.vuejs.org/breaking-changes/array-refs.html) | | V_ON_KEYCODE_MODIFIER | ✔ | `v-on` no longer supports keyCode modifiers | [link](https://v3-migration.vuejs.org/breaking-changes/keycode-modifiers.html) | | CUSTOM_DIR | ✔ | Custom directive hook names changed | [link](https://v3-migration.vuejs.org/breaking-changes/custom-directives.html) | | ATTR_FALSE_VALUE | ✔ | No longer removes attribute if binding value is boolean `false` | [link](https://v3-migration.vuejs.org/breaking-changes/attribute-coercion.html) | -| ATTR_ENUMERATED_COERSION | ✔ | No longer special case enumerated attributes | [link](https://v3-migration.vuejs.org/breaking-changes/attribute-coercion.html) | +| ATTR_ENUMERATED_COERCION | ✔ | No longer special case enumerated attributes | [link](https://v3-migration.vuejs.org/breaking-changes/attribute-coercion.html) | | TRANSITION_GROUP_ROOT | ✔ | `` no longer renders a root element by default | [link](https://v3-migration.vuejs.org/breaking-changes/transition-group.html) | | COMPONENT_ASYNC | ✔ | Async component API changed (now requires `defineAsyncComponent`) | [link](https://v3-migration.vuejs.org/breaking-changes/async-components.html) | | COMPONENT_FUNCTIONAL | ✔ | Functional component API changed (now must be plain functions) | [link](https://v3-migration.vuejs.org/breaking-changes/functional-components.html) | diff --git a/packages/vue-compat/__tests__/compiler.spec.ts b/packages/vue-compat/__tests__/compiler.spec.ts index 2b13233e3e6..88de3d20f3b 100644 --- a/packages/vue-compat/__tests__/compiler.spec.ts +++ b/packages/vue-compat/__tests__/compiler.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest' import Vue from '@vue/compat' import { nextTick } from '@vue/runtime-core' import { CompilerDeprecationTypes } from '../../compiler-core/src' @@ -31,6 +32,7 @@ test('COMPILER_IS_ON_ELEMENT', () => { } }).$mount() + expect(vm.$el).toBeInstanceOf(HTMLDivElement) expect(vm.$el.outerHTML).toBe(`
    text
    `) expect(CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT).toHaveBeenWarned() }) @@ -47,6 +49,7 @@ test('COMPILER_IS_ON_ELEMENT (dynamic)', () => { } }).$mount() + expect(vm.$el).toBeInstanceOf(HTMLDivElement) expect(vm.$el.outerHTML).toBe(`
    text
    `) expect(CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT).toHaveBeenWarned() }) @@ -69,9 +72,10 @@ test('COMPILER_V_BIND_SYNC', async () => { } }).$mount() + expect(vm.$el).toBeInstanceOf(HTMLButtonElement) expect(vm.$el.textContent).toBe(`0`) - triggerEvent(vm.$el, 'click') + triggerEvent(vm.$el as Element, 'click') await nextTick() expect(vm.$el.textContent).toBe(`1`) @@ -82,6 +86,8 @@ test('COMPILER_V_BIND_PROP', () => { const vm = new Vue({ template: `
    ` }).$mount() + + expect(vm.$el).toBeInstanceOf(HTMLDivElement) expect(vm.$el.id).toBe('foo') expect(CompilerDeprecationTypes.COMPILER_V_BIND_PROP).toHaveBeenWarned() }) @@ -90,6 +96,7 @@ test('COMPILER_V_BIND_OBJECT_ORDER', () => { const vm = new Vue({ template: `
    ` }).$mount() + expect(vm.$el).toBeInstanceOf(HTMLDivElement) expect(vm.$el.id).toBe('foo') expect(vm.$el.className).toBe('baz') expect( @@ -98,7 +105,7 @@ test('COMPILER_V_BIND_OBJECT_ORDER', () => { }) test('COMPILER_V_ON_NATIVE', () => { - const spy = jest.fn() + const spy = vi.fn() const vm = new Vue({ template: ``, components: { @@ -111,7 +118,8 @@ test('COMPILER_V_ON_NATIVE', () => { } }).$mount() - triggerEvent(vm.$el, 'click') + expect(vm.$el).toBeInstanceOf(HTMLButtonElement) + triggerEvent(vm.$el as HTMLButtonElement, 'click') expect(spy).toHaveBeenCalledTimes(1) expect(CompilerDeprecationTypes.COMPILER_V_ON_NATIVE).toHaveBeenWarned() }) @@ -127,6 +135,8 @@ test('COMPILER_NATIVE_TEMPLATE', () => { const vm = new Vue({ template: `
    ` }).$mount() + + expect(vm.$el).toBeInstanceOf(HTMLDivElement) expect(vm.$el.innerHTML).toBe(`
    `) expect(CompilerDeprecationTypes.COMPILER_NATIVE_TEMPLATE).toHaveBeenWarned() }) @@ -143,6 +153,7 @@ test('COMPILER_INLINE_TEMPLATE', () => { } }).$mount() - expect(vm.$el.outerHTML).toBe(`
    123
    `) + expect(vm.$el).toBeInstanceOf(HTMLDivElement) + expect(vm.$el?.outerHTML).toBe(`
    123
    `) expect(CompilerDeprecationTypes.COMPILER_INLINE_TEMPLATE).toHaveBeenWarned() }) diff --git a/packages/vue-compat/__tests__/componentAsync.spec.ts b/packages/vue-compat/__tests__/componentAsync.spec.ts index 9e7316a6ce4..8fda196a099 100644 --- a/packages/vue-compat/__tests__/componentAsync.spec.ts +++ b/packages/vue-compat/__tests__/componentAsync.spec.ts @@ -30,6 +30,8 @@ describe('COMPONENT_ASYNC', () => { template: `
    `, components: { comp } }).$mount() + + expect(vm.$el).toBeInstanceOf(HTMLDivElement) expect(vm.$el.innerHTML).toBe(``) resolve({ template: 'foo' }) @@ -49,6 +51,7 @@ describe('COMPONENT_ASYNC', () => { template: `
    `, components: { comp } }).$mount() + expect(vm.$el).toBeInstanceOf(HTMLDivElement) expect(vm.$el.innerHTML).toBe(``) await timeout(0) expect(vm.$el.innerHTML).toBe(`foo`) @@ -69,6 +72,8 @@ describe('COMPONENT_ASYNC', () => { template: `
    `, components: { comp } }).$mount() + + expect(vm.$el).toBeInstanceOf(HTMLDivElement) expect(vm.$el.innerHTML).toBe(``) await timeout(0) expect(vm.$el.innerHTML).toBe(`foo`) diff --git a/packages/vue-compat/__tests__/componentFunctional.spec.ts b/packages/vue-compat/__tests__/componentFunctional.spec.ts index e17534ce25c..8ee0b3cd96b 100644 --- a/packages/vue-compat/__tests__/componentFunctional.spec.ts +++ b/packages/vue-compat/__tests__/componentFunctional.spec.ts @@ -52,7 +52,7 @@ describe('COMPONENT_FUNCTIONAL', () => { expect(vm.$el.querySelector('.inject').textContent).toBe('123') expect(vm.$el.querySelector('.slot').textContent).toBe('hello') expect(vm.$el.outerHTML).toMatchInlineSnapshot( - `"
    123
    hello
    "` + '"
    123
    hello
    "' ) expect( diff --git a/packages/vue-compat/__tests__/filters.spec.ts b/packages/vue-compat/__tests__/filters.spec.ts index 819ccc5d3ef..c1acbd899bc 100644 --- a/packages/vue-compat/__tests__/filters.spec.ts +++ b/packages/vue-compat/__tests__/filters.spec.ts @@ -43,6 +43,7 @@ describe('FILTERS', () => { msg: 'hi' }) }).$mount() + expect(vm.$el).toBeInstanceOf(HTMLDivElement) expect(vm.$el.textContent).toBe('HI') expect(deprecationData[DeprecationTypes.FILTERS].message).toHaveBeenWarned() expect(CompilerDeprecationTypes.COMPILER_FILTERS).toHaveBeenWarned() @@ -115,7 +116,7 @@ describe('FILTERS', () => { } } }).$mount() as any - expect(vm.$refs.test.pattern instanceof RegExp).toBe(true) + expect(vm.$refs.test.pattern).toBeInstanceOf(RegExp) expect(vm.$refs.test.pattern.toString()).toBe('/a|b\\//') expect(CompilerDeprecationTypes.COMPILER_FILTERS).toHaveBeenWarned() }) diff --git a/packages/vue-compat/__tests__/global.spec.ts b/packages/vue-compat/__tests__/global.spec.ts index 86bb4391092..d189d65f67b 100644 --- a/packages/vue-compat/__tests__/global.spec.ts +++ b/packages/vue-compat/__tests__/global.spec.ts @@ -1,6 +1,7 @@ +import { expect, vi } from 'vitest' import Vue from '@vue/compat' import { effect, isReactive } from '@vue/reactivity' -import { nextTick } from '@vue/runtime-core' +import { h, nextTick } from '@vue/runtime-core' import { DeprecationTypes, deprecationData, @@ -121,6 +122,7 @@ describe('GLOBAL_EXTEND', () => { template: '
    ', components: { foo, bar } }).$mount() + expect(vm.$el).toBeInstanceOf(HTMLDivElement) expect(vm.$el.innerHTML).toBe('foobar') }) @@ -145,10 +147,10 @@ describe('GLOBAL_EXTEND', () => { }) it('should not merge nested mixins created with Vue.extend', () => { - const a = jest.fn() - const b = jest.fn() - const c = jest.fn() - const d = jest.fn() + const a = vi.fn() + const b = vi.fn() + const c = vi.fn() + const d = vi.fn() const A = Vue.extend({ created: a }) @@ -220,6 +222,7 @@ describe('GLOBAL_EXTEND', () => { const b = new B({ template: '
    ' }).$mount() + expect(b.$el).toBeInstanceOf(HTMLDivElement) expect(b.$el.innerHTML).toBe('
    A
    B
    ') }) @@ -285,6 +288,28 @@ describe('GLOBAL_PROTOTYPE', () => { delete Vue.prototype.$test }) + test('functions keeps additional properties', () => { + function test(this: any) { + return this.msg + } + test.additionalFn = () => { + return 'additional fn' + } + + Vue.prototype.$test = test + const vm = new Vue({ + data() { + return { + msg: 'test' + } + } + }) as any + expect(typeof vm.$test).toBe('function') + expect(typeof vm.$test.additionalFn).toBe('function') + expect(vm.$test.additionalFn()).toBe('additional fn') + delete Vue.prototype.$test + }) + test('extended prototype', async () => { const Foo = Vue.extend() Foo.prototype.$test = 1 @@ -365,6 +390,7 @@ describe('GLOBAL_PRIVATE_UTIL', () => { }, template: `
    {{ foo }}
    ` }).$mount() as any + expect(vm.$el).toBeInstanceOf(HTMLDivElement) expect(vm.$el.textContent).toBe('1') vm.foo = 2 await nextTick() @@ -426,3 +452,57 @@ test('global asset registration should affect apps created via createApp', () => expect(vm.$el.textContent).toBe('foo') delete singletonApp._context.components.foo }) + +test('post-facto global asset registration should affect apps created via createApp', () => { + const app = createApp({ + template: '' + }) + Vue.component('foo', { template: 'foo' }) + const vm = app.mount(document.createElement('div')) + expect(vm.$el.textContent).toBe('foo') + delete singletonApp._context.components.foo +}) + +test('local asset registration should not affect other local apps', () => { + const app1 = createApp({}) + const app2 = createApp({}) + + app1.component('foo', {}) + app2.component('foo', {}) + + expect( + `Component "foo" has already been registered in target app` + ).not.toHaveBeenWarned() +}) + +test('local app-level mixin registration should not affect other local apps', () => { + const app1 = createApp({ render: () => h('div') }) + const app2 = createApp({}) + + const mixin = { created: vi.fn() } + app1.mixin(mixin) + app2.mixin(mixin) + + expect(`Mixin has already been applied`).not.toHaveBeenWarned() + + app1.mount(document.createElement('div')) + expect(mixin.created).toHaveBeenCalledTimes(1) +}) + +// #5699 +test('local app config should not affect other local apps in v3 mode', () => { + Vue.configureCompat({ MODE: 3 }) + const app1 = createApp({ + render: () => h('div'), + provide() { + return { + test: 123 + } + } + }) + app1.config.globalProperties.test = () => {} + app1.mount(document.createElement('div')) + + const app2 = createApp({}) + expect(app2.config.globalProperties.test).toBe(undefined) +}) diff --git a/packages/vue-compat/__tests__/globalConfig.spec.ts b/packages/vue-compat/__tests__/globalConfig.spec.ts index f2aa27d4ec8..2a3adddba38 100644 --- a/packages/vue-compat/__tests__/globalConfig.spec.ts +++ b/packages/vue-compat/__tests__/globalConfig.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest' import Vue from '@vue/compat' import { DeprecationTypes, @@ -24,8 +25,8 @@ test('GLOBAL_KEY_CODES', () => { bar: [38, 87] } - const onFoo = jest.fn() - const onBar = jest.fn() + const onFoo = vi.fn() + const onBar = vi.fn() const el = document.createElement('div') new Vue({ diff --git a/packages/vue-compat/__tests__/instance.spec.ts b/packages/vue-compat/__tests__/instance.spec.ts index b6de7f24bbe..abcd3d1fab0 100644 --- a/packages/vue-compat/__tests__/instance.spec.ts +++ b/packages/vue-compat/__tests__/instance.spec.ts @@ -1,3 +1,4 @@ +import { vi, Mock } from 'vitest' import Vue from '@vue/compat' import { Slots } from '../../runtime-core/src/componentSlots' import { Text } from '../../runtime-core/src/vnode' @@ -49,18 +50,18 @@ test('INSTANCE_DESTROY', () => { // https://github.com/vuejs/vue/blob/dev/test/unit/features/instance/methods-events.spec.js describe('INSTANCE_EVENT_EMITTER', () => { let vm: LegacyPublicInstance - let spy: jest.Mock + let spy: Mock beforeEach(() => { vm = new Vue() - spy = jest.fn() + spy = vi.fn() }) it('$on', () => { vm.$on('test', function (this: any) { // expect correct context expect(this).toBe(vm) - spy.apply(this, arguments) + spy.apply(this, arguments as unknown as any[]) }) vm.$emit('test', 1, 2, 3, 4) expect(spy).toHaveBeenCalledTimes(1) @@ -73,7 +74,7 @@ describe('INSTANCE_EVENT_EMITTER', () => { it('$on multi event', () => { vm.$on(['test1', 'test2'], function (this: any) { expect(this).toBe(vm) - spy.apply(this, arguments) + spy.apply(this, arguments as unknown as any[]) }) vm.$emit('test1', 1, 2, 3, 4) expect(spy).toHaveBeenCalledTimes(1) @@ -157,7 +158,7 @@ describe('INSTANCE_EVENT_EMITTER', () => { }) it('$off event + fn', () => { - const spy2 = jest.fn() + const spy2 = vi.fn() vm.$on('test', spy) vm.$on('test', spy2) vm.$off('test', spy) @@ -173,7 +174,7 @@ describe('INSTANCE_EVENT_EMITTER', () => { describe('INSTANCE_EVENT_HOOKS', () => { test('instance API', () => { - const spy = jest.fn() + const spy = vi.fn() const vm = new Vue({ template: 'foo' }) vm.$on('hook:mounted', spy) vm.$mount() @@ -187,7 +188,7 @@ describe('INSTANCE_EVENT_HOOKS', () => { }) test('via template', () => { - const spy = jest.fn() + const spy = vi.fn() new Vue({ template: ``, methods: { spy }, @@ -318,6 +319,7 @@ test('INSTANCE_ATTR_CLASS_STYLE', () => { } }).$mount() + expect(vm.$el).toBeInstanceOf(HTMLDivElement) expect(vm.$el.outerHTML).toBe( `
    ` ) diff --git a/packages/vue-compat/__tests__/misc.spec.ts b/packages/vue-compat/__tests__/misc.spec.ts index a788fccb4a6..5d16ae2907a 100644 --- a/packages/vue-compat/__tests__/misc.spec.ts +++ b/packages/vue-compat/__tests__/misc.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest' import Vue from '@vue/compat' import { nextTick } from '../../runtime-core/src/scheduler' import { @@ -43,11 +44,12 @@ test('mode as function', () => { template: `
    ` }).$mount() + expect(vm.$el).toBeInstanceOf(HTMLDivElement) expect(vm.$el.innerHTML).toBe(`
    foo
    bar
    `) }) test('WATCH_ARRAY', async () => { - const spy = jest.fn() + const spy = vi.fn() const vm = new Vue({ data() { return { @@ -114,11 +116,12 @@ test('PROPS_DEFAULT_THIS', () => { }) test('V_ON_KEYCODE_MODIFIER', () => { - const spy = jest.fn() + const spy = vi.fn() const vm = new Vue({ template: ``, methods: { spy } }).$mount() + expect(vm.$el).toBeInstanceOf(HTMLInputElement) triggerEvent(vm.$el, 'keyup', e => { e.key = '_' e.keyCode = 1 @@ -131,11 +134,11 @@ test('V_ON_KEYCODE_MODIFIER', () => { test('CUSTOM_DIR', async () => { const myDir = { - bind: jest.fn(), - inserted: jest.fn(), - update: jest.fn(), - componentUpdated: jest.fn(), - unbind: jest.fn() + bind: vi.fn(), + inserted: vi.fn(), + update: vi.fn(), + componentUpdated: vi.fn(), + unbind: vi.fn() } as any const getCalls = () => @@ -191,6 +194,7 @@ test('ATTR_FALSE_VALUE', () => { const vm = new Vue({ template: `
    ` }).$mount() + expect(vm.$el).toBeInstanceOf(HTMLDivElement) expect(vm.$el.hasAttribute('id')).toBe(false) expect(vm.$el.hasAttribute('foo')).toBe(false) expect( @@ -209,6 +213,8 @@ test('ATTR_ENUMERATED_COERCION', () => { const vm = new Vue({ template: `
    ` }).$mount() + + expect(vm.$el).toBeInstanceOf(HTMLDivElement) expect(vm.$el.getAttribute('draggable')).toBe('false') expect(vm.$el.getAttribute('spellcheck')).toBe('true') expect(vm.$el.getAttribute('contenteditable')).toBe('true') diff --git a/packages/vue-compat/__tests__/options.spec.ts b/packages/vue-compat/__tests__/options.spec.ts index 7baac30dc29..75b5a440d3c 100644 --- a/packages/vue-compat/__tests__/options.spec.ts +++ b/packages/vue-compat/__tests__/options.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest' import Vue from '@vue/compat' import { nextTick } from '../../runtime-core/src/scheduler' import { @@ -84,8 +85,8 @@ test('data deep merge w/ extended constructor', () => { }) test('beforeDestroy/destroyed', async () => { - const beforeDestroy = jest.fn() - const destroyed = jest.fn() + const beforeDestroy = vi.fn() + const destroyed = vi.fn() const child = { template: `foo`, @@ -116,8 +117,8 @@ test('beforeDestroy/destroyed', async () => { }) test('beforeDestroy/destroyed in Vue.extend components', async () => { - const beforeDestroy = jest.fn() - const destroyed = jest.fn() + const beforeDestroy = vi.fn() + const destroyed = vi.fn() const child = Vue.extend({ template: `foo`, diff --git a/packages/vue-compat/__tests__/renderFn.spec.ts b/packages/vue-compat/__tests__/renderFn.spec.ts index 73876b4cf22..9b3d1466597 100644 --- a/packages/vue-compat/__tests__/renderFn.spec.ts +++ b/packages/vue-compat/__tests__/renderFn.spec.ts @@ -230,7 +230,7 @@ describe('compat: render function', () => { ) } }).$mount() - + expect(vm.$el).toBeInstanceOf(HTMLDivElement) expect(vm.$el.outerHTML).toBe(`
    hello
    `) expect( deprecationData[DeprecationTypes.RENDER_FUNCTION].message @@ -249,6 +249,7 @@ describe('compat: render function', () => { return createVNode('div', null, c.a) } }).$mount() + expect(vm.$el).toBeInstanceOf(HTMLDivElement) expect(vm.$el.outerHTML).toBe(`
    hello
    `) }) }) diff --git a/packages/vue-compat/package.json b/packages/vue-compat/package.json index 662c6c4f42a..92b0bdddfa2 100644 --- a/packages/vue-compat/package.json +++ b/packages/vue-compat/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compat", - "version": "3.2.31", + "version": "3.3.4", "description": "Vue 3 compatibility build for Vue 2", "main": "index.js", "module": "dist/vue.runtime.esm-bundler.js", @@ -37,7 +37,12 @@ "url": "https://github.com/vuejs/core/issues" }, "homepage": "https://github.com/vuejs/core/tree/main/packages/vue-compat#readme", + "dependencies": { + "@babel/parser": "^7.21.3", + "estree-walker": "^2.0.2", + "source-map-js": "^1.0.2" + }, "peerDependencies": { - "vue": "3.2.31" + "vue": "3.3.4" } } diff --git a/packages/vue/__tests__/customElementCasing.spec.ts b/packages/vue/__tests__/customElementCasing.spec.ts new file mode 100644 index 00000000000..b08de351de1 --- /dev/null +++ b/packages/vue/__tests__/customElementCasing.spec.ts @@ -0,0 +1,43 @@ +import { vi } from 'vitest' +import { createApp } from '../src' + +// https://github.com/vuejs/docs/pull/1890 +// https://github.com/vuejs/core/issues/5401 +// https://github.com/vuejs/docs/issues/1708 +test('custom element event casing', () => { + customElements.define( + 'custom-event-casing', + class Foo extends HTMLElement { + connectedCallback() { + this.dispatchEvent(new Event('camelCase')) + this.dispatchEvent(new Event('CAPScase')) + this.dispatchEvent(new Event('PascalCase')) + } + } + ) + + const container = document.createElement('div') + document.body.appendChild(container) + + const handler = vi.fn() + const handler2 = vi.fn() + createApp({ + template: ` + `, + methods: { + handler, + handler2 + } + }).mount(container) + + expect(handler).toHaveBeenCalledTimes(3) + expect(handler2).toHaveBeenCalledTimes(3) +}) diff --git a/packages/vue/__tests__/Transition.spec.ts b/packages/vue/__tests__/e2e/Transition.spec.ts similarity index 85% rename from packages/vue/__tests__/Transition.spec.ts rename to packages/vue/__tests__/e2e/Transition.spec.ts index 97b3ccd9625..f283d82608c 100644 --- a/packages/vue/__tests__/Transition.spec.ts +++ b/packages/vue/__tests__/e2e/Transition.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest' import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils' import path from 'path' import { h, createApp, Transition, ref, nextTick } from 'vue' @@ -29,6 +30,8 @@ describe('e2e: Transition', () => { test( 'basic transition', async () => { + await page().goto(baseUrl) + await page().waitForSelector('#app') await page().evaluate(() => { const { createApp, ref } = (window as any).Vue createApp({ @@ -264,12 +267,12 @@ describe('e2e: Transition', () => { test( 'transition events without appear', async () => { - const beforeLeaveSpy = jest.fn() - const onLeaveSpy = jest.fn() - const afterLeaveSpy = jest.fn() - const beforeEnterSpy = jest.fn() - const onEnterSpy = jest.fn() - const afterEnterSpy = jest.fn() + const beforeLeaveSpy = vi.fn() + const onLeaveSpy = vi.fn() + const afterLeaveSpy = vi.fn() + const beforeEnterSpy = vi.fn() + const onEnterSpy = vi.fn() + const afterEnterSpy = vi.fn() await page().exposeFunction('onLeaveSpy', onLeaveSpy) await page().exposeFunction('onEnterSpy', onEnterSpy) @@ -328,7 +331,6 @@ describe('e2e: Transition', () => { 'test-leave-from', 'test-leave-active' ]) - // todo test event with arguments. Note: not get dom, get object. '{}' expect(beforeLeaveSpy).toBeCalled() expect(onLeaveSpy).toBeCalled() expect(afterLeaveSpy).not.toBeCalled() @@ -366,8 +368,124 @@ describe('e2e: Transition', () => { E2E_TIMEOUT ) + test( + 'events with arguments', + async () => { + const beforeLeaveSpy = vi.fn() + const onLeaveSpy = vi.fn() + const afterLeaveSpy = vi.fn() + const beforeEnterSpy = vi.fn() + const onEnterSpy = vi.fn() + const afterEnterSpy = vi.fn() + + await page().exposeFunction('onLeaveSpy', onLeaveSpy) + await page().exposeFunction('onEnterSpy', onEnterSpy) + await page().exposeFunction('beforeLeaveSpy', beforeLeaveSpy) + await page().exposeFunction('beforeEnterSpy', beforeEnterSpy) + await page().exposeFunction('afterLeaveSpy', afterLeaveSpy) + await page().exposeFunction('afterEnterSpy', afterEnterSpy) + + await page().evaluate(() => { + const { + beforeEnterSpy, + onEnterSpy, + afterEnterSpy, + beforeLeaveSpy, + onLeaveSpy, + afterLeaveSpy + } = window as any + const { createApp, ref } = (window as any).Vue + createApp({ + template: ` +
    + +
    content
    +
    +
    + + `, + setup: () => { + const toggle = ref(true) + const click = () => (toggle.value = !toggle.value) + return { + toggle, + click, + beforeEnterSpy(el: Element) { + beforeEnterSpy() + el.classList.add('before-enter') + }, + onEnterSpy(el: Element, done: () => void) { + onEnterSpy() + el.classList.add('enter') + setTimeout(done, 200) + }, + afterEnterSpy(el: Element) { + afterEnterSpy() + el.classList.add('after-enter') + }, + beforeLeaveSpy(el: HTMLDivElement) { + beforeLeaveSpy() + el.classList.add('before-leave') + }, + onLeaveSpy(el: HTMLDivElement, done: () => void) { + onLeaveSpy() + el.classList.add('leave') + setTimeout(done, 200) + }, + afterLeaveSpy: (el: Element) => { + afterLeaveSpy() + } + } + } + }).mount('#app') + }) + expect(await html('#container')).toBe('
    content
    ') + + // leave + await click('#toggleBtn') + expect(beforeLeaveSpy).toBeCalled() + expect(onLeaveSpy).toBeCalled() + expect(afterLeaveSpy).not.toBeCalled() + expect(await classList('.test')).toStrictEqual([ + 'test', + 'before-leave', + 'leave' + ]) + + await timeout(200 + buffer) + expect(afterLeaveSpy).toBeCalled() + expect(await html('#container')).toBe('') + + // enter + await click('#toggleBtn') + expect(beforeEnterSpy).toBeCalled() + expect(onEnterSpy).toBeCalled() + expect(afterEnterSpy).not.toBeCalled() + expect(await classList('.test')).toStrictEqual([ + 'test', + 'before-enter', + 'enter' + ]) + + await timeout(200 + buffer) + expect(afterEnterSpy).toBeCalled() + expect(await html('#container')).toBe( + '
    content
    ' + ) + }, + E2E_TIMEOUT + ) + test('onEnterCancelled', async () => { - const enterCancelledSpy = jest.fn() + const enterCancelledSpy = vi.fn() await page().exposeFunction('enterCancelledSpy', enterCancelledSpy) @@ -507,15 +625,15 @@ describe('e2e: Transition', () => { test( 'transition events with appear', async () => { - const onLeaveSpy = jest.fn() - const onEnterSpy = jest.fn() - const onAppearSpy = jest.fn() - const beforeLeaveSpy = jest.fn() - const beforeEnterSpy = jest.fn() - const beforeAppearSpy = jest.fn() - const afterLeaveSpy = jest.fn() - const afterEnterSpy = jest.fn() - const afterAppearSpy = jest.fn() + const onLeaveSpy = vi.fn() + const onEnterSpy = vi.fn() + const onAppearSpy = vi.fn() + const beforeLeaveSpy = vi.fn() + const beforeEnterSpy = vi.fn() + const beforeAppearSpy = vi.fn() + const afterLeaveSpy = vi.fn() + const afterEnterSpy = vi.fn() + const afterAppearSpy = vi.fn() await page().exposeFunction('onLeaveSpy', onLeaveSpy) await page().exposeFunction('onEnterSpy', onEnterSpy) @@ -655,12 +773,12 @@ describe('e2e: Transition', () => { test( 'css: false', async () => { - const onBeforeEnterSpy = jest.fn() - const onEnterSpy = jest.fn() - const onAfterEnterSpy = jest.fn() - const onBeforeLeaveSpy = jest.fn() - const onLeaveSpy = jest.fn() - const onAfterLeaveSpy = jest.fn() + const onBeforeEnterSpy = vi.fn() + const onEnterSpy = vi.fn() + const onAfterEnterSpy = vi.fn() + const onBeforeLeaveSpy = vi.fn() + const onLeaveSpy = vi.fn() + const onAfterLeaveSpy = vi.fn() await page().exposeFunction('onBeforeEnterSpy', onBeforeEnterSpy) await page().exposeFunction('onEnterSpy', onEnterSpy) @@ -1104,8 +1222,8 @@ describe('e2e: Transition', () => { test( 'async component transition inside Suspense', async () => { - const onLeaveSpy = jest.fn() - const onEnterSpy = jest.fn() + const onLeaveSpy = vi.fn() + const onEnterSpy = vi.fn() await page().exposeFunction('onLeaveSpy', onLeaveSpy) await page().exposeFunction('onEnterSpy', onEnterSpy) @@ -1256,8 +1374,8 @@ describe('e2e: Transition', () => { test( 'out-in mode with Suspense', async () => { - const onLeaveSpy = jest.fn() - const onEnterSpy = jest.fn() + const onLeaveSpy = vi.fn() + const onEnterSpy = vi.fn() await page().exposeFunction('onLeaveSpy', onLeaveSpy) await page().exposeFunction('onEnterSpy', onEnterSpy) @@ -1446,12 +1564,12 @@ describe('e2e: Transition', () => { test( 'transition events with v-show', async () => { - const beforeLeaveSpy = jest.fn() - const onLeaveSpy = jest.fn() - const afterLeaveSpy = jest.fn() - const beforeEnterSpy = jest.fn() - const onEnterSpy = jest.fn() - const afterEnterSpy = jest.fn() + const beforeLeaveSpy = vi.fn() + const onLeaveSpy = vi.fn() + const afterLeaveSpy = vi.fn() + const beforeEnterSpy = vi.fn() + const onEnterSpy = vi.fn() + const afterEnterSpy = vi.fn() await page().exposeFunction('onLeaveSpy', onLeaveSpy) await page().exposeFunction('onEnterSpy', onEnterSpy) @@ -1552,7 +1670,7 @@ describe('e2e: Transition', () => { test( 'onLeaveCancelled (v-show only)', async () => { - const onLeaveCancelledSpy = jest.fn() + const onLeaveCancelledSpy = vi.fn() await page().exposeFunction('onLeaveCancelledSpy', onLeaveCancelledSpy) await page().evaluate(() => { @@ -1614,8 +1732,17 @@ describe('e2e: Transition', () => { test( 'transition on appear with v-show', async () => { + const beforeEnterSpy = vi.fn() + const onEnterSpy = vi.fn() + const afterEnterSpy = vi.fn() + + await page().exposeFunction('onEnterSpy', onEnterSpy) + await page().exposeFunction('beforeEnterSpy', beforeEnterSpy) + await page().exposeFunction('afterEnterSpy', afterEnterSpy) + const appearClass = await page().evaluate(async () => { const { createApp, ref } = (window as any).Vue + const { beforeEnterSpy, onEnterSpy, afterEnterSpy } = window as any createApp({ template: `
    @@ -1623,7 +1750,10 @@ describe('e2e: Transition', () => { appear appear-from-class="test-appear-from" appear-to-class="test-appear-to" - appear-active-class="test-appear-active"> + appear-active-class="test-appear-active" + @before-enter="beforeEnterSpy" + @enter="onEnterSpy" + @after-enter="afterEnterSpy">
    content
    @@ -1632,13 +1762,24 @@ describe('e2e: Transition', () => { setup: () => { const toggle = ref(true) const click = () => (toggle.value = !toggle.value) - return { toggle, click } + return { + toggle, + click, + beforeEnterSpy, + onEnterSpy, + afterEnterSpy + } } }).mount('#app') return Promise.resolve().then(() => { return document.querySelector('.test')!.className.split(/\s+/g) }) }) + + expect(beforeEnterSpy).toBeCalledTimes(1) + expect(onEnterSpy).toBeCalledTimes(1) + expect(afterEnterSpy).toBeCalledTimes(0) + // appear expect(appearClass).toStrictEqual([ 'test', @@ -1654,6 +1795,10 @@ describe('e2e: Transition', () => { await transitionFinish() expect(await html('#container')).toBe('
    content
    ') + expect(beforeEnterSpy).toBeCalledTimes(1) + expect(onEnterSpy).toBeCalledTimes(1) + expect(afterEnterSpy).toBeCalledTimes(1) + // leave expect(await classWhenTransitionStart()).toStrictEqual([ 'test', @@ -1688,6 +1833,79 @@ describe('e2e: Transition', () => { }, E2E_TIMEOUT ) + + // #4845 + test( + 'transition events should not call onEnter with v-show false', + async () => { + const beforeEnterSpy = vi.fn() + const onEnterSpy = vi.fn() + const afterEnterSpy = vi.fn() + + await page().exposeFunction('onEnterSpy', onEnterSpy) + await page().exposeFunction('beforeEnterSpy', beforeEnterSpy) + await page().exposeFunction('afterEnterSpy', afterEnterSpy) + + await page().evaluate(() => { + const { beforeEnterSpy, onEnterSpy, afterEnterSpy } = window as any + const { createApp, ref } = (window as any).Vue + createApp({ + template: ` +
    + +
    content
    +
    +
    + + `, + setup: () => { + const toggle = ref(false) + const click = () => (toggle.value = !toggle.value) + return { + toggle, + click, + beforeEnterSpy, + onEnterSpy, + afterEnterSpy + } + } + }).mount('#app') + }) + await nextTick() + + expect(await isVisible('.test')).toBe(false) + + expect(beforeEnterSpy).toBeCalledTimes(0) + expect(onEnterSpy).toBeCalledTimes(0) + // enter + expect(await classWhenTransitionStart()).toStrictEqual([ + 'test', + 'test-enter-from', + 'test-enter-active' + ]) + expect(beforeEnterSpy).toBeCalledTimes(1) + expect(onEnterSpy).toBeCalledTimes(1) + expect(afterEnterSpy).not.toBeCalled() + await nextFrame() + expect(await classList('.test')).toStrictEqual([ + 'test', + 'test-enter-active', + 'test-enter-to' + ]) + expect(afterEnterSpy).not.toBeCalled() + await transitionFinish() + expect(await html('#container')).toBe( + '
    content
    ' + ) + expect(afterEnterSpy).toBeCalled() + }, + E2E_TIMEOUT + ) }) describe('explicit durations', () => { @@ -1979,15 +2197,13 @@ describe('e2e: Transition', () => {
    ` }).mount(document.createElement('div')) - expect( - `invalid mode: none` - ).toHaveBeenWarned() + expect(`invalid mode: none`).toHaveBeenWarned() }) // #3227 test(`HOC w/ merged hooks`, async () => { - const innerSpy = jest.fn() - const outerSpy = jest.fn() + const innerSpy = vi.fn() + const outerSpy = vi.fn() const MyTransition = { render(this: any) { @@ -2023,4 +2239,74 @@ describe('e2e: Transition', () => { expect(outerSpy).toHaveBeenCalledTimes(1) expect(root.innerHTML).toBe(``) }) + + test( + 'should work with dev root fragment', + async () => { + await page().evaluate(() => { + const { createApp, ref } = (window as any).Vue + createApp({ + components: { + Comp: { + template: ` + +
    + ` + } + }, + template: ` +
    + + +
    content
    +
    +
    +
    + + `, + setup: () => { + const toggle = ref(true) + const click = () => (toggle.value = !toggle.value) + return { toggle, click } + } + }).mount('#app') + }) + expect(await html('#container')).toBe( + '
    content
    ' + ) + + // leave + expect(await classWhenTransitionStart()).toStrictEqual([ + 'test', + 'v-leave-from', + 'v-leave-active' + ]) + await nextFrame() + expect(await classList('.test')).toStrictEqual([ + 'test', + 'v-leave-active', + 'v-leave-to' + ]) + await transitionFinish() + expect(await html('#container')).toBe('') + + // enter + expect(await classWhenTransitionStart()).toStrictEqual([ + 'test', + 'v-enter-from', + 'v-enter-active' + ]) + await nextFrame() + expect(await classList('.test')).toStrictEqual([ + 'test', + 'v-enter-active', + 'v-enter-to' + ]) + await transitionFinish() + expect(await html('#container')).toBe( + '
    content
    ' + ) + }, + E2E_TIMEOUT + ) }) diff --git a/packages/vue/__tests__/TransitionGroup.spec.ts b/packages/vue/__tests__/e2e/TransitionGroup.spec.ts similarity index 98% rename from packages/vue/__tests__/TransitionGroup.spec.ts rename to packages/vue/__tests__/e2e/TransitionGroup.spec.ts index 38d742538a2..a78f3912412 100644 --- a/packages/vue/__tests__/TransitionGroup.spec.ts +++ b/packages/vue/__tests__/e2e/TransitionGroup.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest' import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils' import path from 'path' import { createApp, ref } from 'vue' @@ -359,15 +360,15 @@ describe('e2e: TransitionGroup', () => { test( 'events', async () => { - const onLeaveSpy = jest.fn() - const onEnterSpy = jest.fn() - const onAppearSpy = jest.fn() - const beforeLeaveSpy = jest.fn() - const beforeEnterSpy = jest.fn() - const beforeAppearSpy = jest.fn() - const afterLeaveSpy = jest.fn() - const afterEnterSpy = jest.fn() - const afterAppearSpy = jest.fn() + const onLeaveSpy = vi.fn() + const onEnterSpy = vi.fn() + const onAppearSpy = vi.fn() + const beforeLeaveSpy = vi.fn() + const beforeEnterSpy = vi.fn() + const beforeAppearSpy = vi.fn() + const afterLeaveSpy = vi.fn() + const afterEnterSpy = vi.fn() + const afterAppearSpy = vi.fn() await page().exposeFunction('onLeaveSpy', onLeaveSpy) await page().exposeFunction('onEnterSpy', onEnterSpy) diff --git a/packages/vue/examples/__tests__/commits.mock.ts b/packages/vue/__tests__/e2e/commits.mock.ts similarity index 99% rename from packages/vue/examples/__tests__/commits.mock.ts rename to packages/vue/__tests__/e2e/commits.mock.ts index 4b2d21c10bf..8bcb8d8f97b 100644 --- a/packages/vue/examples/__tests__/commits.mock.ts +++ b/packages/vue/__tests__/e2e/commits.mock.ts @@ -1,5 +1,5 @@ export default { - master: [ + main: [ { sha: 'd1527fbee422c7170e56845e55b49c4fd6de72a7', node_id: @@ -273,7 +273,7 @@ export default { ] } ], - sync: [ + 'v2-compat': [ { sha: 'ecf4da822eea97f5db5fa769d39f994755384a4b', node_id: diff --git a/packages/vue/examples/__tests__/commits.spec.ts b/packages/vue/__tests__/e2e/commits.spec.ts similarity index 71% rename from packages/vue/examples/__tests__/commits.spec.ts rename to packages/vue/__tests__/e2e/commits.spec.ts index 2190fb0d93f..1fe26bac2f1 100644 --- a/packages/vue/examples/__tests__/commits.spec.ts +++ b/packages/vue/__tests__/e2e/commits.spec.ts @@ -1,5 +1,5 @@ import path from 'path' -import { setupPuppeteer, E2E_TIMEOUT } from '../../__tests__/e2eUtils' +import { setupPuppeteer, E2E_TIMEOUT } from './e2eUtils' import mocks from './commits.mock' describe('e2e: commits', () => { @@ -8,7 +8,7 @@ describe('e2e: commits', () => { async function testCommits(apiType: 'classic' | 'composition') { const baseUrl = `file://${path.resolve( __dirname, - `../${apiType}/commits.html` + `../../examples/${apiType}/commits.html` )}` // intercept and mock the response to avoid hitting the actual API @@ -22,7 +22,7 @@ describe('e2e: commits', () => { status: 200, contentType: 'application/json', headers: { 'Access-Control-Allow-Origin': '*' }, - body: JSON.stringify(mocks[match[1] as 'master' | 'sync']) + body: JSON.stringify(mocks[match[1] as 'main' | 'v2-compat']) }) } }) @@ -31,16 +31,16 @@ describe('e2e: commits', () => { await page().waitForSelector('li') expect(await count('input')).toBe(2) expect(await count('label')).toBe(2) - expect(await text('label[for="master"]')).toBe('master') - expect(await text('label[for="sync"]')).toBe('sync') - expect(await isChecked('#master')).toBe(true) - expect(await isChecked('#sync')).toBe(false) - expect(await text('p')).toBe('vuejs/vue@master') + expect(await text('label[for="main"]')).toBe('main') + expect(await text('label[for="v2-compat"]')).toBe('v2-compat') + expect(await isChecked('#main')).toBe(true) + expect(await isChecked('#v2-compat')).toBe(false) + expect(await text('p')).toBe('vuejs/core@main') expect(await count('li')).toBe(3) expect(await count('li .commit')).toBe(3) expect(await count('li .message')).toBe(3) - await click('#sync') - expect(await text('p')).toBe('vuejs/vue@sync') + await click('#v2-compat') + expect(await text('p')).toBe('vuejs/core@v2-compat') expect(await count('li')).toBe(3) expect(await count('li .commit')).toBe(3) expect(await count('li .message')).toBe(3) diff --git a/packages/vue/__tests__/e2eUtils.ts b/packages/vue/__tests__/e2e/e2eUtils.ts similarity index 94% rename from packages/vue/__tests__/e2eUtils.ts rename to packages/vue/__tests__/e2e/e2eUtils.ts index 9ae3cc7fd3e..182cf021f17 100644 --- a/packages/vue/__tests__/e2eUtils.ts +++ b/packages/vue/__tests__/e2e/e2eUtils.ts @@ -1,4 +1,4 @@ -import puppeteer from 'puppeteer' +import puppeteer, { Browser, Page, ClickOptions } from 'puppeteer' export const E2E_TIMEOUT = 30 * 1000 @@ -25,12 +25,12 @@ export async function expectByPolling( } export function setupPuppeteer() { - let browser: puppeteer.Browser - let page: puppeteer.Page + let browser: Browser + let page: Page beforeAll(async () => { browser = await puppeteer.launch(puppeteerOptions) - }) + }, 20000) beforeEach(async () => { page = await browser.newPage() @@ -44,7 +44,7 @@ export function setupPuppeteer() { const err = e.args()[0] console.error( `Error from Puppeteer-loaded page:\n`, - err._remoteObject.description + err.remoteObject().description ) } }) @@ -58,7 +58,7 @@ export function setupPuppeteer() { await browser.close() }) - async function click(selector: string, options?: puppeteer.ClickOptions) { + async function click(selector: string, options?: ClickOptions) { await page.click(selector, options) } diff --git a/packages/vue/examples/__tests__/grid.spec.ts b/packages/vue/__tests__/e2e/grid.spec.ts similarity index 97% rename from packages/vue/examples/__tests__/grid.spec.ts rename to packages/vue/__tests__/e2e/grid.spec.ts index c73044cee2d..728804e0b50 100644 --- a/packages/vue/examples/__tests__/grid.spec.ts +++ b/packages/vue/__tests__/e2e/grid.spec.ts @@ -1,5 +1,5 @@ import path from 'path' -import { setupPuppeteer, E2E_TIMEOUT } from '../../__tests__/e2eUtils' +import { setupPuppeteer, E2E_TIMEOUT } from './e2eUtils' interface TableData { name: string @@ -24,7 +24,7 @@ describe('e2e: grid', () => { async function testGrid(apiType: 'classic' | 'composition') { const baseUrl = `file://${path.resolve( __dirname, - `../${apiType}/grid.html` + `../../examples/${apiType}/grid.html` )}` await page().goto(baseUrl) diff --git a/packages/vue/examples/__tests__/markdown.spec.ts b/packages/vue/__tests__/e2e/markdown.spec.ts similarity index 89% rename from packages/vue/examples/__tests__/markdown.spec.ts rename to packages/vue/__tests__/e2e/markdown.spec.ts index 35df22a2570..56c570e198f 100644 --- a/packages/vue/examples/__tests__/markdown.spec.ts +++ b/packages/vue/__tests__/e2e/markdown.spec.ts @@ -1,9 +1,5 @@ import path from 'path' -import { - setupPuppeteer, - expectByPolling, - E2E_TIMEOUT -} from '../../__tests__/e2eUtils' +import { setupPuppeteer, expectByPolling, E2E_TIMEOUT } from './e2eUtils' describe('e2e: markdown', () => { const { page, isVisible, value, html } = setupPuppeteer() @@ -11,7 +7,7 @@ describe('e2e: markdown', () => { async function testMarkdown(apiType: 'classic' | 'composition') { const baseUrl = `file://${path.resolve( __dirname, - `../${apiType}/markdown.html#test` + `../../examples/${apiType}/markdown.html#test` )}` await page().goto(baseUrl) diff --git a/packages/vue/examples/__tests__/svg.spec.ts b/packages/vue/__tests__/e2e/svg.spec.ts similarity index 96% rename from packages/vue/examples/__tests__/svg.spec.ts rename to packages/vue/__tests__/e2e/svg.spec.ts index 4c5bf9e4a34..09b5be81a91 100644 --- a/packages/vue/examples/__tests__/svg.spec.ts +++ b/packages/vue/__tests__/e2e/svg.spec.ts @@ -1,5 +1,5 @@ import path from 'path' -import { setupPuppeteer, E2E_TIMEOUT } from '../../__tests__/e2eUtils' +import { setupPuppeteer, E2E_TIMEOUT } from './e2eUtils' declare const globalStats: { label: string @@ -22,7 +22,7 @@ describe('e2e: svg', () => { async function assertPolygon(total: number) { expect( await page().evaluate( - total => { + ([total]) => { const points = globalStats .map((stat, i) => { const point = valueToPoint(stat.value, i, total) @@ -41,7 +41,7 @@ describe('e2e: svg', () => { // assert the position of each label is correct async function assertLabels(total: number) { const positions = await page().evaluate( - total => { + ([total]) => { return globalStats.map((stat, i) => { const point = valueToPoint(+stat.value + 10, i, total) return [point.x, point.y] @@ -73,7 +73,7 @@ describe('e2e: svg', () => { async function testSvg(apiType: 'classic' | 'composition') { const baseUrl = `file://${path.resolve( __dirname, - `../${apiType}/svg.html` + `../../examples/${apiType}/svg.html` )}` await page().goto(baseUrl) diff --git a/packages/vue/examples/__tests__/todomvc.spec.ts b/packages/vue/__tests__/e2e/todomvc.spec.ts similarity index 98% rename from packages/vue/examples/__tests__/todomvc.spec.ts rename to packages/vue/__tests__/e2e/todomvc.spec.ts index f6f1b05e576..668f9d33390 100644 --- a/packages/vue/examples/__tests__/todomvc.spec.ts +++ b/packages/vue/__tests__/e2e/todomvc.spec.ts @@ -1,5 +1,5 @@ import path from 'path' -import { setupPuppeteer, E2E_TIMEOUT } from '../../__tests__/e2eUtils' +import { setupPuppeteer, E2E_TIMEOUT } from './e2eUtils' describe('e2e: todomvc', () => { const { @@ -26,7 +26,7 @@ describe('e2e: todomvc', () => { async function testTodomvc(apiType: 'classic' | 'composition') { const baseUrl = `file://${path.resolve( __dirname, - `../${apiType}/todomvc.html` + `../../examples/${apiType}/todomvc.html` )}` await page().goto(baseUrl) diff --git a/packages/vue/__tests__/transition.html b/packages/vue/__tests__/e2e/transition.html similarity index 95% rename from packages/vue/__tests__/transition.html rename to packages/vue/__tests__/e2e/transition.html index 51553064554..2a794234103 100644 --- a/packages/vue/__tests__/transition.html +++ b/packages/vue/__tests__/e2e/transition.html @@ -1,4 +1,4 @@ - +