diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 04ecf049ca9..4296d0f39b6 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,3 +1,5 @@ +/* eslint-disable no-restricted-globals */ + const DOMGlobals = ['window', 'document'] const NodeGlobals = ['module', 'require'] @@ -9,12 +11,6 @@ module.exports = { plugins: ['jest'], rules: { 'no-debugger': 'error', - 'no-unused-vars': [ - 'error', - // we are only using this rule to check for unused arguments since TS - // catches unused variables but not args. - { varsIgnorePattern: '.*', args: 'none' } - ], // most of the codebase are expected to be env agnostic 'no-restricted-globals': ['error', ...DOMGlobals, ...NodeGlobals], @@ -72,6 +68,14 @@ module.exports = { 'no-restricted-syntax': 'off' } }, + // JavaScript files + { + files: ['*.js', '*.cjs'], + rules: { + // We only do `no-unused-vars` checks for js files, TS files are checked by TypeScript itself. + 'no-unused-vars': ['error', { vars: 'all', args: 'none' }] + } + }, // Node scripts { files: ['scripts/**', '*.{js,ts}', 'packages/**/index.js'], diff --git a/.github/contributing.md b/.github/contributing.md index 0c6771ca0b4..25628e2ba21 100644 --- a/.github/contributing.md +++ b/.github/contributing.md @@ -57,7 +57,7 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before ## Development Setup -You will need [Node.js](https://nodejs.org) **version 16+**, and [PNPM](https://pnpm.io) **version 8+**. +You will need [Node.js](https://nodejs.org) **version 18.12+**, and [PNPM](https://pnpm.io) **version 8+**. 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. @@ -181,11 +181,11 @@ Shortcut for starting the SFC Playground in local dev mode. This provides the fa ### `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. +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 reproduction that requires 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 useful when working on pure compiler issues. +The `dev-compiler` script builds, watches and serves the [Template Explorer](https://github.com/vuejs/core/tree/main/packages/template-explorer) at `http://localhost:3000`. This is useful when working on pure compiler issues. ### `nr test` diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 206deefc560..00000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,70 +0,0 @@ -version: 2 -updates: -- package-ecosystem: npm - directory: "/" - schedule: - interval: monthly - open-pull-requests-limit: 10 - versioning-strategy: lockfile-only - ignore: - - dependency-name: "@types/node" - versions: - - 14.14.24 - - 14.14.37 - - dependency-name: "@babel/parser" - versions: - - 7.12.11 - - 7.12.13 - - 7.12.14 - - 7.12.15 - - 7.12.16 - - 7.12.17 - - 7.13.0 - - 7.13.10 - - 7.13.11 - - 7.13.13 - - 7.13.4 - - 7.13.9 - - dependency-name: eslint - versions: - - 7.23.0 - - dependency-name: postcss - versions: - - 8.2.4 - - 8.2.5 - - 8.2.7 - - 8.2.8 - - dependency-name: typescript - versions: - - 4.2.2 - - dependency-name: "@babel/types" - versions: - - 7.12.12 - - 7.12.13 - - 7.12.17 - - 7.13.0 - - dependency-name: pug-code-gen - versions: - - 2.0.3 - - dependency-name: estree-walker - versions: - - 2.0.2 - - dependency-name: "@typescript-eslint/parser" - versions: - - 4.14.2 - - 4.15.0 - - dependency-name: "@microsoft/api-extractor" - versions: - - 7.13.1 - - dependency-name: rollup - versions: - - 2.38.5 - - 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/renovate.json5 b/.github/renovate.json5 new file mode 100644 index 00000000000..088913317c7 --- /dev/null +++ b/.github/renovate.json5 @@ -0,0 +1,50 @@ +{ + $schema: 'https://docs.renovatebot.com/renovate-schema.json', + extends: ['config:base', 'schedule:weekly', 'group:allNonMajor'], + labels: ['dependencies'], + ignorePaths: ['**/__tests__/**'], + rangeStrategy: 'bump', + packageRules: [ + { + depTypeList: ['peerDependencies'], + enabled: false + }, + { + groupName: 'test', + matchPackageNames: ['vitest', 'jsdom', 'puppeteer'], + matchPackagePrefixes: ['@vitest'] + }, + { + groupName: 'playground', + matchFileNames: [ + 'packages/sfc-playground/package.json', + 'packages/template-explorer/package.json' + ] + }, + { + groupName: 'compiler', + matchPackageNames: ['magic-string'], + matchPackagePrefixes: ['@babel', 'postcss'] + }, + { + groupName: 'build', + matchPackageNames: ['vite', 'terser'], + matchPackagePrefixes: ['rollup', 'esbuild', '@rollup', '@vitejs'] + }, + { + groupName: 'lint', + matchPackageNames: ['simple-git-hooks', 'lint-staged'], + matchPackagePrefixes: ['@typescript-eslint', 'eslint', 'prettier'] + } + ], + ignoreDeps: [ + 'vue', + + // manually bumping + 'node', + 'typescript', + + // ESM only + 'estree-walker' + ] +} diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml new file mode 100644 index 00000000000..1c81ece394d --- /dev/null +++ b/.github/workflows/autofix.yml @@ -0,0 +1,33 @@ +name: autofix.ci + +on: + pull_request: +permissions: + contents: read + +jobs: + autofix: + runs-on: ubuntu-latest + env: + PUPPETEER_SKIP_DOWNLOAD: 'true' + steps: + - uses: actions/checkout@v4 + + - 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 + + - run: pnpm install + + - name: Run eslint + run: pnpm run lint --fix + + - name: Run prettier + run: pnpm run format + + - uses: autofix-ci/action@d3e591514b99d0fca6779455ff8338516663f7cc diff --git a/.github/workflows/canary-minor.yml b/.github/workflows/canary-minor.yml new file mode 100644 index 00000000000..2aa6db12b36 --- /dev/null +++ b/.github/workflows/canary-minor.yml @@ -0,0 +1,33 @@ +name: canary minor 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@v4 + with: + ref: minor + + - 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 --tag minor + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index cabd601a8ef..61265c2d0e6 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -12,15 +12,15 @@ jobs: runs-on: ubuntu-latest environment: Release steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v2 - - name: Set node version to 18 + - name: Install Node.js uses: actions/setup-node@v3 with: - node-version: 18 + node-version-file: '.node-version' registry-url: 'https://registry.npmjs.org' cache: 'pnpm' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c08c9a935a..d4ec93e3e22 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,21 +14,20 @@ jobs: unit-test: runs-on: ubuntu-latest if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository + env: + PUPPETEER_SKIP_DOWNLOAD: 'true' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v2 - - name: Set node version to 18 + - name: Install Node.js uses: actions/setup-node@v3 with: - node-version: 18 + node-version-file: '.node-version' cache: 'pnpm' - - name: Skip Puppeteer download - run: echo "PUPPETEER_SKIP_DOWNLOAD=1" >> $GITHUB_ENV - - run: pnpm install - name: Run unit tests @@ -37,21 +36,20 @@ jobs: unit-test-windows: runs-on: windows-latest if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository + env: + PUPPETEER_SKIP_DOWNLOAD: 'true' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v2 - - name: Set node version to 18 + - name: Install Node.js uses: actions/setup-node@v3 with: - node-version: 18 + node-version-file: '.node-version' cache: 'pnpm' - - name: Skip Puppeteer download - run: echo "PUPPETEER_SKIP_DOWNLOAD=1" >> $env:GITHUB_ENV - - run: pnpm install - name: Run compiler unit tests @@ -64,24 +62,25 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup cache for Chromium binary uses: actions/cache@v3 with: - path: ~/.cache/puppeteer/chrome + path: ~/.cache/puppeteer key: chromium-${{ hashFiles('pnpm-lock.yaml') }} - name: Install pnpm uses: pnpm/action-setup@v2 - - name: Set node version to 18 + - name: Install Node.js uses: actions/setup-node@v3 with: - node-version: 18 + node-version-file: '.node-version' cache: 'pnpm' - run: pnpm install + - run: node node_modules/puppeteer/install.mjs - name: Run e2e tests run: pnpm run test-e2e @@ -89,21 +88,20 @@ jobs: lint-and-test-dts: runs-on: ubuntu-latest if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository + env: + PUPPETEER_SKIP_DOWNLOAD: 'true' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v2 - - name: Set node version to 18 + - name: Install Node.js uses: actions/setup-node@v3 with: - node-version: 18 + node-version-file: '.node-version' cache: 'pnpm' - - name: Skip Puppeteer download - run: echo "PUPPETEER_SKIP_DOWNLOAD=1" >> $GITHUB_ENV - - run: pnpm install - name: Run eslint diff --git a/.github/workflows/lock-closed-issues.yml b/.github/workflows/lock-closed-issues.yml new file mode 100644 index 00000000000..cf01a5f6cdf --- /dev/null +++ b/.github/workflows/lock-closed-issues.yml @@ -0,0 +1,20 @@ +name: Lock Closed Issues + +on: + schedule: + - cron: '0 0 * * *' + +permissions: + issues: write + +jobs: + action: + if: github.repository == 'vuejs/core' + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v4 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + issue-inactive-days: '14' + issue-lock-reason: '' + process-only: 'issues' diff --git a/.github/workflows/size-data.yml b/.github/workflows/size-data.yml index 647e029c578..83141e242f8 100644 --- a/.github/workflows/size-data.yml +++ b/.github/workflows/size-data.yml @@ -11,24 +11,27 @@ on: permissions: contents: read +env: + PUPPETEER_SKIP_DOWNLOAD: 'true' + jobs: upload: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v2 - - name: Set node version to LTS + - name: Install Node.js uses: actions/setup-node@v3 with: - node-version: lts/* + node-version-file: '.node-version' cache: pnpm - name: Install dependencies - run: PUPPETEER_SKIP_DOWNLOAD=1 pnpm install + run: pnpm install - run: pnpm run size @@ -42,7 +45,7 @@ jobs: if: ${{github.event_name == 'pull_request'}} run: echo ${{ github.event.number }} > ./pr.txt - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 if: ${{github.event_name == 'pull_request'}} with: name: pr-number diff --git a/.github/workflows/size-report.yml b/.github/workflows/size-report.yml index 75c52f717b3..cdfce9a979f 100644 --- a/.github/workflows/size-report.yml +++ b/.github/workflows/size-report.yml @@ -11,6 +11,9 @@ permissions: pull-requests: write issues: write +env: + PUPPETEER_SKIP_DOWNLOAD: 'true' + jobs: size-report: runs-on: ubuntu-latest @@ -18,19 +21,19 @@ jobs: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v2 - - name: Set node version to LTS + - name: Install Node.js uses: actions/setup-node@v3 with: - node-version: lts/* + node-version-file: '.node-version' cache: pnpm - name: Install dependencies - run: PUPPETEER_SKIP_DOWNLOAD=1 pnpm install + run: pnpm install - name: Download PR number uses: dawidd6/action-download-artifact@v2 diff --git a/.node-version b/.node-version new file mode 100644 index 00000000000..209e3ef4b62 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +20 diff --git a/CHANGELOG.md b/CHANGELOG.md index f2659ed589a..858b722d192 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,95 @@ +# [3.4.0-alpha.1](https://github.com/vuejs/core/compare/v3.3.7...v3.4.0-alpha.1) (2023-10-28) + + +### Features + +* **compiler-core:** export error message ([#8729](https://github.com/vuejs/core/issues/8729)) ([f7e80ee](https://github.com/vuejs/core/commit/f7e80ee4a065a9eaba98720abf415d9e87756cbd)) +* **compiler-sfc:** expose resolve type-based props and emits ([#8874](https://github.com/vuejs/core/issues/8874)) ([9e77580](https://github.com/vuejs/core/commit/9e77580c0c2f0d977bd0031a1d43cc334769d433)) +* export runtime error strings ([#9301](https://github.com/vuejs/core/issues/9301)) ([feb2f2e](https://github.com/vuejs/core/commit/feb2f2edce2d91218a5e9a52c81e322e4033296b)) +* **reactivity:** more efficient reactivity system ([#5912](https://github.com/vuejs/core/issues/5912)) ([16e06ca](https://github.com/vuejs/core/commit/16e06ca08f5a1e2af3fc7fb35de153dbe0c3087d)), closes [#311](https://github.com/vuejs/core/issues/311) [#1811](https://github.com/vuejs/core/issues/1811) [#6018](https://github.com/vuejs/core/issues/6018) [#7160](https://github.com/vuejs/core/issues/7160) [#8714](https://github.com/vuejs/core/issues/8714) [#9149](https://github.com/vuejs/core/issues/9149) [#9419](https://github.com/vuejs/core/issues/9419) [#9464](https://github.com/vuejs/core/issues/9464) +* **runtime-core:** add `once` option to watch ([#9034](https://github.com/vuejs/core/issues/9034)) ([a645e7a](https://github.com/vuejs/core/commit/a645e7aa51006516ba668b3a4365d296eb92ee7d)) + + + +## [3.3.7](https://github.com/vuejs/core/compare/v3.3.6...v3.3.7) (2023-10-24) + + +### Bug Fixes + +* **compiler-sfc:** avoid gen useCssVars when targeting SSR ([#6979](https://github.com/vuejs/core/issues/6979)) ([c568778](https://github.com/vuejs/core/commit/c568778ea3265d8e57f788b00864c9509bf88a4e)), closes [#6926](https://github.com/vuejs/core/issues/6926) +* **compiler-ssr:** proper scope analysis for ssr vnode slot fallback ([#7184](https://github.com/vuejs/core/issues/7184)) ([e09c26b](https://github.com/vuejs/core/commit/e09c26bc9bc4394c2c2d928806d382515c2676f3)), closes [#7095](https://github.com/vuejs/core/issues/7095) +* correctly resolve types from relative paths on Windows ([#9446](https://github.com/vuejs/core/issues/9446)) ([089d36d](https://github.com/vuejs/core/commit/089d36d167dc7834065b03ca689f9b6a44eead8a)), closes [#8671](https://github.com/vuejs/core/issues/8671) +* **hmr:** fix hmr error for hoisted children array in v-for ([7334376](https://github.com/vuejs/core/commit/733437691f70ebca8dd6cc3bc8356f5b57d4d5d8)), closes [#6978](https://github.com/vuejs/core/issues/6978) [#7114](https://github.com/vuejs/core/issues/7114) +* **reactivity:** assigning array.length while observing a symbol property ([#7568](https://github.com/vuejs/core/issues/7568)) ([e9e2778](https://github.com/vuejs/core/commit/e9e2778e9ec5cca07c1df5f0c9b7b3595a1a3244)) +* **scheduler:** ensure jobs are in the correct order ([#7748](https://github.com/vuejs/core/issues/7748)) ([a8f6638](https://github.com/vuejs/core/commit/a8f663867b8cd2736b82204bc58756ef02441276)), closes [#7576](https://github.com/vuejs/core/issues/7576) +* **ssr:** fix hydration mismatch for disabled teleport at component root ([#9399](https://github.com/vuejs/core/issues/9399)) ([d8990fc](https://github.com/vuejs/core/commit/d8990fc6182d1c2cf0a8eab7b35a9d04df668507)), closes [#6152](https://github.com/vuejs/core/issues/6152) +* **Suspense:** calling hooks before the transition finishes ([#9388](https://github.com/vuejs/core/issues/9388)) ([00de3e6](https://github.com/vuejs/core/commit/00de3e61ed7a55e7d6c2e1987551d66ad0f909ff)), closes [#5844](https://github.com/vuejs/core/issues/5844) [#5952](https://github.com/vuejs/core/issues/5952) +* **transition/ssr:** make transition appear work with SSR ([#8859](https://github.com/vuejs/core/issues/8859)) ([5ea8a8a](https://github.com/vuejs/core/commit/5ea8a8a4fab4e19a71e123e4d27d051f5e927172)), closes [#6951](https://github.com/vuejs/core/issues/6951) +* **types:** fix ComponentCustomProps augmentation ([#9468](https://github.com/vuejs/core/issues/9468)) ([7374e93](https://github.com/vuejs/core/commit/7374e93f0281f273b90ab5a6724cc47332a01d6c)), closes [#8376](https://github.com/vuejs/core/issues/8376) +* **types:** improve `h` overload to support union of string and component ([#5432](https://github.com/vuejs/core/issues/5432)) ([16ecb44](https://github.com/vuejs/core/commit/16ecb44c89cd8299a3b8de33cccc2e2cc36f065b)), closes [#5431](https://github.com/vuejs/core/issues/5431) + + + +## [3.3.6](https://github.com/vuejs/core/compare/v3.3.5...v3.3.6) (2023-10-20) + + +### Bug Fixes + +* **compiler-sfc:** model name conflict ([#8798](https://github.com/vuejs/core/issues/8798)) ([df81da8](https://github.com/vuejs/core/commit/df81da8be97c8a1366563c7e3e01076ef02eb8f7)) +* **compiler-sfc:** support asset paths containing spaces ([#8752](https://github.com/vuejs/core/issues/8752)) ([36c99a9](https://github.com/vuejs/core/commit/36c99a9c6bb6bc306be054c3c8a85ff8ce50605a)) +* **compiler-ssr:** fix missing scopeId on server-rendered TransitionGroup ([#7557](https://github.com/vuejs/core/issues/7557)) ([61c1357](https://github.com/vuejs/core/commit/61c135742795aa5e3189a79c7dec6afa21bbc8d9)), closes [#7554](https://github.com/vuejs/core/issues/7554) +* **compiler-ssr:** fix ssr compile error for select with non-option children ([#9442](https://github.com/vuejs/core/issues/9442)) ([cdb2e72](https://github.com/vuejs/core/commit/cdb2e725e7ea297f1f4180fb04889a3b757bc84e)), closes [#9440](https://github.com/vuejs/core/issues/9440) +* **runtime-core:** delete stale slots which are present but undefined ([#6484](https://github.com/vuejs/core/issues/6484)) ([75b8722](https://github.com/vuejs/core/commit/75b872213574cb37e2c9e8a15f65613f867ca9a6)), closes [#9109](https://github.com/vuejs/core/issues/9109) +* **runtime-core:** fix error when using cssvars with disabled teleport ([#7341](https://github.com/vuejs/core/issues/7341)) ([8f0472c](https://github.com/vuejs/core/commit/8f0472c9abedb337dc256143b69d8ab8759dbf5c)), closes [#7342](https://github.com/vuejs/core/issues/7342) +* **teleport:** ensure descendent component would be unmounted correctly ([#6529](https://github.com/vuejs/core/issues/6529)) ([4162311](https://github.com/vuejs/core/commit/4162311efdb0db5ca458542e1604b19efa2fae0e)), closes [#6347](https://github.com/vuejs/core/issues/6347) +* **types:** support contenteditable="plaintext-only" ([#8796](https://github.com/vuejs/core/issues/8796)) ([26ca89e](https://github.com/vuejs/core/commit/26ca89e5cf734fbef81e182050d2a215ec8a437b)) + + +### Performance Improvements + +* replace Map/Set with WeakMap/WeakSet ([#8549](https://github.com/vuejs/core/issues/8549)) ([712f96d](https://github.com/vuejs/core/commit/712f96d6ac4d3d984732cba448cb84624daba850)) + + + +## [3.3.5](https://github.com/vuejs/core/compare/v3.3.4...v3.3.5) (2023-10-20) + + +### Bug Fixes + +* add isGloballyWhitelisted back, but deprecated ([#8556](https://github.com/vuejs/core/issues/8556)) ([63dfe8e](https://github.com/vuejs/core/commit/63dfe8eab499979bcc2f7829e82464e13899c895)), closes [#8416](https://github.com/vuejs/core/issues/8416) +* **build:** disable useDefineForClassFields in esbuild ([#9252](https://github.com/vuejs/core/issues/9252)) ([6d14fa8](https://github.com/vuejs/core/commit/6d14fa88e85d4c9e264be394ddb37a54ca6738a8)) +* **compat:** return value of vue compat set() ([#9377](https://github.com/vuejs/core/issues/9377)) ([e3c2d69](https://github.com/vuejs/core/commit/e3c2d699f694d9500ddee78571172a24f0e3b17a)) +* **compiler-sfc:** don't hoist props and emit ([#8535](https://github.com/vuejs/core/issues/8535)) ([24db951](https://github.com/vuejs/core/commit/24db9516d8b4857182ec1a3af86cb7346691679b)), closes [#7805](https://github.com/vuejs/core/issues/7805) [#7812](https://github.com/vuejs/core/issues/7812) +* **compiler-sfc:** don't registerTS when bundling for browsers ([#8582](https://github.com/vuejs/core/issues/8582)) ([6f45f76](https://github.com/vuejs/core/commit/6f45f76df2c43796b35067ef8f8b9a7bca454040)) +* **compiler-sfc:** fix using imported ref as template ref during dev ([#7593](https://github.com/vuejs/core/issues/7593)) ([776ebf2](https://github.com/vuejs/core/commit/776ebf25b2e7570e78ac1c148fc45c823c21a542)), closes [#7567](https://github.com/vuejs/core/issues/7567) +* **compiler-sfc:** handle dynamic directive arguments in template usage check ([#8538](https://github.com/vuejs/core/issues/8538)) ([e404a69](https://github.com/vuejs/core/commit/e404a699f48ae5c5a5da947f42679343192158c7)), closes [#8537](https://github.com/vuejs/core/issues/8537) +* **compiler-sfc:** ignore style v-bind in double slash comments ([#5409](https://github.com/vuejs/core/issues/5409)) ([381b497](https://github.com/vuejs/core/commit/381b4977af25ba5392704f72ec6b3f2394d87ae7)) +* **compiler-sfc:** pass options directly to stylus ([#3848](https://github.com/vuejs/core/issues/3848)) ([d6446a6](https://github.com/vuejs/core/commit/d6446a6d40774b79045a9ddba7b5fd5201d51450)) +* **compiler-sfc:** support resolve multiple re-export /w same source type name ([#8365](https://github.com/vuejs/core/issues/8365)) ([4fa8da8](https://github.com/vuejs/core/commit/4fa8da8576717c619e1e8c04d19038488c75fbea)), closes [#8364](https://github.com/vuejs/core/issues/8364) +* **compiler-sfc:** typo in experimental feature warnings ([#8513](https://github.com/vuejs/core/issues/8513)) ([fd1a3f9](https://github.com/vuejs/core/commit/fd1a3f95990d7c372fa1c0c40c55caca761a33a4)) +* **deps:** update dependency monaco-editor to ^0.44.0 ([#9237](https://github.com/vuejs/core/issues/9237)) ([8611874](https://github.com/vuejs/core/commit/8611874e09a827b6491173836c8942284d5de22c)) +* **deps:** update playground ([#9154](https://github.com/vuejs/core/issues/9154)) ([c8566a2](https://github.com/vuejs/core/commit/c8566a22b7cf37e6aefab7bad7b97ce2db9fae4c)) +* **playground:** fix github button style ([#7722](https://github.com/vuejs/core/issues/7722)) ([5ee992c](https://github.com/vuejs/core/commit/5ee992cfeabc6c4b871980c6057d0ac7140ad2fa)) +* **runtime-core:** swap client/server debug labels ([#9089](https://github.com/vuejs/core/issues/9089)) ([8f311c6](https://github.com/vuejs/core/commit/8f311c6f823f6776ca1c49bfbbbf8c7d9dea9cf1)) +* **ssr:** render correct initial selected state for select with v-model ([#7432](https://github.com/vuejs/core/issues/7432)) ([201c46d](https://github.com/vuejs/core/commit/201c46df07a38f3c2b73f384e8e6846dc62f224e)), closes [#7392](https://github.com/vuejs/core/issues/7392) +* **ssr:** reset current instance if setting up options component errors ([#7743](https://github.com/vuejs/core/issues/7743)) ([020851e](https://github.com/vuejs/core/commit/020851e57d9a9f727c6ea07e9c1575430af02b73)), closes [#7733](https://github.com/vuejs/core/issues/7733) +* **teleport:** handle target change while disabled ([#7837](https://github.com/vuejs/core/issues/7837)) ([140a89b](https://github.com/vuejs/core/commit/140a89b833bceed60838182b875d2953c70af114)), closes [#7835](https://github.com/vuejs/core/issues/7835) +* **transition:** handle possible auto value for transition/animation durations ([96c76fa](https://github.com/vuejs/core/commit/96c76facb7de37fc241ccd55e121fd60a49a1452)), closes [#8409](https://github.com/vuejs/core/issues/8409) +* **types/jsx:** add `inert` attribute and missing `hidden` values ([#8090](https://github.com/vuejs/core/issues/8090)) ([ceb0732](https://github.com/vuejs/core/commit/ceb0732e0b1bb4c8c505d80e97ff6fc89035fa90)) +* **types/jsx:** add missing loading attr for img element ([#6160](https://github.com/vuejs/core/issues/6160)) ([68d6b43](https://github.com/vuejs/core/commit/68d6b43f7e29b76aab2c6c1882885380a43fa3e3)) +* **types:** correct withDefaults return type for boolean prop with undefined default value ([#8602](https://github.com/vuejs/core/issues/8602)) ([f07cb18](https://github.com/vuejs/core/commit/f07cb18fedf9a446545aadf76bcdfb957c7ebcbd)) +* **types:** ensure nextTick return type reflect correct Promise value ([#8406](https://github.com/vuejs/core/issues/8406)) ([6a22b1f](https://github.com/vuejs/core/commit/6a22b1f6c287b60eda385df8a514335af8e040ea)) +* **types:** support correct types for style on svg elements ([#6322](https://github.com/vuejs/core/issues/6322)) ([364dc53](https://github.com/vuejs/core/commit/364dc53c7cc6f97d812ad175199c698faa92538e)) + + +### Performance Improvements + +* **compiler-sfc:** lazy require typescript ([d2c3d8b](https://github.com/vuejs/core/commit/d2c3d8b70b2df6e16f053a7ac58e6b04e7b2078f)) +* **custom-element:** cancel `MutationObserver` listener when disconnected ([#8666](https://github.com/vuejs/core/issues/8666)) ([24d98f0](https://github.com/vuejs/core/commit/24d98f03276de5b0fbced5a4c9d61b24e7d9d084)) +* mark `defineComponent` as side-effects-free ([#8512](https://github.com/vuejs/core/issues/8512)) ([438027c](https://github.com/vuejs/core/commit/438027cf9ecb63260f59d3027e0b188717694795)) + + + ## [3.3.4](https://github.com/vuejs/core/compare/v3.3.3...v3.3.4) (2023-05-18) diff --git a/package.json b/package.json index b4ee1b00082..1655ff8b546 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "private": true, - "version": "3.3.4", - "packageManager": "pnpm@8.6.2", + "version": "3.4.0-alpha.1", + "packageManager": "pnpm@8.9.2", "type": "module", "scripts": { "dev": "node scripts/dev.js", "build": "node scripts/build.js", "build-dts": "tsc -p tsconfig.build.json && rollup -c rollup.dts.config.js", + "clean": "rimraf packages/*/dist temp .eslintcache", "size": "run-s \"size-*\" && tsx scripts/usage-size.ts", "size-global": "node scripts/build.js vue runtime-dom -f global -p --size", "size-esm-runtime": "node scripts/build.js vue -f esm-bundler-runtime", @@ -30,9 +31,9 @@ "dev-sfc-serve": "vite packages/sfc-playground --host", "dev-sfc-run": "run-p \"dev compiler-sfc -f esm-browser\" \"dev vue -if esm-bundler-runtime\" \"dev server-renderer -if esm-bundler\" dev-sfc-serve", "serve": "serve", - "open": "open http://localhost:5000/packages/template-explorer/local.html", - "build-sfc-playground": "run-s build-compiler-cjs build-runtime-esm build-ssr-esm build-sfc-playground-self", - "build-compiler-cjs": "node scripts/build.js compiler reactivity-transform shared -af cjs", + "open": "open http://localhost:3000/packages/template-explorer/local.html", + "build-sfc-playground": "run-s build-all-cjs build-runtime-esm build-ssr-esm build-sfc-playground-self", + "build-all-cjs": "node scripts/build.js vue runtime compiler reactivity reactivity-transform shared -af cjs", "build-runtime-esm": "node scripts/build.js runtime reactivity shared -af esm-bundler && node scripts/build.js vue -f esm-bundler-runtime && node scripts/build.js vue -f esm-browser-runtime", "build-ssr-esm": "node scripts/build.js compiler-sfc server-renderer -f esm-browser", "build-sfc-playground-self": "cd packages/sfc-playground && npm run build", @@ -53,56 +54,57 @@ ] }, "engines": { - "node": ">=16.11.0" + "node": ">=18.12.0" }, "devDependencies": { - "@babel/parser": "^7.21.3", - "@babel/types": "^7.21.3", - "@rollup/plugin-alias": "^4.0.3", - "@rollup/plugin-commonjs": "^24.0.1", - "@rollup/plugin-json": "^6.0.0", - "@rollup/plugin-node-resolve": "^15.0.1", - "@rollup/plugin-replace": "^5.0.2", - "@rollup/plugin-terser": "^0.4.0", - "@types/hash-sum": "^1.0.0", - "@types/node": "^16.4.7", - "@typescript-eslint/parser": "^5.56.0", - "@vitest/coverage-istanbul": "^0.29.7", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "@rollup/plugin-alias": "^5.0.1", + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-json": "^6.0.1", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-replace": "^5.0.4", + "@rollup/plugin-terser": "^0.4.4", + "@types/hash-sum": "^1.0.1", + "@types/node": "^20.8.7", + "@typescript-eslint/parser": "^6.8.0", + "@vitest/coverage-istanbul": "^0.34.6", "@vue/consolidate": "0.17.3", - "chalk": "^4.1.0", - "conventional-changelog-cli": "^2.0.31", - "enquirer": "^2.3.2", - "esbuild": "^0.17.4", - "esbuild-plugin-polyfill-node": "^0.2.0", - "eslint": "^8.33.0", - "eslint-plugin-jest": "^27.2.1", + "conventional-changelog-cli": "^4.1.0", + "enquirer": "^2.4.1", + "esbuild": "^0.19.5", + "esbuild-plugin-polyfill-node": "^0.3.0", + "eslint": "^8.52.0", + "eslint-plugin-jest": "^27.4.3", "estree-walker": "^2.0.2", - "execa": "^4.0.2", - "jsdom": "^21.1.0", - "lint-staged": "^10.2.10", - "lodash": "^4.17.15", - "magic-string": "^0.30.0", + "execa": "^8.0.1", + "jsdom": "^22.1.0", + "lint-staged": "^15.0.2", + "lodash": "^4.17.21", + "magic-string": "^0.30.5", "markdown-table": "^3.0.3", - "marked": "^4.0.10", - "minimist": "^1.2.0", + "marked": "^9.1.2", + "minimist": "^1.2.8", "npm-run-all": "^4.1.5", - "prettier": "^3.0.1", + "picocolors": "^1.0.0", + "prettier": "^3.0.3", "pretty-bytes": "^6.1.1", - "pug": "^3.0.1", - "puppeteer": "~19.6.0", - "rollup": "^3.26.0", - "rollup-plugin-dts": "^5.3.0", - "rollup-plugin-esbuild": "^5.0.0", + "pug": "^3.0.2", + "puppeteer": "~21.4.0", + "rimraf": "^5.0.5", + "rollup": "^4.1.4", + "rollup-plugin-dts": "^6.1.0", + "rollup-plugin-esbuild": "^6.1.0", "rollup-plugin-polyfill-node": "^0.12.0", - "semver": "^7.3.2", - "serve": "^12.0.0", - "simple-git-hooks": "^2.8.1", - "terser": "^5.19.2", - "todomvc-app-css": "^2.3.0", - "tslib": "^2.5.0", - "tsx": "^3.12.7", - "typescript": "^5.1.6", - "vite": "^4.3.0", - "vitest": "^0.30.1" + "semver": "^7.5.4", + "serve": "^14.2.1", + "simple-git-hooks": "^2.9.0", + "terser": "^5.22.0", + "todomvc-app-css": "^2.4.3", + "tslib": "^2.6.2", + "tsx": "^3.14.0", + "typescript": "^5.2.2", + "vite": "^4.5.0", + "vitest": "^0.34.6" } } diff --git a/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts b/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts index eec5a76d363..49ad7ad8982 100644 --- a/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts +++ b/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts @@ -593,5 +593,17 @@ describe('compiler: hoistStatic transform', () => { expect(root.hoists.length).toBe(2) expect(generate(root).code).toMatchSnapshot() }) + + test('clone hoisted array children in HMR mode', () => { + const root = transformWithHoist(`
`, { + hmr: true + }) + expect(root.hoists.length).toBe(2) + expect(root.codegenNode).toMatchObject({ + children: { + content: '[..._hoisted_2]' + } + }) + }) }) }) diff --git a/packages/compiler-core/package.json b/packages/compiler-core/package.json index 68daa30ecd2..e32771ab1cc 100644 --- a/packages/compiler-core/package.json +++ b/packages/compiler-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-core", - "version": "3.3.4", + "version": "3.4.0-alpha.1", "description": "@vue/compiler-core", "main": "index.js", "module": "dist/compiler-core.esm-bundler.js", @@ -32,12 +32,12 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-core#readme", "dependencies": { - "@babel/parser": "^7.21.3", - "@vue/shared": "3.3.4", + "@babel/parser": "^7.23.0", + "@vue/shared": "3.4.0-alpha.1", "estree-walker": "^2.0.2", "source-map-js": "^1.0.2" }, "devDependencies": { - "@babel/types": "^7.21.3" + "@babel/types": "^7.23.0" } } diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts index 4898a181dfc..588bb92cc5f 100644 --- a/packages/compiler-core/src/index.ts +++ b/packages/compiler-core/src/index.ts @@ -24,6 +24,7 @@ export { export { generate, type CodegenContext, type CodegenResult } from './codegen' export { ErrorCodes, + errorMessages, createCompilerError, type CoreCompilerError, type CompilerError diff --git a/packages/compiler-core/src/options.ts b/packages/compiler-core/src/options.ts index 65bbcb36dd6..abfba98e35c 100644 --- a/packages/compiler-core/src/options.ts +++ b/packages/compiler-core/src/options.ts @@ -256,6 +256,12 @@ export interface TransformOptions * needed to render inline CSS variables on component root */ ssrCssVars?: string + /** + * Whether to compile the template assuming it needs to handle HMR. + * Some edge cases may need to generate different code for HMR to work + * correctly, e.g. #6938, #7138 + */ + hmr?: boolean } export interface CodegenOptions extends SharedTransformCodegenOptions { diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index 4d9e8c6ed49..04f85679cae 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -117,7 +117,7 @@ export interface TransformContext removeIdentifiers(exp: ExpressionNode | string): void hoist(exp: string | JSChildNode | ArrayExpression): SimpleExpressionNode cache(exp: T, isVNode?: boolean): CacheExpression | T - constantCache: Map + constantCache: WeakMap // 2.x Compat only filters?: Set @@ -129,6 +129,7 @@ export function createTransformContext( filename = '', prefixIdentifiers = false, hoistStatic = false, + hmr = false, cacheHandlers = false, nodeTransforms = [], directiveTransforms = {}, @@ -155,6 +156,7 @@ export function createTransformContext( selfName: nameMatch && capitalize(camelize(nameMatch[1])), prefixIdentifiers, hoistStatic, + hmr, cacheHandlers, nodeTransforms, directiveTransforms, @@ -181,7 +183,7 @@ export function createTransformContext( directives: new Set(), hoists: [], imports: [], - constantCache: new Map(), + constantCache: new WeakMap(), temps: 0, cached: 0, identifiers: Object.create(null), diff --git a/packages/compiler-core/src/transforms/hoistStatic.ts b/packages/compiler-core/src/transforms/hoistStatic.ts index 5526163c6f9..fd443496ca7 100644 --- a/packages/compiler-core/src/transforms/hoistStatic.ts +++ b/packages/compiler-core/src/transforms/hoistStatic.ts @@ -140,9 +140,16 @@ function walk( node.codegenNode.type === NodeTypes.VNODE_CALL && isArray(node.codegenNode.children) ) { - node.codegenNode.children = context.hoist( + const hoisted = context.hoist( createArrayExpression(node.codegenNode.children) ) + // #6978, #7138, #7114 + // a hoisted children array inside v-for can caused HMR errors since + // it might be mutated when mounting the v-for list + if (context.hmr) { + hoisted.content = `[...${hoisted.content}]` + } + node.codegenNode.children = hoisted } } diff --git a/packages/compiler-core/src/transforms/vSlot.ts b/packages/compiler-core/src/transforms/vSlot.ts index c4416dd45f7..ffa90ea1171 100644 --- a/packages/compiler-core/src/transforms/vSlot.ts +++ b/packages/compiler-core/src/transforms/vSlot.ts @@ -100,11 +100,12 @@ export const trackVForSlotScopes: NodeTransform = (node, context) => { export type SlotFnBuilder = ( slotProps: ExpressionNode | undefined, + vForExp: ExpressionNode | undefined, slotChildren: TemplateChildNode[], loc: SourceLocation ) => FunctionExpression -const buildClientSlotFn: SlotFnBuilder = (props, children, loc) => +const buildClientSlotFn: SlotFnBuilder = (props, _vForExp, children, loc) => createFunctionExpression( props, children, @@ -149,7 +150,7 @@ export function buildSlots( slotsProperties.push( createObjectProperty( arg || createSimpleExpression('default', true), - buildSlotFn(exp, children, loc) + buildSlotFn(exp, undefined, children, loc) ) ) } @@ -201,11 +202,17 @@ export function buildSlots( hasDynamicSlots = true } - const slotFunction = buildSlotFn(slotProps, slotChildren, slotLoc) + const vFor = findDir(slotElement, 'for') + const slotFunction = buildSlotFn( + slotProps, + vFor?.exp, + slotChildren, + slotLoc + ) + // check if this slot is conditional (v-if/v-for) let vIf: DirectiveNode | undefined let vElse: DirectiveNode | undefined - let vFor: DirectiveNode | undefined if ((vIf = findDir(slotElement, 'if'))) { hasDynamicSlots = true dynamicSlots.push( @@ -257,7 +264,7 @@ export function buildSlots( createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, vElse.loc) ) } - } else if ((vFor = findDir(slotElement, 'for'))) { + } else if (vFor) { hasDynamicSlots = true const parseResult = vFor.parseResult || @@ -306,7 +313,7 @@ export function buildSlots( props: ExpressionNode | undefined, children: TemplateChildNode[] ) => { - const fn = buildSlotFn(props, children, loc) + const fn = buildSlotFn(props, undefined, children, loc) if (__COMPAT__ && context.compatConfig) { fn.isNonScopedSlot = true } diff --git a/packages/compiler-dom/package.json b/packages/compiler-dom/package.json index d21043af9e6..f39057c076b 100644 --- a/packages/compiler-dom/package.json +++ b/packages/compiler-dom/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-dom", - "version": "3.3.4", + "version": "3.4.0-alpha.1", "description": "@vue/compiler-dom", "main": "index.js", "module": "dist/compiler-dom.esm-bundler.js", @@ -37,7 +37,7 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-dom#readme", "dependencies": { - "@vue/shared": "3.3.4", - "@vue/compiler-core": "3.3.4" + "@vue/shared": "3.4.0-alpha.1", + "@vue/compiler-core": "3.4.0-alpha.1" } } diff --git a/packages/compiler-dom/src/index.ts b/packages/compiler-dom/src/index.ts index 2c6f71cefbb..a2f4aff2e4c 100644 --- a/packages/compiler-dom/src/index.ts +++ b/packages/compiler-dom/src/index.ts @@ -68,5 +68,9 @@ export function parse(template: string, options: ParserOptions = {}): RootNode { export * from './runtimeHelpers' export { transformStyle } from './transforms/transformStyle' -export { createDOMCompilerError, DOMErrorCodes } from './errors' +export { + createDOMCompilerError, + DOMErrorCodes, + DOMErrorMessages +} from './errors' export * from '@vue/compiler-core' diff --git a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineModel.spec.ts.snap b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineModel.spec.ts.snap index fdfd3710efc..297ee62724c 100644 --- a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineModel.spec.ts.snap +++ b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineModel.spec.ts.snap @@ -7,15 +7,17 @@ export default { props: { \\"modelValue\\": { required: true }, \\"count\\": {}, + \\"toString\\": { type: Function }, }, - emits: [\\"update:modelValue\\", \\"update:count\\"], + emits: [\\"update:modelValue\\", \\"update:count\\", \\"update:toString\\"], setup(__props, { expose: __expose }) { __expose(); const modelValue = _useModel(__props, \\"modelValue\\") const c = _useModel(__props, \\"count\\") + const toString = _useModel(__props, \\"toString\\") -return { modelValue, c } +return { modelValue, c, toString } } }" diff --git a/packages/compiler-sfc/__tests__/compileScript/defineModel.spec.ts b/packages/compiler-sfc/__tests__/compileScript/defineModel.spec.ts index 61a9adcbe0d..10fab947c13 100644 --- a/packages/compiler-sfc/__tests__/compileScript/defineModel.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/defineModel.spec.ts @@ -8,6 +8,7 @@ describe('defineModel()', () => { `, { defineModel: true } @@ -16,18 +17,22 @@ describe('defineModel()', () => { 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('"toString": { type: Function },') + expect(content).toMatch( + 'emits: ["update:modelValue", "update:count", "update:toString"],' + ) expect(content).toMatch( `const modelValue = _useModel(__props, "modelValue")` ) expect(content).toMatch(`const c = _useModel(__props, "count")`) - expect(content).toMatch(`return { modelValue, c }`) + expect(content).toMatch(`return { modelValue, c, toString }`) expect(content).not.toMatch('defineModel') expect(bindings).toStrictEqual({ modelValue: BindingTypes.SETUP_REF, count: BindingTypes.PROPS, - c: BindingTypes.SETUP_REF + c: BindingTypes.SETUP_REF, + toString: BindingTypes.SETUP_REF }) }) diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index 607654a952b..fc600f1a518 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -1,3 +1,4 @@ +import { normalize } from 'node:path' import { Identifier } from '@babel/types' import { SFCScriptCompileOptions, parse } from '../../src' import { ScriptCompileContext } from '../../src/script/context' @@ -478,6 +479,33 @@ describe('resolveType', () => { expect(deps && [...deps]).toStrictEqual(Object.keys(files)) }) + test.runIf(process.platform === 'win32')('relative ts on Windows', () => { + const files = { + 'C:\\Test\\foo.ts': 'export type P = { foo: number }', + 'C:\\Test\\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, + {}, + 'C:\\Test\\Test.vue' + ) + expect(props).toStrictEqual({ + foo: ['Number'], + bar: ['String'] + }) + expect(deps && [...deps].map(normalize)).toStrictEqual( + Object.keys(files).map(normalize) + ) + }) + // #8244 test('utility type in external file', () => { const files = { @@ -898,19 +926,20 @@ describe('resolveType', () => { function resolve( code: string, files: Record = {}, - options?: Partial + options?: Partial, + sourceFileName: string = '/Test.vue' ) { const { descriptor } = parse(``, { - filename: '/Test.vue' + filename: sourceFileName }) const ctx = new ScriptCompileContext(descriptor, { id: 'test', fs: { fileExists(file) { - return !!files[file] + return !!(files[file] ?? files[normalize(file)]) }, readFile(file) { - return files[file] + return files[file] ?? files[normalize(file)] } }, ...options diff --git a/packages/compiler-sfc/__tests__/cssVars.spec.ts b/packages/compiler-sfc/__tests__/cssVars.spec.ts index 5b01d73d772..9fb72d7ad50 100644 --- a/packages/compiler-sfc/__tests__/cssVars.spec.ts +++ b/packages/compiler-sfc/__tests__/cssVars.spec.ts @@ -272,5 +272,73 @@ describe('CSS vars injection', () => { `export default {\n setup(__props, { expose: __expose }) {\n __expose();\n\n_useCssVars(_ctx => ({\n "xxxxxxxx-background": (_unref(background))\n}))` ) }) + + describe('skip codegen in SSR', () => { + test('script setup, inline', () => { + const { content } = compileSFCScript( + `\n` + + ``, + { + inlineTemplate: true, + templateOptions: { + ssr: true + } + } + ) + expect(content).not.toMatch(`_useCssVars`) + }) + + // #6926 + test('script, non-inline', () => { + const { content } = compileSFCScript( + `\n` + + ``, + { + inlineTemplate: false, + templateOptions: { + ssr: true + } + } + ) + expect(content).not.toMatch(`_useCssVars`) + }) + + test('normal script', () => { + const { content } = compileSFCScript( + `\n` + + ``, + { + templateOptions: { + ssr: true + } + } + ) + expect(content).not.toMatch(`_useCssVars`) + }) + }) }) }) diff --git a/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts b/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts index f267e73ede0..44c13e47ea2 100644 --- a/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts +++ b/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts @@ -166,4 +166,34 @@ describe('compiler sfc: transform asset url', () => { expect(code).toMatch(`_createStaticVNode`) expect(code).toMatchSnapshot() }) + + test('transform with stringify with space in absolute filename', () => { + const { code } = compileWithAssetUrls( + `

`, + { + includeAbsolute: true + }, + { + hoistStatic: true, + transformHoist: stringifyStatic + } + ) + expect(code).toMatch(`_createElementVNode`) + expect(code).toContain(`import _imports_0 from '/foo bar.png'`) + }) + + test('transform with stringify with space in relative filename', () => { + const { code } = compileWithAssetUrls( + `
`, + { + includeAbsolute: true + }, + { + hoistStatic: true, + transformHoist: stringifyStatic + } + ) + expect(code).toMatch(`_createElementVNode`) + expect(code).toContain(`import _imports_0 from './foo bar.png'`) + }) }) diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json index 33a8c40d185..6c28d1c7f03 100644 --- a/packages/compiler-sfc/package.json +++ b/packages/compiler-sfc/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-sfc", - "version": "3.3.4", + "version": "3.4.0-alpha.1", "description": "@vue/compiler-sfc", "main": "dist/compiler-sfc.cjs.js", "module": "dist/compiler-sfc.esm-browser.js", @@ -32,29 +32,27 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-sfc#readme", "dependencies": { - "@babel/parser": "^7.20.15", - "@vue/compiler-core": "3.3.4", - "@vue/compiler-dom": "3.3.4", - "@vue/compiler-ssr": "3.3.4", - "@vue/reactivity-transform": "3.3.4", - "@vue/shared": "3.3.4", + "@babel/parser": "^7.23.0", + "@vue/compiler-core": "3.4.0-alpha.1", + "@vue/compiler-dom": "3.4.0-alpha.1", + "@vue/compiler-ssr": "3.4.0-alpha.1", + "@vue/reactivity-transform": "3.4.0-alpha.1", + "@vue/shared": "3.4.0-alpha.1", "estree-walker": "^2.0.2", - "magic-string": "^0.30.0", - "postcss": "^8.1.10", + "magic-string": "^0.30.5", + "postcss": "^8.4.31", "source-map-js": "^1.0.2" }, "devDependencies": { - "@babel/types": "^7.21.3", - "@types/estree": "^0.0.48", - "@types/lru-cache": "^5.1.0", + "@babel/types": "^7.23.0", "@vue/consolidate": "^0.17.3", "hash-sum": "^2.0.0", - "lru-cache": "^5.1.1", + "lru-cache": "^10.0.1", "merge-source-map": "^1.1.0", - "minimatch": "^9.0.0", - "postcss-modules": "^4.0.0", - "postcss-selector-parser": "^6.0.4", - "pug": "^3.0.1", - "sass": "^1.26.9" + "minimatch": "^9.0.3", + "postcss-modules": "^4.3.1", + "postcss-selector-parser": "^6.0.13", + "pug": "^3.0.2", + "sass": "^1.69.4" } } diff --git a/packages/compiler-sfc/src/cache.ts b/packages/compiler-sfc/src/cache.ts index 36d240810c7..f04b231f6d9 100644 --- a/packages/compiler-sfc/src/cache.ts +++ b/packages/compiler-sfc/src/cache.ts @@ -1,11 +1,10 @@ -import LRU from 'lru-cache' +import { LRUCache } from 'lru-cache' -export function createCache(size = 500): Map & { max?: number } { +export function createCache( + max = 500 +): Map | LRUCache { if (__GLOBAL__ || __ESM_BROWSER__) { return new Map() } - const cache = new LRU(size) - // @ts-expect-error - cache.delete = cache.del.bind(cache) - return cache as any as Map + return new LRUCache({ max }) } diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index cfcc607c72d..2a33f69936d 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -765,7 +765,7 @@ export function compileScript( if ( sfc.cssVars.length && // no need to do this when targeting SSR - !(options.inlineTemplate && options.templateOptions?.ssr) + !options.templateOptions?.ssr ) { ctx.helperImports.add(CSS_VARS_HELPER) ctx.helperImports.add('unref') diff --git a/packages/compiler-sfc/src/compileTemplate.ts b/packages/compiler-sfc/src/compileTemplate.ts index fbd100c9784..b036619c794 100644 --- a/packages/compiler-sfc/src/compileTemplate.ts +++ b/packages/compiler-sfc/src/compileTemplate.ts @@ -212,6 +212,7 @@ function doCompileTemplate({ slotted, sourceMap: true, ...compilerOptions, + hmr: !isProd, nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []), filename, onError: e => errors.push(e), diff --git a/packages/compiler-sfc/src/index.ts b/packages/compiler-sfc/src/index.ts index 76b4900d46d..c6ee604146e 100644 --- a/packages/compiler-sfc/src/index.ts +++ b/packages/compiler-sfc/src/index.ts @@ -33,6 +33,8 @@ export { // Internals for type resolution export { invalidateTypeCache, registerTS } from './script/resolveType' +export { extractRuntimeProps } from './script/defineProps' +export { extractRuntimeEmits } from './script/defineEmits' // Types export type { @@ -58,6 +60,7 @@ export type { SFCScriptCompileOptions } from './compileScript' export type { ScriptCompileContext } from './script/context' export type { TypeResolveContext, + SimpleTypeResolveOptions, SimpleTypeResolveContext } from './script/resolveType' export type { diff --git a/packages/compiler-sfc/src/rewriteDefault.ts b/packages/compiler-sfc/src/rewriteDefault.ts index ae5e7366bde..277eedce011 100644 --- a/packages/compiler-sfc/src/rewriteDefault.ts +++ b/packages/compiler-sfc/src/rewriteDefault.ts @@ -37,7 +37,7 @@ export function rewriteDefaultAST( // multi-line comments or template strings. fallback to a full parse. ast.forEach(node => { if (node.type === 'ExportDefaultDeclaration') { - if (node.declaration.type === 'ClassDeclaration') { + if (node.declaration.type === 'ClassDeclaration' && node.declaration.id) { let start: number = node.declaration.decorators && node.declaration.decorators.length > 0 ? node.declaration.decorators[ diff --git a/packages/compiler-sfc/src/script/context.ts b/packages/compiler-sfc/src/script/context.ts index 5fe09d28a42..692eab3ab9e 100644 --- a/packages/compiler-sfc/src/script/context.ts +++ b/packages/compiler-sfc/src/script/context.ts @@ -53,7 +53,7 @@ export class ScriptCompileContext { emitDecl: Node | undefined // defineModel - modelDecls: Record = {} + modelDecls: Record = Object.create(null) // defineOptions optionsRuntimeDecl: Node | undefined diff --git a/packages/compiler-sfc/src/script/defineEmits.ts b/packages/compiler-sfc/src/script/defineEmits.ts index 02014d1b276..b7453076cfe 100644 --- a/packages/compiler-sfc/src/script/defineEmits.ts +++ b/packages/compiler-sfc/src/script/defineEmits.ts @@ -1,7 +1,18 @@ -import { Identifier, LVal, Node, RestElement } from '@babel/types' +import { + ArrayPattern, + Identifier, + LVal, + Node, + ObjectPattern, + RestElement +} from '@babel/types' import { isCallOf } from './utils' import { ScriptCompileContext } from './context' -import { resolveTypeElements, resolveUnionType } from './resolveType' +import { + TypeResolveContext, + resolveTypeElements, + resolveUnionType +} from './resolveType' export const DEFINE_EMITS = 'defineEmits' @@ -57,7 +68,7 @@ export function genRuntimeEmits(ctx: ScriptCompileContext): string | undefined { return emitsDecl } -function extractRuntimeEmits(ctx: ScriptCompileContext): Set { +export function extractRuntimeEmits(ctx: TypeResolveContext): Set { const emits = new Set() const node = ctx.emitsTypeDecl! @@ -90,8 +101,8 @@ function extractRuntimeEmits(ctx: ScriptCompileContext): Set { } function extractEventNames( - ctx: ScriptCompileContext, - eventName: Identifier | RestElement, + ctx: TypeResolveContext, + eventName: ArrayPattern | Identifier | ObjectPattern | RestElement, emits: Set ) { if ( diff --git a/packages/compiler-sfc/src/script/defineProps.ts b/packages/compiler-sfc/src/script/defineProps.ts index 5004e314da1..449ed250d1d 100644 --- a/packages/compiler-sfc/src/script/defineProps.ts +++ b/packages/compiler-sfc/src/script/defineProps.ts @@ -8,7 +8,11 @@ import { } from '@babel/types' import { BindingTypes, isFunctionType } from '@vue/compiler-dom' import { ScriptCompileContext } from './context' -import { inferRuntimeType, resolveTypeElements } from './resolveType' +import { + TypeResolveContext, + inferRuntimeType, + resolveTypeElements +} from './resolveType' import { resolveObjectKey, UNKNOWN_TYPE, @@ -150,7 +154,7 @@ export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined { } } } else if (ctx.propsTypeDecl) { - propsDecls = genRuntimePropsFromTypes(ctx) + propsDecls = extractRuntimeProps(ctx) } const modelsDecls = genModelProps(ctx) @@ -162,7 +166,9 @@ export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined { } } -function genRuntimePropsFromTypes(ctx: ScriptCompileContext) { +export function extractRuntimeProps( + ctx: TypeResolveContext +): string | undefined { // this is only called if propsTypeDecl exists const props = resolveRuntimePropsFromType(ctx, ctx.propsTypeDecl!) if (!props.length) { @@ -175,7 +181,7 @@ function genRuntimePropsFromTypes(ctx: ScriptCompileContext) { for (const prop of props) { propStrings.push(genRuntimePropFromType(ctx, prop, hasStaticDefaults)) // register bindings - if (!(prop.key in ctx.bindingMetadata)) { + if ('bindingMetadata' in ctx && !(prop.key in ctx.bindingMetadata)) { ctx.bindingMetadata[prop.key] = BindingTypes.PROPS } } @@ -193,7 +199,7 @@ function genRuntimePropsFromTypes(ctx: ScriptCompileContext) { } function resolveRuntimePropsFromType( - ctx: ScriptCompileContext, + ctx: TypeResolveContext, node: Node ): PropTypeData[] { const props: PropTypeData[] = [] @@ -222,7 +228,7 @@ function resolveRuntimePropsFromType( } function genRuntimePropFromType( - ctx: ScriptCompileContext, + ctx: TypeResolveContext, { key, required, type, skipCheck }: PropTypeData, hasStaticDefaults: boolean ): string { @@ -284,7 +290,7 @@ function genRuntimePropFromType( * static properties, we can directly generate more optimized default * declarations. Otherwise we will have to fallback to runtime merging. */ -function hasStaticWithDefaults(ctx: ScriptCompileContext) { +function hasStaticWithDefaults(ctx: TypeResolveContext) { return !!( ctx.propsRuntimeDefaults && ctx.propsRuntimeDefaults.type === 'ObjectExpression' && @@ -297,7 +303,7 @@ function hasStaticWithDefaults(ctx: ScriptCompileContext) { } function genDestructuredDefaultValue( - ctx: ScriptCompileContext, + ctx: TypeResolveContext, key: string, inferredType?: string[] ): diff --git a/packages/compiler-sfc/src/script/normalScript.ts b/packages/compiler-sfc/src/script/normalScript.ts index 76b25c66350..d0f16134273 100644 --- a/packages/compiler-sfc/src/script/normalScript.ts +++ b/packages/compiler-sfc/src/script/normalScript.ts @@ -55,7 +55,7 @@ export function processNormalScript( const s = new MagicString(content) rewriteDefaultAST(scriptAst.body, s, defaultVar) content = s.toString() - if (cssVars.length) { + if (cssVars.length && !ctx.options.templateOptions?.ssr) { content += genNormalScriptCssVarsCode( cssVars, bindings, diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index 146c454729c..12666341e73 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -42,6 +42,13 @@ import type TS from 'typescript' import { extname, dirname } from 'path' import { minimatch as isMatch } from 'minimatch' +export type SimpleTypeResolveOptions = Partial< + Pick< + SFCScriptCompileOptions, + 'globalTypeFiles' | 'fs' | 'babelParserPlugins' | 'isProd' + > +> + /** * TypeResolveContext is compatible with ScriptCompileContext * but also allows a simpler version of it with minimal required properties @@ -59,13 +66,28 @@ import { minimatch as isMatch } from 'minimatch' */ export type SimpleTypeResolveContext = Pick< ScriptCompileContext, - // required - 'source' | 'filename' | 'error' | 'options' + // file + | 'source' + | 'filename' + + // utils + | 'error' + | 'helper' + | 'getString' + + // props + | 'propsTypeDecl' + | 'propsRuntimeDefaults' + | 'propsDestructuredBindings' + + // emits + | 'emitsTypeDecl' > & Partial< Pick > & { ast: Statement[] + options: SimpleTypeResolveOptions } export type TypeResolveContext = ScriptCompileContext | SimpleTypeResolveContext @@ -778,7 +800,7 @@ function importSourceToScope( if (!resolved) { if (source.startsWith('.')) { // relative import - fast path - const filename = joinPaths(scope.filename, '..', source) + const filename = joinPaths(dirname(scope.filename), source) resolved = resolveExt(filename, fs) } else { // module or aliased import - use full TS resolution, only supported in Node @@ -1227,7 +1249,7 @@ function recordType( break } case 'ClassDeclaration': - types[overwriteId || getId(node.id)] = node + if (overwriteId || node.id) types[overwriteId || getId(node.id!)] = node break case 'TSTypeAliasDeclaration': types[node.id.name] = node.typeAnnotation diff --git a/packages/compiler-sfc/src/template/transformAssetUrl.ts b/packages/compiler-sfc/src/template/transformAssetUrl.ts index 32bf33bcea1..4267d4ea352 100644 --- a/packages/compiler-sfc/src/template/transformAssetUrl.ts +++ b/packages/compiler-sfc/src/template/transformAssetUrl.ts @@ -168,7 +168,13 @@ function getImportsExpressionExp( loc, ConstantTypes.CAN_STRINGIFY ) - context.imports.push({ exp, path }) + + // We need to ensure the path is not encoded (to %2F), + // so we decode it back in case it is encoded + context.imports.push({ + exp, + path: decodeURIComponent(path) + }) } if (!hash) { diff --git a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts index 9391c01e37e..a8ea08a5349 100644 --- a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts @@ -181,11 +181,14 @@ describe('ssr: components', () => { }) test('v-for slot', () => { - expect( - compile(` - - `).code - ).toMatchInlineSnapshot(` + const { code } = compile(` + + `) + expect(code).not.toMatch(`_ctx.msg`) + expect(code).not.toMatch(`_ctx.key`) + expect(code).not.toMatch(`_ctx.index`) + expect(code).toMatch(`_ctx.bar`) + expect(code).toMatchInlineSnapshot(` "const { resolveComponent: _resolveComponent, withCtx: _withCtx, toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, renderList: _renderList, createSlots: _createSlots } = require(\\"vue\\") const { ssrRenderComponent: _ssrRenderComponent, ssrInterpolate: _ssrInterpolate } = require(\\"vue/server-renderer\\") @@ -193,15 +196,15 @@ describe('ssr: components', () => { const _component_foo = _resolveComponent(\\"foo\\") _push(_ssrRenderComponent(_component_foo, _attrs, _createSlots({ _: 2 /* DYNAMIC */ }, [ - _renderList(_ctx.names, (key) => { + _renderList(_ctx.names, (key, index) => { return { name: key, fn: _withCtx(({ msg }, _push, _parent, _scopeId) => { if (_push) { - _push(\`\${_ssrInterpolate(msg + key + _ctx.bar)}\`) + _push(\`\${_ssrInterpolate(msg + key + index + _ctx.bar)}\`) } else { return [ - _createTextVNode(_toDisplayString(msg + _ctx.key + _ctx.bar), 1 /* TEXT */) + _createTextVNode(_toDisplayString(msg + key + index + _ctx.bar), 1 /* TEXT */) ] } }) diff --git a/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts b/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts index 3445a84fda9..1be6a2c180c 100644 --- a/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts @@ -124,4 +124,48 @@ describe('ssr: scopeId', () => { }" `) }) + + // #7554 + test('scopeId is correctly transform to scope attribute of transition-group ', () => { + expect( + compile( + `hello`, + { + scopeId, + mode: 'module' + } + ).code + ).toMatchInlineSnapshot(` + "import { mergeProps as _mergeProps } from \\"vue\\" + import { ssrRenderAttrs as _ssrRenderAttrs } from \\"vue/server-renderer\\" + + export function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`hello\`) + }" + `) + + // with dynamic tag + expect( + compile( + `hello`, + { + scopeId, + mode: 'module' + } + ).code + ).toMatchInlineSnapshot(` + "import { mergeProps as _mergeProps } from \\"vue\\" + import { ssrRenderAttrs as _ssrRenderAttrs } from \\"vue/server-renderer\\" + + export function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`<\${ + _ctx.someTag + }\${ + _ssrRenderAttrs(_mergeProps({ class: \\"red\\" }, _attrs)) + } data-v-xxxxxxx>hello\`) + }" + `) + }) }) diff --git a/packages/compiler-ssr/__tests__/ssrTransition.spec.ts b/packages/compiler-ssr/__tests__/ssrTransition.spec.ts new file mode 100644 index 00000000000..319b3902239 --- /dev/null +++ b/packages/compiler-ssr/__tests__/ssrTransition.spec.ts @@ -0,0 +1,25 @@ +import { compile } from '../src' + +describe('transition', () => { + test('basic', () => { + expect(compile(`
foo
`).code) + .toMatchInlineSnapshot(` + "const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`foo\`) + }" + `) + }) + + test('with appear', () => { + expect(compile(`
foo
`).code) + .toMatchInlineSnapshot(` + "const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) + }" + `) + }) +}) diff --git a/packages/compiler-ssr/__tests__/ssrVModel.spec.ts b/packages/compiler-ssr/__tests__/ssrVModel.spec.ts index 5bccbcb788c..f28c38d4026 100644 --- a/packages/compiler-ssr/__tests__/ssrVModel.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrVModel.spec.ts @@ -69,6 +69,57 @@ describe('ssr: v-model', () => { }>\`) }" `) + + expect( + compileWithWrapper(``) + .code + ).toMatchInlineSnapshot(` + "const { ssrRenderSlot: _ssrRenderSlot, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) + }" + `) + + expect( + compileWithWrapper(` + `).code + ).toMatchInlineSnapshot(` + "const { ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) + }" + `) + + expect( + compileWithWrapper(` + `).code + ).toMatchInlineSnapshot(` + "const { ssrRenderSlot: _ssrRenderSlot, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) + }" + `) }) test('', () => { diff --git a/packages/compiler-ssr/package.json b/packages/compiler-ssr/package.json index c8e4d33bf4e..df467affd10 100644 --- a/packages/compiler-ssr/package.json +++ b/packages/compiler-ssr/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-ssr", - "version": "3.3.4", + "version": "3.4.0-alpha.1", "description": "@vue/compiler-ssr", "main": "dist/compiler-ssr.cjs.js", "types": "dist/compiler-ssr.d.ts", @@ -28,7 +28,7 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-ssr#readme", "dependencies": { - "@vue/shared": "3.3.4", - "@vue/compiler-dom": "3.3.4" + "@vue/shared": "3.4.0-alpha.1", + "@vue/compiler-dom": "3.4.0-alpha.1" } } diff --git a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts index dc8c6a4ae4f..7a12cb29009 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts @@ -56,6 +56,10 @@ import { } from './ssrTransformTransitionGroup' import { isSymbol, isObject, isArray } from '@vue/shared' import { buildSSRProps } from './ssrTransformElement' +import { + ssrProcessTransition, + ssrTransformTransition +} from './ssrTransformTransition' // We need to construct the slot functions in the 1st pass to ensure proper // scope tracking, but the children of each slot cannot be processed until @@ -99,9 +103,10 @@ export const ssrTransformComponent: NodeTransform = (node, context) => { if (isSymbol(component)) { if (component === SUSPENSE) { return ssrTransformSuspense(node, context) - } - if (component === TRANSITION_GROUP) { + } else if (component === TRANSITION_GROUP) { return ssrTransformTransitionGroup(node, context) + } else if (component === TRANSITION) { + return ssrTransformTransition(node, context) } return // other built-in components: fallthrough } @@ -120,8 +125,10 @@ export const ssrTransformComponent: NodeTransform = (node, context) => { // fallback in case the child is render-fn based). Store them in an array // for later use. if (clonedNode.children.length) { - buildSlots(clonedNode, context, (props, children) => { - vnodeBranches.push(createVNodeSlotBranch(props, children, context)) + buildSlots(clonedNode, context, (props, vFor, children) => { + vnodeBranches.push( + createVNodeSlotBranch(props, vFor, children, context) + ) return createFunctionExpression(undefined) }) } @@ -145,7 +152,7 @@ export const ssrTransformComponent: NodeTransform = (node, context) => { const wipEntries: WIPSlotEntry[] = [] wipMap.set(node, wipEntries) - const buildSSRSlotFn: SlotFnBuilder = (props, children, loc) => { + const buildSSRSlotFn: SlotFnBuilder = (props, _vForExp, children, loc) => { const param0 = (props && stringifyExpression(props)) || `_` const fn = createFunctionExpression( [param0, `_push`, `_parent`, `_scopeId`], @@ -216,9 +223,8 @@ export function ssrProcessComponent( if ((parent as WIPSlotEntry).type === WIP_SLOT) { context.pushStringPart(``) } - // #5351: filter out comment children inside transition if (component === TRANSITION) { - node.children = node.children.filter(c => c.type !== NodeTypes.COMMENT) + return ssrProcessTransition(node, context) } processChildren(node, context) } @@ -273,6 +279,7 @@ const vnodeDirectiveTransforms = { function createVNodeSlotBranch( props: ExpressionNode | undefined, + vForExp: ExpressionNode | undefined, children: TemplateChildNode[], parentContext: TransformContext ): ReturnStatement { @@ -299,8 +306,8 @@ function createVNodeSlotBranch( tag: 'template', tagType: ElementTypes.TEMPLATE, isSelfClosing: false, - // important: provide v-slot="props" on the wrapper for proper - // scope analysis + // important: provide v-slot="props" and v-for="exp" on the wrapper for + // proper scope analysis props: [ { type: NodeTypes.DIRECTIVE, @@ -309,6 +316,14 @@ function createVNodeSlotBranch( arg: undefined, modifiers: [], loc: locStub + }, + { + type: NodeTypes.DIRECTIVE, + name: 'for', + exp: vForExp, + arg: undefined, + modifiers: [], + loc: locStub } ], children, diff --git a/packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts b/packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts index 207e9348eef..e7efbe1fb73 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts @@ -36,20 +36,24 @@ export function ssrTransformSuspense( wipSlots: [] } wipMap.set(node, wipEntry) - wipEntry.slotsExp = buildSlots(node, context, (_props, children, loc) => { - const fn = createFunctionExpression( - [], - undefined, // no return, assign body later - true, // newline - false, // suspense slots are not treated as normal slots - loc - ) - wipEntry.wipSlots.push({ - fn, - children - }) - return fn - }).slots + wipEntry.slotsExp = buildSlots( + node, + context, + (_props, _vForExp, children, loc) => { + const fn = createFunctionExpression( + [], + undefined, // no return, assign body later + true, // newline + false, // suspense slots are not treated as normal slots + loc + ) + wipEntry.wipSlots.push({ + fn, + children + }) + return fn + } + ).slots } } } diff --git a/packages/compiler-ssr/src/transforms/ssrTransformTransition.ts b/packages/compiler-ssr/src/transforms/ssrTransformTransition.ts new file mode 100644 index 00000000000..d09a806f7b0 --- /dev/null +++ b/packages/compiler-ssr/src/transforms/ssrTransformTransition.ts @@ -0,0 +1,36 @@ +import { + ComponentNode, + findProp, + NodeTypes, + TransformContext +} from '@vue/compiler-dom' +import { processChildren, SSRTransformContext } from '../ssrCodegenTransform' + +const wipMap = new WeakMap() + +export function ssrTransformTransition( + node: ComponentNode, + context: TransformContext +) { + return () => { + const appear = findProp(node, 'appear', false, true) + wipMap.set(node, !!appear) + } +} + +export function ssrProcessTransition( + node: ComponentNode, + context: SSRTransformContext +) { + // #5351: filter out comment children inside transition + node.children = node.children.filter(c => c.type !== NodeTypes.COMMENT) + + const appear = wipMap.get(node) + if (appear) { + context.pushStringPart(``) + } else { + processChildren(node, context, false, true) + } +} diff --git a/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts b/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts index 00b0d9dd45a..b0f96e4dd6c 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts @@ -18,6 +18,7 @@ const wipMap = new WeakMap() interface WIPEntry { tag: AttributeNode | DirectiveNode propsExp: string | JSChildNode | null + scopeId: string | null } // phase 1: build props @@ -45,7 +46,8 @@ export function ssrTransformTransitionGroup( } wipMap.set(node, { tag, - propsExp + propsExp, + scopeId: context.scopeId || null }) } } @@ -58,7 +60,7 @@ export function ssrProcessTransitionGroup( ) { const entry = wipMap.get(node) if (entry) { - const { tag, propsExp } = entry + const { tag, propsExp, scopeId } = entry if (tag.type === NodeTypes.DIRECTIVE) { // dynamic :tag context.pushStringPart(`<`) @@ -66,6 +68,9 @@ export function ssrProcessTransitionGroup( if (propsExp) { context.pushStringPart(propsExp) } + if (scopeId) { + context.pushStringPart(` ${scopeId}`) + } context.pushStringPart(`>`) processChildren( @@ -89,6 +94,9 @@ export function ssrProcessTransitionGroup( if (propsExp) { context.pushStringPart(propsExp) } + if (scopeId) { + context.pushStringPart(` ${scopeId}`) + } context.pushStringPart(`>`) processChildren(node, context, false, true) context.pushStringPart(``) diff --git a/packages/compiler-ssr/src/transforms/ssrVModel.ts b/packages/compiler-ssr/src/transforms/ssrVModel.ts index bd587edcb9c..0c4bd177875 100644 --- a/packages/compiler-ssr/src/transforms/ssrVModel.ts +++ b/packages/compiler-ssr/src/transforms/ssrVModel.ts @@ -38,6 +38,38 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => { } } + function processOption(plainNode: PlainElementNode) { + if (plainNode.tag === 'option') { + if (plainNode.props.findIndex(p => p.name === 'selected') === -1) { + const value = findValueBinding(plainNode) + plainNode.ssrCodegenNode!.elements.push( + createConditionalExpression( + createCallExpression(context.helper(SSR_INCLUDE_BOOLEAN_ATTR), [ + createConditionalExpression( + createCallExpression(`Array.isArray`, [model]), + createCallExpression(context.helper(SSR_LOOSE_CONTAIN), [ + model, + value + ]), + createCallExpression(context.helper(SSR_LOOSE_EQUAL), [ + model, + value + ]) + ) + ]), + createSimpleExpression(' selected', true), + createSimpleExpression('', true), + false /* no newline */ + ) + ) + } + } else if (plainNode.tag === 'optgroup') { + plainNode.children.forEach(option => + processOption(option as PlainElementNode) + ) + } + } + if (node.tagType === ElementTypes.ELEMENT) { const res: DirectiveTransformResult = { props: [] } const defaultProps = [ @@ -130,32 +162,9 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => { checkDuplicatedValue() node.children = [createInterpolation(model, model.loc)] } else if (node.tag === 'select') { - node.children.forEach(option => { - if (option.type === NodeTypes.ELEMENT) { - const plainNode = option as PlainElementNode - if (plainNode.props.findIndex(p => p.name === 'selected') === -1) { - const value = findValueBinding(plainNode) - plainNode.ssrCodegenNode!.elements.push( - createConditionalExpression( - createCallExpression(context.helper(SSR_INCLUDE_BOOLEAN_ATTR), [ - createConditionalExpression( - createCallExpression(`Array.isArray`, [model]), - createCallExpression(context.helper(SSR_LOOSE_CONTAIN), [ - model, - value - ]), - createCallExpression(context.helper(SSR_LOOSE_EQUAL), [ - model, - value - ]) - ) - ]), - createSimpleExpression(' selected', true), - createSimpleExpression('', true), - false /* no newline */ - ) - ) - } + node.children.forEach(child => { + if (child.type === NodeTypes.ELEMENT) { + processOption(child as PlainElementNode) } }) } else { diff --git a/packages/dts-built-test/README.md b/packages/dts-built-test/README.md new file mode 100644 index 00000000000..8191d66e32e --- /dev/null +++ b/packages/dts-built-test/README.md @@ -0,0 +1,5 @@ +# dts built-package test + +This package is private and for testing only. It is used to verify edge cases for external libraries that build their types using Vue core types - e.g. Vuetify as in [#8376](https://github.com/vuejs/core/issues/8376). + +When running the `build-dts` task, this package's types are built alongside other packages. Then, during `test-dts-only` it is imported and used in [`packages/dts-test/built.test-d.ts`](https://github.com/vuejs/core/blob/main/packages/dts-test/built.test-d.ts) to verify that the built types work correctly. diff --git a/packages/dts-built-test/package.json b/packages/dts-built-test/package.json new file mode 100644 index 00000000000..0a544787753 --- /dev/null +++ b/packages/dts-built-test/package.json @@ -0,0 +1,11 @@ +{ + "name": "@vue/dts-built-test", + "private": true, + "types": "dist/dts-built-test.d.ts", + "dependencies": { + "@vue/shared": "workspace:*", + "@vue/reactivity": "workspace:*", + "vue": "workspace:*" + }, + "version": "3.4.0-alpha.1" +} diff --git a/packages/dts-built-test/src/index.ts b/packages/dts-built-test/src/index.ts new file mode 100644 index 00000000000..2d9d4033254 --- /dev/null +++ b/packages/dts-built-test/src/index.ts @@ -0,0 +1,12 @@ +import { defineComponent } from 'vue' + +const _CustomPropsNotErased = defineComponent({ + props: {}, + setup() {} +}) + +// #8376 +export const CustomPropsNotErased = + _CustomPropsNotErased as typeof _CustomPropsNotErased & { + foo: string + } diff --git a/packages/dts-test/built.test-d.ts b/packages/dts-test/built.test-d.ts new file mode 100644 index 00000000000..8ac3e333f99 --- /dev/null +++ b/packages/dts-test/built.test-d.ts @@ -0,0 +1,13 @@ +import { CustomPropsNotErased } from '@vue/dts-built-test' +import { expectType, describe } from './utils' + +declare module 'vue' { + interface ComponentCustomProps { + custom?: number + } +} + +// #8376 - custom props should not be erased +describe('Custom Props not erased', () => { + expectType(new CustomPropsNotErased().$props.custom) +}) diff --git a/packages/dts-test/h.test-d.ts b/packages/dts-test/h.test-d.ts index 5c700800e94..f2e984b49b8 100644 --- a/packages/dts-test/h.test-d.ts +++ b/packages/dts-test/h.test-d.ts @@ -1,6 +1,7 @@ import { h, defineComponent, + DefineComponent, ref, Fragment, Teleport, @@ -231,3 +232,18 @@ describe('resolveComponent should work', () => { message: '1' }) }) + +// #5431 +describe('h should work with multiple types', () => { + const serializers = { + Paragraph: 'p', + Component: {} as Component, + DefineComponent: {} as DefineComponent + } + + const sampleComponent = serializers['' as keyof typeof serializers] + + h(sampleComponent) + h(sampleComponent, {}) + h(sampleComponent, {}, []) +}) diff --git a/packages/dts-test/package.json b/packages/dts-test/package.json index a47f064f4eb..ff3c8313ea4 100644 --- a/packages/dts-test/package.json +++ b/packages/dts-test/package.json @@ -2,7 +2,8 @@ "name": "dts-test", "private": true, "dependencies": { - "vue": "workspace:*" + "vue": "workspace:*", + "@vue/dts-built-test": "workspace:*" }, - "version": "3.3.4" + "version": "3.4.0-alpha.1" } diff --git a/packages/reactivity-transform/package.json b/packages/reactivity-transform/package.json index d23e1ee8a3c..8e43685c5dd 100644 --- a/packages/reactivity-transform/package.json +++ b/packages/reactivity-transform/package.json @@ -1,6 +1,6 @@ { "name": "@vue/reactivity-transform", - "version": "3.3.4", + "version": "3.4.0-alpha.1", "description": "@vue/reactivity-transform", "main": "dist/reactivity-transform.cjs.js", "files": [ @@ -28,14 +28,14 @@ }, "homepage": "https://github.com/vuejs/core/tree/dev/packages/reactivity-transform#readme", "dependencies": { - "@babel/parser": "^7.20.15", - "@vue/compiler-core": "3.3.4", - "@vue/shared": "3.3.4", + "@babel/parser": "^7.23.0", + "@vue/compiler-core": "3.4.0-alpha.1", + "@vue/shared": "3.4.0-alpha.1", "estree-walker": "^2.0.2", - "magic-string": "^0.30.0" + "magic-string": "^0.30.5" }, "devDependencies": { - "@babel/core": "^7.21.3", - "@babel/types": "^7.21.3" + "@babel/core": "^7.23.2", + "@babel/types": "^7.23.0" } } diff --git a/packages/reactivity/__tests__/computed.spec.ts b/packages/reactivity/__tests__/computed.spec.ts index c044b5feb35..d9b8f888caf 100644 --- a/packages/reactivity/__tests__/computed.spec.ts +++ b/packages/reactivity/__tests__/computed.spec.ts @@ -184,7 +184,7 @@ describe('reactivity/computed', () => { // mutate n n.value++ // on the 2nd run, plusOne.value should have already updated. - expect(plusOneValues).toMatchObject([1, 2, 2]) + expect(plusOneValues).toMatchObject([1, 2]) }) it('should warn if trying to set a readonly computed', () => { @@ -288,4 +288,167 @@ describe('reactivity/computed', () => { oldValue: 2 }) }) + + // https://github.com/vuejs/core/pull/5912#issuecomment-1497596875 + it('should query deps dirty sequentially', () => { + const cSpy = vi.fn() + + const a = ref({ + v: 1 + }) + const b = computed(() => { + return a.value + }) + const c = computed(() => { + cSpy() + return b.value?.v + }) + const d = computed(() => { + if (b.value) { + return c.value + } + return 0 + }) + + d.value + a.value!.v = 2 + a.value = null + d.value + expect(cSpy).toHaveBeenCalledTimes(1) + }) + + // https://github.com/vuejs/core/pull/5912#issuecomment-1738257692 + it('chained computed dirty reallocation after querying dirty', () => { + let _msg: string | undefined + + const items = ref() + const isLoaded = computed(() => { + return !!items.value + }) + const msg = computed(() => { + if (isLoaded.value) { + return 'The items are loaded' + } else { + return 'The items are not loaded' + } + }) + + effect(() => { + _msg = msg.value + }) + + items.value = [1, 2, 3] + items.value = [1, 2, 3] + items.value = undefined + + expect(_msg).toBe('The items are not loaded') + }) + + it('chained computed dirty reallocation after trigger computed getter', () => { + let _msg: string | undefined + + const items = ref() + const isLoaded = computed(() => { + return !!items.value + }) + const msg = computed(() => { + if (isLoaded.value) { + return 'The items are loaded' + } else { + return 'The items are not loaded' + } + }) + + _msg = msg.value + items.value = [1, 2, 3] + isLoaded.value // <- trigger computed getter + _msg = msg.value + items.value = undefined + _msg = msg.value + + expect(_msg).toBe('The items are not loaded') + }) + + // https://github.com/vuejs/core/pull/5912#issuecomment-1739159832 + it('deps order should be consistent with the last time get value', () => { + const cSpy = vi.fn() + + const a = ref(0) + const b = computed(() => { + return a.value % 3 !== 0 + }) + const c = computed(() => { + cSpy() + if (a.value % 3 === 2) { + return 'expensive' + } + return 'cheap' + }) + const d = computed(() => { + return a.value % 3 === 2 + }) + const e = computed(() => { + if (b.value) { + if (d.value) { + return 'Avoiding expensive calculation' + } + } + return c.value + }) + + e.value + a.value++ + e.value + + expect(e.effect.deps.length).toBe(3) + expect(e.effect.deps.indexOf((b as any).dep)).toBe(0) + expect(e.effect.deps.indexOf((d as any).dep)).toBe(1) + expect(e.effect.deps.indexOf((c as any).dep)).toBe(2) + expect(cSpy).toHaveBeenCalledTimes(2) + + a.value++ + e.value + + expect(cSpy).toHaveBeenCalledTimes(2) + }) + + it('should trigger by the second computed that maybe dirty', () => { + const cSpy = vi.fn() + + const src1 = ref(0) + const src2 = ref(0) + const c1 = computed(() => src1.value) + const c2 = computed(() => (src1.value % 2) + src2.value) + const c3 = computed(() => { + cSpy() + c1.value + c2.value + }) + + c3.value + src1.value = 2 + c3.value + expect(cSpy).toHaveBeenCalledTimes(2) + src2.value = 1 + c3.value + expect(cSpy).toHaveBeenCalledTimes(3) + }) + + it('should trigger the second effect', () => { + const fnSpy = vi.fn() + const v = ref(1) + const c = computed(() => v.value) + + effect(() => { + c.value + }) + effect(() => { + c.value + fnSpy() + }) + + expect(fnSpy).toBeCalledTimes(1) + v.value = 2 + expect(fnSpy).toBeCalledTimes(2) + }) }) diff --git a/packages/reactivity/__tests__/deferredComputed.spec.ts b/packages/reactivity/__tests__/deferredComputed.spec.ts index 100f14ae358..8e78ba959c3 100644 --- a/packages/reactivity/__tests__/deferredComputed.spec.ts +++ b/packages/reactivity/__tests__/deferredComputed.spec.ts @@ -1,57 +1,32 @@ -import { computed, deferredComputed, effect, ref } from '../src' +import { computed, effect, ref } from '../src' describe('deferred computed', () => { - const tick = Promise.resolve() - - test('should only trigger once on multiple mutations', async () => { + test('should not trigger if value did not change', () => { const src = ref(0) - const c = deferredComputed(() => src.value) + const c = computed(() => src.value % 2) const spy = vi.fn() effect(() => { spy(c.value) }) expect(spy).toHaveBeenCalledTimes(1) - src.value = 1 src.value = 2 - src.value = 3 - // not called yet - expect(spy).toHaveBeenCalledTimes(1) - await tick - // should only trigger once - expect(spy).toHaveBeenCalledTimes(2) - expect(spy).toHaveBeenCalledWith(c.value) - }) - test('should not trigger if value did not change', async () => { - const src = ref(0) - const c = deferredComputed(() => src.value % 2) - const spy = vi.fn() - effect(() => { - spy(c.value) - }) - expect(spy).toHaveBeenCalledTimes(1) - src.value = 1 - src.value = 2 - - await tick // should not trigger expect(spy).toHaveBeenCalledTimes(1) src.value = 3 - src.value = 4 src.value = 5 - await tick // should trigger because latest value changes expect(spy).toHaveBeenCalledTimes(2) }) - test('chained computed trigger', async () => { + test('chained computed trigger', () => { const effectSpy = vi.fn() const c1Spy = vi.fn() const c2Spy = vi.fn() const src = ref(0) - const c1 = deferredComputed(() => { + const c1 = computed(() => { c1Spy() return src.value % 2 }) @@ -69,19 +44,18 @@ describe('deferred computed', () => { expect(effectSpy).toHaveBeenCalledTimes(1) src.value = 1 - await tick expect(c1Spy).toHaveBeenCalledTimes(2) expect(c2Spy).toHaveBeenCalledTimes(2) expect(effectSpy).toHaveBeenCalledTimes(2) }) - test('chained computed avoid re-compute', async () => { + test('chained computed avoid re-compute', () => { const effectSpy = vi.fn() const c1Spy = vi.fn() const c2Spy = vi.fn() const src = ref(0) - const c1 = deferredComputed(() => { + const c1 = computed(() => { c1Spy() return src.value % 2 }) @@ -98,26 +72,24 @@ describe('deferred computed', () => { src.value = 2 src.value = 4 src.value = 6 - await tick - // c1 should re-compute once. - expect(c1Spy).toHaveBeenCalledTimes(2) + expect(c1Spy).toHaveBeenCalledTimes(4) // c2 should not have to re-compute because c1 did not change. expect(c2Spy).toHaveBeenCalledTimes(1) // effect should not trigger because c2 did not change. expect(effectSpy).toHaveBeenCalledTimes(1) }) - test('chained computed value invalidation', async () => { + test('chained computed value invalidation', () => { const effectSpy = vi.fn() const c1Spy = vi.fn() const c2Spy = vi.fn() const src = ref(0) - const c1 = deferredComputed(() => { + const c1 = computed(() => { c1Spy() return src.value % 2 }) - const c2 = deferredComputed(() => { + const c2 = computed(() => { c2Spy() return c1.value + 1 }) @@ -139,17 +111,17 @@ describe('deferred computed', () => { expect(c2Spy).toHaveBeenCalledTimes(2) }) - test('sync access of invalidated chained computed should not prevent final effect from running', async () => { + test('sync access of invalidated chained computed should not prevent final effect from running', () => { const effectSpy = vi.fn() const c1Spy = vi.fn() const c2Spy = vi.fn() const src = ref(0) - const c1 = deferredComputed(() => { + const c1 = computed(() => { c1Spy() return src.value % 2 }) - const c2 = deferredComputed(() => { + const c2 = computed(() => { c2Spy() return c1.value + 1 }) @@ -162,14 +134,13 @@ describe('deferred computed', () => { src.value = 1 // sync access c2 c2.value - await tick expect(effectSpy).toHaveBeenCalledTimes(2) }) - test('should not compute if deactivated before scheduler is called', async () => { + test('should not compute if deactivated before scheduler is called', () => { const c1Spy = vi.fn() const src = ref(0) - const c1 = deferredComputed(() => { + const c1 = computed(() => { c1Spy() return src.value % 2 }) @@ -179,7 +150,6 @@ describe('deferred computed', () => { c1.effect.stop() // trigger src.value++ - await tick expect(c1Spy).toHaveBeenCalledTimes(1) }) }) diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index 69d24a76520..2ebb2edea8a 100644 --- a/packages/reactivity/__tests__/effect.spec.ts +++ b/packages/reactivity/__tests__/effect.spec.ts @@ -1,5 +1,4 @@ import { - ref, reactive, effect, stop, @@ -12,7 +11,8 @@ import { readonly, ReactiveEffectRunner } from '../src/index' -import { ITERATE_KEY } from '../src/effect' +import { pauseScheduling, resetScheduling } from '../src/effect' +import { ITERATE_KEY, getDepFromReactive } from '../src/reactiveEffect' describe('reactivity/effect', () => { it('should run the passed function once (wrapped by a effect)', () => { @@ -243,6 +243,22 @@ describe('reactivity/effect', () => { expect(dummy).toBe(undefined) }) + it('should support manipulating an array while observing symbol keyed properties', () => { + const key = Symbol() + let dummy + const array: any = reactive([1, 2, 3]) + effect(() => (dummy = array[key])) + + expect(dummy).toBe(undefined) + array.pop() + array.shift() + array.splice(0, 1) + expect(dummy).toBe(undefined) + array[key] = 'value' + array.length = 0 + expect(dummy).toBe('value') + }) + it('should observe function valued properties', () => { const oldFunc = () => {} const newFunc = () => {} @@ -558,8 +574,8 @@ describe('reactivity/effect', () => { expect(output.fx2).toBe(1 + 3 + 3) expect(fx1Spy).toHaveBeenCalledTimes(1) - // Invoked twice due to change of fx1. - expect(fx2Spy).toHaveBeenCalledTimes(2) + // Invoked due to change of fx1. + expect(fx2Spy).toHaveBeenCalledTimes(1) fx1Spy.mockClear() fx2Spy.mockClear() @@ -585,6 +601,14 @@ describe('reactivity/effect', () => { expect(runner.effect.fn).toBe(otherRunner.effect.fn) }) + it('should wrap if the passed function is a fake effect', () => { + const fakeRunner = () => {} + fakeRunner.effect = {} + const runner = effect(fakeRunner) + expect(fakeRunner).not.toBe(runner) + expect(runner.effect.fn).toBe(fakeRunner) + }) + it('should not run multiple times for a single mutation', () => { let dummy const obj = reactive>({}) @@ -797,26 +821,6 @@ describe('reactivity/effect', () => { expect(dummy).toBe(3) }) - // #5707 - // when an effect completes its run, it should clear the tracking bits of - // its tracked deps. However, if the effect stops itself, the deps list is - // emptied so their bits are never cleared. - it('edge case: self-stopping effect tracking ref', () => { - const c = ref(true) - const runner = effect(() => { - // reference ref - if (!c.value) { - // stop itself while running - stop(runner) - } - }) - // trigger run - c.value = !c.value - // should clear bits - expect((c as any).dep.w).toBe(0) - expect((c as any).dep.n).toBe(0) - }) - it('events: onStop', () => { const onStop = vi.fn() const runner = effect(() => {}, { @@ -991,4 +995,83 @@ describe('reactivity/effect', () => { expect(has).toBe(false) }) }) + + it('should be triggered once with pauseScheduling', () => { + const counter = reactive({ num: 0 }) + + const counterSpy = vi.fn(() => counter.num) + effect(counterSpy) + + counterSpy.mockClear() + + pauseScheduling() + counter.num++ + counter.num++ + resetScheduling() + expect(counterSpy).toHaveBeenCalledTimes(1) + }) + + describe('empty dep cleanup', () => { + it('should remove the dep when the effect is stopped', () => { + const obj = reactive({ prop: 1 }) + expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined() + const runner = effect(() => obj.prop) + const dep = getDepFromReactive(toRaw(obj), 'prop') + expect(dep).toHaveLength(1) + obj.prop = 2 + expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep) + expect(dep).toHaveLength(1) + stop(runner) + expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined() + obj.prop = 3 + runner() + expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined() + }) + + it('should only remove the dep when the last effect is stopped', () => { + const obj = reactive({ prop: 1 }) + expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined() + const runner1 = effect(() => obj.prop) + const dep = getDepFromReactive(toRaw(obj), 'prop') + expect(dep).toHaveLength(1) + const runner2 = effect(() => obj.prop) + expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep) + expect(dep).toHaveLength(2) + obj.prop = 2 + expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep) + expect(dep).toHaveLength(2) + stop(runner1) + expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep) + expect(dep).toHaveLength(1) + obj.prop = 3 + expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep) + expect(dep).toHaveLength(1) + stop(runner2) + expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined() + obj.prop = 4 + runner1() + runner2() + expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined() + }) + + it('should remove the dep when it is no longer used by the effect', () => { + const obj = reactive<{ a: number; b: number; c: 'a' | 'b' }>({ + a: 1, + b: 2, + c: 'a' + }) + expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined() + effect(() => obj[obj.c]) + const depC = getDepFromReactive(toRaw(obj), 'c') + expect(getDepFromReactive(toRaw(obj), 'a')).toHaveLength(1) + expect(getDepFromReactive(toRaw(obj), 'b')).toBeUndefined() + expect(depC).toHaveLength(1) + obj.c = 'b' + obj.a = 4 + expect(getDepFromReactive(toRaw(obj), 'a')).toBeUndefined() + expect(getDepFromReactive(toRaw(obj), 'b')).toHaveLength(1) + expect(getDepFromReactive(toRaw(obj), 'c')).toBe(depC) + expect(depC).toHaveLength(1) + }) + }) }) diff --git a/packages/reactivity/__tests__/gc.spec.ts b/packages/reactivity/__tests__/gc.spec.ts new file mode 100644 index 00000000000..7676a0e12d0 --- /dev/null +++ b/packages/reactivity/__tests__/gc.spec.ts @@ -0,0 +1,81 @@ +import { + ComputedRef, + computed, + effect, + reactive, + shallowRef as ref, + toRaw +} from '../src/index' +import { getDepFromReactive } from '../src/reactiveEffect' + +describe.skipIf(!global.gc)('reactivity/gc', () => { + const gc = () => { + return new Promise(resolve => { + setTimeout(() => { + global.gc!() + resolve() + }) + }) + } + + // #9233 + it('should release computed cache', async () => { + const src = ref<{} | undefined>({}) + const srcRef = new WeakRef(src.value!) + + let c: ComputedRef | undefined = computed(() => src.value) + + c.value // cache src value + src.value = undefined // release value + c = undefined // release computed + + await gc() + expect(srcRef.deref()).toBeUndefined() + }) + + it('should release reactive property dep', async () => { + const src = reactive({ foo: 1 }) + + let c: ComputedRef | undefined = computed(() => src.foo) + + c.value + expect(getDepFromReactive(toRaw(src), 'foo')).not.toBeUndefined() + + c = undefined + await gc() + await gc() + expect(getDepFromReactive(toRaw(src), 'foo')).toBeUndefined() + }) + + it('should not release effect for ref', async () => { + const spy = vi.fn() + const src = ref(0) + + effect(() => { + spy() + src.value + }) + + expect(spy).toHaveBeenCalledTimes(1) + + await gc() + src.value++ + expect(spy).toHaveBeenCalledTimes(2) + }) + + it('should not release effect for reactive', async () => { + const spy = vi.fn() + const src = reactive({ foo: 1 }) + + effect(() => { + spy() + src.foo + }) + + expect(spy).toHaveBeenCalledTimes(1) + + await gc() + src.foo++ + expect(spy).toHaveBeenCalledTimes(2) + }) +}) diff --git a/packages/reactivity/__tests__/reactiveArray.spec.ts b/packages/reactivity/__tests__/reactiveArray.spec.ts index 808c5aa5529..f4eb7b58384 100644 --- a/packages/reactivity/__tests__/reactiveArray.spec.ts +++ b/packages/reactivity/__tests__/reactiveArray.spec.ts @@ -99,6 +99,39 @@ describe('reactivity/reactive/Array', () => { expect(fn).toHaveBeenCalledTimes(1) }) + test('shift on Array should trigger dependency once', () => { + const arr = reactive([1, 2, 3]) + const fn = vi.fn() + effect(() => { + for (let i = 0; i < arr.length; i++) { + arr[i] + } + fn() + }) + expect(fn).toHaveBeenCalledTimes(1) + arr.shift() + expect(fn).toHaveBeenCalledTimes(2) + }) + + //#6018 + test('edge case: avoid trigger effect in deleteProperty when array length-decrease mutation methods called', () => { + const arr = ref([1]) + const fn1 = vi.fn() + const fn2 = vi.fn() + effect(() => { + fn1() + if (arr.value.length > 0) { + arr.value.slice() + fn2() + } + }) + expect(fn1).toHaveBeenCalledTimes(1) + expect(fn2).toHaveBeenCalledTimes(1) + arr.value.splice(0) + expect(fn1).toHaveBeenCalledTimes(2) + expect(fn2).toHaveBeenCalledTimes(1) + }) + test('add existing index on Array should not trigger length dependency', () => { const array = new Array(3) const observed = reactive(array) diff --git a/packages/reactivity/package.json b/packages/reactivity/package.json index f9d5c77b856..7244b9a926a 100644 --- a/packages/reactivity/package.json +++ b/packages/reactivity/package.json @@ -1,6 +1,6 @@ { "name": "@vue/reactivity", - "version": "3.3.4", + "version": "3.4.0-alpha.1", "description": "@vue/reactivity", "main": "index.js", "module": "dist/reactivity.esm-bundler.js", @@ -36,6 +36,6 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/reactivity#readme", "dependencies": { - "@vue/shared": "3.3.4" + "@vue/shared": "3.4.0-alpha.1" } } diff --git a/packages/reactivity/src/baseHandlers.ts b/packages/reactivity/src/baseHandlers.ts index 259b44a1edc..36e4d311b4b 100644 --- a/packages/reactivity/src/baseHandlers.ts +++ b/packages/reactivity/src/baseHandlers.ts @@ -2,7 +2,6 @@ import { reactive, readonly, toRaw, - ReactiveFlags, Target, readonlyMap, reactiveMap, @@ -11,14 +10,14 @@ import { isReadonly, isShallow } from './reactive' -import { TrackOpTypes, TriggerOpTypes } from './operations' +import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants' import { - track, - trigger, - ITERATE_KEY, pauseTracking, - resetTracking + resetTracking, + pauseScheduling, + resetScheduling } from './effect' +import { track, trigger, ITERATE_KEY } from './reactiveEffect' import { isObject, hasOwn, @@ -71,7 +70,9 @@ function createArrayInstrumentations() { ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => { instrumentations[key] = function (this: unknown[], ...args: unknown[]) { pauseTracking() + pauseScheduling() const res = (toRaw(this) as any)[key].apply(this, args) + resetScheduling() resetTracking() return res } diff --git a/packages/reactivity/src/collectionHandlers.ts b/packages/reactivity/src/collectionHandlers.ts index 1d07af3be8c..e8d99840f71 100644 --- a/packages/reactivity/src/collectionHandlers.ts +++ b/packages/reactivity/src/collectionHandlers.ts @@ -1,6 +1,11 @@ -import { toRaw, ReactiveFlags, toReactive, toReadonly } from './reactive' -import { track, trigger, ITERATE_KEY, MAP_KEY_ITERATE_KEY } from './effect' -import { TrackOpTypes, TriggerOpTypes } from './operations' +import { toRaw, toReactive, toReadonly } from './reactive' +import { + track, + trigger, + ITERATE_KEY, + MAP_KEY_ITERATE_KEY +} from './reactiveEffect' +import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants' import { capitalize, hasOwn, hasChanged, toRawType, isMap } from '@vue/shared' export type CollectionTypes = IterableCollections | WeakCollections diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index b24484c9e62..09247360d06 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -1,8 +1,9 @@ import { DebuggerOptions, ReactiveEffect } from './effect' import { Ref, trackRefValue, triggerRefValue } from './ref' -import { isFunction, NOOP } from '@vue/shared' -import { ReactiveFlags, toRaw } from './reactive' +import { hasChanged, isFunction, NOOP } from '@vue/shared' +import { toRaw } from './reactive' import { Dep } from './dep' +import { DirtyLevels, ReactiveFlags } from './constants' declare const ComputedRefSymbol: unique symbol @@ -32,7 +33,6 @@ export class ComputedRefImpl { public readonly __v_isRef = true public readonly [ReactiveFlags.IS_READONLY]: boolean = false - public _dirty = true public _cacheable: boolean constructor( @@ -42,10 +42,7 @@ export class ComputedRefImpl { isSSR: boolean ) { this.effect = new ReactiveEffect(getter, () => { - if (!this._dirty) { - this._dirty = true - triggerRefValue(this) - } + triggerRefValue(this, DirtyLevels.ComputedValueMaybeDirty) }) this.effect.computed = this this.effect.active = this._cacheable = !isSSR @@ -56,9 +53,10 @@ export class ComputedRefImpl { // the computed ref may get wrapped by other proxies e.g. readonly() #3376 const self = toRaw(this) trackRefValue(self) - if (self._dirty || !self._cacheable) { - self._dirty = false - self._value = self.effect.run()! + if (!self._cacheable || self.effect.dirty) { + if (hasChanged(self._value, (self._value = self.effect.run()!))) { + triggerRefValue(self, DirtyLevels.ComputedValueDirty) + } } return self._value } @@ -66,6 +64,16 @@ export class ComputedRefImpl { set value(newValue: T) { this._setter(newValue) } + + // #region polyfill _dirty for backward compatibility third party code for Vue <= 3.3.x + get _dirty() { + return this.effect.dirty + } + + set _dirty(v) { + this.effect.dirty = v + } + // #endregion } /** diff --git a/packages/reactivity/src/constants.ts b/packages/reactivity/src/constants.ts new file mode 100644 index 00000000000..4ad2ec3c7da --- /dev/null +++ b/packages/reactivity/src/constants.ts @@ -0,0 +1,30 @@ +// using literal strings instead of numbers so that it's easier to inspect +// debugger events + +export const enum TrackOpTypes { + GET = 'get', + HAS = 'has', + ITERATE = 'iterate' +} + +export const enum TriggerOpTypes { + SET = 'set', + ADD = 'add', + DELETE = 'delete', + CLEAR = 'clear' +} + +export const enum ReactiveFlags { + SKIP = '__v_skip', + IS_REACTIVE = '__v_isReactive', + IS_READONLY = '__v_isReadonly', + IS_SHALLOW = '__v_isShallow', + RAW = '__v_raw' +} + +export const enum DirtyLevels { + NotDirty = 0, + ComputedValueMaybeDirty = 1, + ComputedValueDirty = 2, + Dirty = 3 +} diff --git a/packages/reactivity/src/deferredComputed.ts b/packages/reactivity/src/deferredComputed.ts index a23122046a4..1dbba1f3f03 100644 --- a/packages/reactivity/src/deferredComputed.ts +++ b/packages/reactivity/src/deferredComputed.ts @@ -1,88 +1,6 @@ -import { Dep } from './dep' -import { ReactiveEffect } from './effect' -import { ComputedGetter, ComputedRef } from './computed' -import { ReactiveFlags, toRaw } from './reactive' -import { trackRefValue, triggerRefValue } from './ref' +import { computed } from './computed' -const tick = /*#__PURE__*/ Promise.resolve() -const queue: any[] = [] -let queued = false - -const scheduler = (fn: any) => { - queue.push(fn) - if (!queued) { - queued = true - tick.then(flush) - } -} - -const flush = () => { - for (let i = 0; i < queue.length; i++) { - queue[i]() - } - queue.length = 0 - queued = false -} - -class DeferredComputedRefImpl { - public dep?: Dep = undefined - - private _value!: T - private _dirty = true - public readonly effect: ReactiveEffect - - public readonly __v_isRef = true - public readonly [ReactiveFlags.IS_READONLY] = true - - constructor(getter: ComputedGetter) { - let compareTarget: any - let hasCompareTarget = false - let scheduled = false - this.effect = new ReactiveEffect(getter, (computedTrigger?: boolean) => { - if (this.dep) { - if (computedTrigger) { - compareTarget = this._value - hasCompareTarget = true - } else if (!scheduled) { - const valueToCompare = hasCompareTarget ? compareTarget : this._value - scheduled = true - hasCompareTarget = false - scheduler(() => { - if (this.effect.active && this._get() !== valueToCompare) { - triggerRefValue(this) - } - scheduled = false - }) - } - // chained upstream computeds are notified synchronously to ensure - // value invalidation in case of sync access; normal effects are - // deferred to be triggered in scheduler. - for (const e of this.dep) { - if (e.computed instanceof DeferredComputedRefImpl) { - e.scheduler!(true /* computedTrigger */) - } - } - } - this._dirty = true - }) - this.effect.computed = this as any - } - - private _get() { - if (this._dirty) { - this._dirty = false - return (this._value = this.effect.run()!) - } - return this._value - } - - get value() { - trackRefValue(this) - // the computed ref may get wrapped by other proxies e.g. readonly() #3376 - return toRaw(this)._get() - } -} - -export function deferredComputed(getter: () => T): ComputedRef { - return new DeferredComputedRefImpl(getter) as any -} +/** + * @deprecated use `computed` instead. See #5912 + */ +export const deferredComputed = computed diff --git a/packages/reactivity/src/dep.ts b/packages/reactivity/src/dep.ts index 8677f575756..eafb2a8af3f 100644 --- a/packages/reactivity/src/dep.ts +++ b/packages/reactivity/src/dep.ts @@ -1,57 +1,17 @@ -import { ReactiveEffect, trackOpBit } from './effect' +import type { ReactiveEffect } from './effect' +import type { ComputedRefImpl } from './computed' -export type Dep = Set & TrackedMarkers - -/** - * wasTracked and newTracked maintain the status for several levels of effect - * tracking recursion. One bit per level is used to define whether the dependency - * was/is tracked. - */ -type TrackedMarkers = { - /** - * wasTracked - */ - w: number - /** - * newTracked - */ - n: number +export type Dep = Map & { + cleanup: () => void + computed?: ComputedRefImpl } -export const createDep = (effects?: ReactiveEffect[]): Dep => { - const dep = new Set(effects) as Dep - dep.w = 0 - dep.n = 0 +export const createDep = ( + cleanup: () => void, + computed?: ComputedRefImpl +): Dep => { + const dep = new Map() as Dep + dep.cleanup = cleanup + dep.computed = computed return dep } - -export const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0 - -export const newTracked = (dep: Dep): boolean => (dep.n & trackOpBit) > 0 - -export const initDepMarkers = ({ deps }: ReactiveEffect) => { - if (deps.length) { - for (let i = 0; i < deps.length; i++) { - deps[i].w |= trackOpBit // set was tracked - } - } -} - -export const finalizeDepMarkers = (effect: ReactiveEffect) => { - const { deps } = effect - if (deps.length) { - let ptr = 0 - for (let i = 0; i < deps.length; i++) { - const dep = deps[i] - if (wasTracked(dep) && !newTracked(dep)) { - dep.delete(effect) - } else { - deps[ptr++] = dep - } - // clear bits - dep.w &= ~trackOpBit - dep.n &= ~trackOpBit - } - deps.length = ptr - } -} diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index d4a34edfef4..3a25295011c 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -1,34 +1,8 @@ -import { TrackOpTypes, TriggerOpTypes } from './operations' -import { extend, isArray, isIntegerKey, isMap } from '@vue/shared' +import { NOOP, extend } from '@vue/shared' +import type { ComputedRefImpl } from './computed' +import { DirtyLevels, TrackOpTypes, TriggerOpTypes } from './constants' +import type { Dep } from './dep' import { EffectScope, recordEffectScope } from './effectScope' -import { - createDep, - Dep, - finalizeDepMarkers, - initDepMarkers, - newTracked, - wasTracked -} from './dep' -import { ComputedRefImpl } from './computed' - -// The main WeakMap that stores {target -> key -> dep} connections. -// Conceptually, it's easier to think of a dependency as a Dep class -// which maintains a Set of subscribers, but we simply store them as -// raw Sets to reduce memory overhead. -type KeyToDepMap = Map -const targetMap = new WeakMap() - -// The number of effects currently being tracked recursively. -let effectTrackDepth = 0 - -export let trackOpBit = 1 - -/** - * The bitwise track markers support at most 30 levels of recursion. - * This value is chosen to enable modern JS engines to use a SMI on all platforms. - * When recursion depth is greater, fall back to using a full cleanup. - */ -const maxMarkerBits = 30 export type EffectScheduler = (...args: any[]) => any @@ -47,13 +21,9 @@ export type DebuggerEventExtraInfo = { export let activeEffect: ReactiveEffect | undefined -export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '') -export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key iterate' : '') - export class ReactiveEffect { active = true deps: Dep[] = [] - parent: ReactiveEffect | undefined = undefined /** * Can be attached after creation @@ -64,10 +34,6 @@ export class ReactiveEffect { * @internal */ allowRecurse?: boolean - /** - * @internal - */ - private deferStop?: boolean onStop?: () => void // dev only @@ -75,77 +41,115 @@ export class ReactiveEffect { // dev only onTrigger?: (event: DebuggerEvent) => void + /** + * @internal + */ + _dirtyLevel = DirtyLevels.Dirty + /** + * @internal + */ + _trackId = 0 + /** + * @internal + */ + _runnings = 0 + /** + * @internal + */ + _queryings = 0 + /** + * @internal + */ + _depsLength = 0 + constructor( public fn: () => T, - public scheduler: EffectScheduler | null = null, + public trigger: () => void, + public scheduler?: EffectScheduler, scope?: EffectScope ) { recordEffectScope(this, scope) } + public get dirty() { + if (this._dirtyLevel === DirtyLevels.ComputedValueMaybeDirty) { + this._dirtyLevel = DirtyLevels.NotDirty + this._queryings++ + pauseTracking() + for (const dep of this.deps) { + if (dep.computed) { + triggerComputed(dep.computed) + if (this._dirtyLevel >= DirtyLevels.ComputedValueDirty) { + break + } + } + } + resetTracking() + this._queryings-- + } + return this._dirtyLevel >= DirtyLevels.ComputedValueDirty + } + + public set dirty(v) { + this._dirtyLevel = v ? DirtyLevels.Dirty : DirtyLevels.NotDirty + } + run() { + this._dirtyLevel = DirtyLevels.NotDirty if (!this.active) { return this.fn() } - let parent: ReactiveEffect | undefined = activeEffect let lastShouldTrack = shouldTrack - while (parent) { - if (parent === this) { - return - } - parent = parent.parent - } + let lastEffect = activeEffect try { - this.parent = activeEffect - activeEffect = this shouldTrack = true - - trackOpBit = 1 << ++effectTrackDepth - - if (effectTrackDepth <= maxMarkerBits) { - initDepMarkers(this) - } else { - cleanupEffect(this) - } + activeEffect = this + this._runnings++ + preCleanupEffect(this) return this.fn() } finally { - if (effectTrackDepth <= maxMarkerBits) { - finalizeDepMarkers(this) - } - - trackOpBit = 1 << --effectTrackDepth - - activeEffect = this.parent + postCleanupEffect(this) + this._runnings-- + activeEffect = lastEffect shouldTrack = lastShouldTrack - this.parent = undefined - - if (this.deferStop) { - this.stop() - } } } stop() { - // stopped while running itself - defer the cleanup - if (activeEffect === this) { - this.deferStop = true - } else if (this.active) { - cleanupEffect(this) - if (this.onStop) { - this.onStop() - } + if (this.active) { + preCleanupEffect(this) + postCleanupEffect(this) + this.onStop?.() this.active = false } } } -function cleanupEffect(effect: ReactiveEffect) { - const { deps } = effect - if (deps.length) { - for (let i = 0; i < deps.length; i++) { - deps[i].delete(effect) +function triggerComputed(computed: ComputedRefImpl) { + return computed.value +} + +function preCleanupEffect(effect: ReactiveEffect) { + effect._trackId++ + effect._depsLength = 0 +} + +function postCleanupEffect(effect: ReactiveEffect) { + if (effect.deps && effect.deps.length > effect._depsLength) { + for (let i = effect._depsLength; i < effect.deps.length; i++) { + cleanupDepEffect(effect.deps[i], effect) + } + effect.deps.length = effect._depsLength + } +} + +function cleanupDepEffect(dep: Dep, effect: ReactiveEffect) { + const trackId = dep.get(effect) + if (trackId !== undefined && effect._trackId !== trackId) { + dep.delete(effect) + if (dep.size === 0) { + dep.cleanup() } - deps.length = 0 } } @@ -181,11 +185,15 @@ export function effect( fn: () => T, options?: ReactiveEffectOptions ): ReactiveEffectRunner { - if ((fn as ReactiveEffectRunner).effect) { + if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) { fn = (fn as ReactiveEffectRunner).effect.fn } - const _effect = new ReactiveEffect(fn) + const _effect = new ReactiveEffect(fn, NOOP, () => { + if (_effect.dirty) { + _effect.run() + } + }) if (options) { extend(_effect, options) if (options.scope) recordEffectScope(_effect, options.scope) @@ -208,6 +216,8 @@ export function stop(runner: ReactiveEffectRunner) { } export let shouldTrack = true +export let pauseScheduleStack = 0 + const trackStack: boolean[] = [] /** @@ -234,196 +244,70 @@ export function resetTracking() { shouldTrack = last === undefined ? true : last } -/** - * Tracks access to a reactive property. - * - * This will check which effect is running at the moment and record it as dep - * which records all effects that depend on the reactive property. - * - * @param target - Object holding the reactive property. - * @param type - Defines the type of access to the reactive property. - * @param key - Identifier of the reactive property to track. - */ -export function track(target: object, type: TrackOpTypes, key: unknown) { - if (shouldTrack && activeEffect) { - let depsMap = targetMap.get(target) - if (!depsMap) { - targetMap.set(target, (depsMap = new Map())) - } - let dep = depsMap.get(key) - if (!dep) { - depsMap.set(key, (dep = createDep())) - } - - const eventInfo = __DEV__ - ? { effect: activeEffect, target, type, key } - : undefined +export function pauseScheduling() { + pauseScheduleStack++ +} - trackEffects(dep, eventInfo) +export function resetScheduling() { + pauseScheduleStack-- + while (!pauseScheduleStack && queueEffectSchedulers.length) { + queueEffectSchedulers.shift()!() } } -export function trackEffects( +export function trackEffect( + effect: ReactiveEffect, dep: Dep, debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { - let shouldTrack = false - if (effectTrackDepth <= maxMarkerBits) { - if (!newTracked(dep)) { - dep.n |= trackOpBit // set newly tracked - shouldTrack = !wasTracked(dep) - } - } else { - // Full cleanup mode. - shouldTrack = !dep.has(activeEffect!) - } - - if (shouldTrack) { - dep.add(activeEffect!) - activeEffect!.deps.push(dep) - if (__DEV__ && activeEffect!.onTrack) { - activeEffect!.onTrack( - extend( - { - effect: activeEffect! - }, - debuggerEventExtraInfo! - ) - ) - } - } -} - -/** - * Finds all deps associated with the target (or a specific property) and - * triggers the effects stored within. - * - * @param target - The reactive object. - * @param type - Defines the type of the operation that needs to trigger effects. - * @param key - Can be used to target a specific reactive property in the target object. - */ -export function trigger( - target: object, - type: TriggerOpTypes, - key?: unknown, - newValue?: unknown, - oldValue?: unknown, - oldTarget?: Map | Set -) { - const depsMap = targetMap.get(target) - if (!depsMap) { - // never been tracked - return - } - - let deps: (Dep | undefined)[] = [] - if (type === TriggerOpTypes.CLEAR) { - // collection being cleared - // trigger all effects for target - deps = [...depsMap.values()] - } else if (key === 'length' && isArray(target)) { - const newLength = Number(newValue) - depsMap.forEach((dep, key) => { - if (key === 'length' || key >= newLength) { - deps.push(dep) - } - }) - } else { - // schedule runs for SET | ADD | DELETE - if (key !== void 0) { - deps.push(depsMap.get(key)) - } - - // also run for iteration key on ADD | DELETE | Map.SET - switch (type) { - case TriggerOpTypes.ADD: - if (!isArray(target)) { - deps.push(depsMap.get(ITERATE_KEY)) - if (isMap(target)) { - deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)) - } - } else if (isIntegerKey(key)) { - // new index added to array -> length changes - deps.push(depsMap.get('length')) - } - break - case TriggerOpTypes.DELETE: - if (!isArray(target)) { - deps.push(depsMap.get(ITERATE_KEY)) - if (isMap(target)) { - deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)) - } - } - break - case TriggerOpTypes.SET: - if (isMap(target)) { - deps.push(depsMap.get(ITERATE_KEY)) - } - break - } - } - - const eventInfo = __DEV__ - ? { target, type, key, newValue, oldValue, oldTarget } - : undefined - - if (deps.length === 1) { - if (deps[0]) { - if (__DEV__) { - triggerEffects(deps[0], eventInfo) - } else { - triggerEffects(deps[0]) - } - } - } else { - const effects: ReactiveEffect[] = [] - for (const dep of deps) { - if (dep) { - effects.push(...dep) + if (dep.get(effect) !== effect._trackId) { + dep.set(effect, effect._trackId) + const oldDep = effect.deps[effect._depsLength] + if (oldDep !== dep) { + if (oldDep) { + cleanupDepEffect(oldDep, effect) } + effect.deps[effect._depsLength++] = dep + } else { + effect._depsLength++ } if (__DEV__) { - triggerEffects(createDep(effects), eventInfo) - } else { - triggerEffects(createDep(effects)) + effect.onTrack?.(extend({ effect }, debuggerEventExtraInfo!)) } } } -export function triggerEffects( - dep: Dep | ReactiveEffect[], - debuggerEventExtraInfo?: DebuggerEventExtraInfo -) { - // spread into array for stabilization - const effects = isArray(dep) ? dep : [...dep] - for (const effect of effects) { - if (effect.computed) { - triggerEffect(effect, debuggerEventExtraInfo) - } - } - for (const effect of effects) { - if (!effect.computed) { - triggerEffect(effect, debuggerEventExtraInfo) - } - } -} +const queueEffectSchedulers: (() => void)[] = [] -function triggerEffect( - effect: ReactiveEffect, +export function triggerEffects( + dep: Dep, + dirtyLevel: DirtyLevels, debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { - if (effect !== activeEffect || effect.allowRecurse) { - if (__DEV__ && effect.onTrigger) { - effect.onTrigger(extend({ effect }, debuggerEventExtraInfo)) + pauseScheduling() + for (const effect of dep.keys()) { + if (!effect.allowRecurse && effect._runnings) { + continue } - if (effect.scheduler) { - effect.scheduler() - } else { - effect.run() + if ( + effect._dirtyLevel < dirtyLevel && + (!effect._runnings || dirtyLevel !== DirtyLevels.ComputedValueDirty) + ) { + const lastDirtyLevel = effect._dirtyLevel + effect._dirtyLevel = dirtyLevel + if ( + lastDirtyLevel === DirtyLevels.NotDirty && + (!effect._queryings || dirtyLevel !== DirtyLevels.ComputedValueDirty) + ) { + if (__DEV__) { + effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo)) + } + effect.trigger() + if (effect.scheduler) { + queueEffectSchedulers.push(effect.scheduler) + } + } } } -} - -export function getDepFromReactive(object: any, key: string | number | symbol) { - return targetMap.get(object)?.get(key) + resetScheduling() } diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index ee4da5b1935..9497527e81e 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -31,7 +31,6 @@ export { shallowReadonly, markRaw, toRaw, - ReactiveFlags /* @remove */, type Raw, type DeepReadonly, type ShallowReactive, @@ -49,12 +48,11 @@ export { deferredComputed } from './deferredComputed' export { effect, stop, - trigger, - track, enableTracking, pauseTracking, resetTracking, - ITERATE_KEY, + pauseScheduling, + resetScheduling, ReactiveEffect, type ReactiveEffectRunner, type ReactiveEffectOptions, @@ -63,6 +61,7 @@ export { type DebuggerEvent, type DebuggerEventExtraInfo } from './effect' +export { trigger, track, ITERATE_KEY } from './reactiveEffect' export { effectScope, EffectScope, @@ -71,5 +70,6 @@ export { } from './effectScope' export { TrackOpTypes /* @remove */, - TriggerOpTypes /* @remove */ -} from './operations' + TriggerOpTypes /* @remove */, + ReactiveFlags /* @remove */ +} from './constants' diff --git a/packages/reactivity/src/operations.ts b/packages/reactivity/src/operations.ts deleted file mode 100644 index 1b96e982571..00000000000 --- a/packages/reactivity/src/operations.ts +++ /dev/null @@ -1,15 +0,0 @@ -// using literal strings instead of numbers so that it's easier to inspect -// debugger events - -export const enum TrackOpTypes { - GET = 'get', - HAS = 'has', - ITERATE = 'iterate' -} - -export const enum TriggerOpTypes { - SET = 'set', - ADD = 'add', - DELETE = 'delete', - CLEAR = 'clear' -} diff --git a/packages/reactivity/src/reactive.ts b/packages/reactivity/src/reactive.ts index 1881955cf1c..2904c69abe2 100644 --- a/packages/reactivity/src/reactive.ts +++ b/packages/reactivity/src/reactive.ts @@ -12,14 +12,7 @@ import { shallowReadonlyCollectionHandlers } from './collectionHandlers' import type { UnwrapRefSimple, Ref, RawSymbol } from './ref' - -export const enum ReactiveFlags { - SKIP = '__v_skip', - IS_REACTIVE = '__v_isReactive', - IS_READONLY = '__v_isReadonly', - IS_SHALLOW = '__v_isShallow', - RAW = '__v_raw' -} +import { ReactiveFlags } from './constants' export interface Target { [ReactiveFlags.SKIP]?: boolean diff --git a/packages/reactivity/src/reactiveEffect.ts b/packages/reactivity/src/reactiveEffect.ts new file mode 100644 index 00000000000..d3474db3da1 --- /dev/null +++ b/packages/reactivity/src/reactiveEffect.ts @@ -0,0 +1,150 @@ +import { isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared' +import { DirtyLevels, TrackOpTypes, TriggerOpTypes } from './constants' +import { createDep, Dep } from './dep' +import { + activeEffect, + pauseScheduling, + resetScheduling, + shouldTrack, + trackEffect, + triggerEffects +} from './effect' + +// The main WeakMap that stores {target -> key -> dep} connections. +// Conceptually, it's easier to think of a dependency as a Dep class +// which maintains a Set of subscribers, but we simply store them as +// raw Sets to reduce memory overhead. +type KeyToDepMap = Map +const targetMap = new WeakMap() + +export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '') +export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key iterate' : '') + +/** + * Tracks access to a reactive property. + * + * This will check which effect is running at the moment and record it as dep + * which records all effects that depend on the reactive property. + * + * @param target - Object holding the reactive property. + * @param type - Defines the type of access to the reactive property. + * @param key - Identifier of the reactive property to track. + */ +export function track(target: object, type: TrackOpTypes, key: unknown) { + if (shouldTrack && activeEffect) { + let depsMap = targetMap.get(target) + if (!depsMap) { + targetMap.set(target, (depsMap = new Map())) + } + let dep = depsMap.get(key) + if (!dep) { + depsMap.set(key, (dep = createDep(() => depsMap!.delete(key)))) + } + trackEffect( + activeEffect, + dep, + __DEV__ + ? { + target, + type, + key + } + : void 0 + ) + } +} + +/** + * Finds all deps associated with the target (or a specific property) and + * triggers the effects stored within. + * + * @param target - The reactive object. + * @param type - Defines the type of the operation that needs to trigger effects. + * @param key - Can be used to target a specific reactive property in the target object. + */ +export function trigger( + target: object, + type: TriggerOpTypes, + key?: unknown, + newValue?: unknown, + oldValue?: unknown, + oldTarget?: Map | Set +) { + const depsMap = targetMap.get(target) + if (!depsMap) { + // never been tracked + return + } + + let deps: (Dep | undefined)[] = [] + if (type === TriggerOpTypes.CLEAR) { + // collection being cleared + // trigger all effects for target + deps = [...depsMap.values()] + } else if (key === 'length' && isArray(target)) { + const newLength = Number(newValue) + depsMap.forEach((dep, key) => { + if (key === 'length' || (!isSymbol(key) && key >= newLength)) { + deps.push(dep) + } + }) + } else { + // schedule runs for SET | ADD | DELETE + if (key !== void 0) { + deps.push(depsMap.get(key)) + } + + // also run for iteration key on ADD | DELETE | Map.SET + switch (type) { + case TriggerOpTypes.ADD: + if (!isArray(target)) { + deps.push(depsMap.get(ITERATE_KEY)) + if (isMap(target)) { + deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)) + } + } else if (isIntegerKey(key)) { + // new index added to array -> length changes + deps.push(depsMap.get('length')) + } + break + case TriggerOpTypes.DELETE: + if (!isArray(target)) { + deps.push(depsMap.get(ITERATE_KEY)) + if (isMap(target)) { + deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)) + } + } + break + case TriggerOpTypes.SET: + if (isMap(target)) { + deps.push(depsMap.get(ITERATE_KEY)) + } + break + } + } + + pauseScheduling() + for (const dep of deps) { + if (dep) { + triggerEffects( + dep, + DirtyLevels.Dirty, + __DEV__ + ? { + target, + type, + key, + newValue, + oldValue, + oldTarget + } + : void 0 + ) + } + } + resetScheduling() +} + +export function getDepFromReactive(object: any, key: string | number | symbol) { + return targetMap.get(object)?.get(key) +} diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 915f5760878..5a4dd710eab 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -1,11 +1,10 @@ import { activeEffect, - getDepFromReactive, shouldTrack, - trackEffects, + trackEffect, triggerEffects } from './effect' -import { TrackOpTypes, TriggerOpTypes } from './operations' +import { DirtyLevels, TrackOpTypes, TriggerOpTypes } from './constants' import { isArray, hasChanged, IfAny, isFunction, isObject } from '@vue/shared' import { isProxy, @@ -18,6 +17,8 @@ import { import type { ShallowReactiveMarker } from './reactive' import { CollectionTypes } from './collectionHandlers' import { createDep, Dep } from './dep' +import { ComputedRefImpl } from './computed' +import { getDepFromReactive } from './reactiveEffect' declare const RefSymbol: unique symbol export declare const RawSymbol: unique symbol @@ -40,32 +41,44 @@ type RefBase = { export function trackRefValue(ref: RefBase) { if (shouldTrack && activeEffect) { ref = toRaw(ref) - if (__DEV__) { - trackEffects(ref.dep || (ref.dep = createDep()), { - target: ref, - type: TrackOpTypes.GET, - key: 'value' - }) - } else { - trackEffects(ref.dep || (ref.dep = createDep())) - } + trackEffect( + activeEffect, + ref.dep || + (ref.dep = createDep( + () => (ref.dep = undefined), + ref instanceof ComputedRefImpl ? ref : undefined + )), + __DEV__ + ? { + target: ref, + type: TrackOpTypes.GET, + key: 'value' + } + : void 0 + ) } } -export function triggerRefValue(ref: RefBase, newVal?: any) { +export function triggerRefValue( + ref: RefBase, + dirtyLevel: DirtyLevels = DirtyLevels.Dirty, + newVal?: any +) { ref = toRaw(ref) const dep = ref.dep if (dep) { - if (__DEV__) { - triggerEffects(dep, { - target: ref, - type: TriggerOpTypes.SET, - key: 'value', - newValue: newVal - }) - } else { - triggerEffects(dep) - } + triggerEffects( + dep, + dirtyLevel, + __DEV__ + ? { + target: ref, + type: TriggerOpTypes.SET, + key: 'value', + newValue: newVal + } + : void 0 + ) } } @@ -158,7 +171,7 @@ class RefImpl { if (hasChanged(newVal, this._rawValue)) { this._rawValue = newVal this._value = useDirectValue ? newVal : toReactive(newVal) - triggerRefValue(this, newVal) + triggerRefValue(this, DirtyLevels.Dirty, newVal) } } } @@ -189,7 +202,7 @@ class RefImpl { * @see {@link https://vuejs.org/api/reactivity-advanced.html#triggerref} */ export function triggerRef(ref: Ref) { - triggerRefValue(ref, __DEV__ ? ref.value : void 0) + triggerRefValue(ref, DirtyLevels.Dirty, __DEV__ ? ref.value : void 0) } export type MaybeRef = T | Ref diff --git a/packages/runtime-core/__tests__/apiOptions.spec.ts b/packages/runtime-core/__tests__/apiOptions.spec.ts index ca712e0d3ac..94f9db1b028 100644 --- a/packages/runtime-core/__tests__/apiOptions.spec.ts +++ b/packages/runtime-core/__tests__/apiOptions.spec.ts @@ -1,7 +1,7 @@ /** * @vitest-environment jsdom */ -import { vi, type Mock } from 'vitest' +import { type Mock } from 'vitest' import { h, nodeOps, diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index f24ce80b9df..bddfc5ff541 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -1205,4 +1205,42 @@ describe('api: watch', () => { expect(countWE).toBe(3) expect(countW).toBe(2) }) + + const options = [ + { name: 'only trigger once watch' }, + { + deep: true, + name: 'only trigger once watch with deep' + }, + { + flush: 'sync', + name: 'only trigger once watch with flush: sync' + }, + { + flush: 'pre', + name: 'only trigger once watch with flush: pre' + }, + { + immediate: true, + name: 'only trigger once watch with immediate' + } + ] as const + test.each(options)('$name', async option => { + const count = ref(0) + const cb = vi.fn() + + watch(count, cb, { once: true, ...option }) + + count.value++ + await nextTick() + + expect(count.value).toBe(1) + expect(cb).toHaveBeenCalledTimes(1) + + count.value++ + await nextTick() + + expect(count.value).toBe(2) + expect(cb).toHaveBeenCalledTimes(1) + }) }) diff --git a/packages/runtime-core/__tests__/componentSlots.spec.ts b/packages/runtime-core/__tests__/componentSlots.spec.ts index f08f1910cd4..708fb20c423 100644 --- a/packages/runtime-core/__tests__/componentSlots.spec.ts +++ b/packages/runtime-core/__tests__/componentSlots.spec.ts @@ -134,9 +134,11 @@ describe('component: slots', () => { } const oldSlots = { - header: 'header' + header: 'header', + footer: undefined } const newSlots = { + header: undefined, footer: 'footer' } diff --git a/packages/runtime-core/__tests__/components/Teleport.spec.ts b/packages/runtime-core/__tests__/components/Teleport.spec.ts index 7371f53f7b6..c0dbd4cae9c 100644 --- a/packages/runtime-core/__tests__/components/Teleport.spec.ts +++ b/packages/runtime-core/__tests__/components/Teleport.spec.ts @@ -172,6 +172,31 @@ describe('renderer: teleport', () => { expect(serializeInner(target)).toBe('') }) + // #6347 + test('descendent component should be unmounted when teleport is disabled and unmounted', () => { + const root = nodeOps.createElement('div') + + const CompWithHook = { + render() { + return [h('p'), h('p')] + }, + beforeUnmount: vi.fn(), + unmounted: vi.fn() + } + + render( + h(() => [h(Teleport, { to: null, disabled: true }, h(CompWithHook))]), + root + ) + expect(CompWithHook.beforeUnmount).toBeCalledTimes(0) + expect(CompWithHook.unmounted).toBeCalledTimes(0) + + render(null, root) + + expect(CompWithHook.beforeUnmount).toBeCalledTimes(1) + expect(CompWithHook.unmounted).toBeCalledTimes(1) + }) + test('multiple teleport with same target', () => { const target = nodeOps.createElement('div') const root = nodeOps.createElement('div') diff --git a/packages/runtime-core/__tests__/hmr.spec.ts b/packages/runtime-core/__tests__/hmr.spec.ts index db713a3f276..2e989e368a3 100644 --- a/packages/runtime-core/__tests__/hmr.spec.ts +++ b/packages/runtime-core/__tests__/hmr.spec.ts @@ -20,7 +20,7 @@ const { createRecord, rerender, reload } = __VUE_HMR_RUNTIME__ registerRuntimeCompiler(compileToFunction) function compileToFunction(template: string) { - const { code } = baseCompile(template) + const { code } = baseCompile(template, { hoistStatic: true, hmr: true }) const render = new Function('Vue', code)( runtimeTest ) as InternalRenderFunction @@ -567,4 +567,40 @@ describe('hot module replacement', () => { rerender(parentId, compileToFunction(`2`)) expect(serializeInner(root)).toBe(`2`) }) + + // #6978, #7138, #7114 + test('hoisted children array inside v-for', () => { + const root = nodeOps.createElement('div') + const appId = 'test-app-id' + const App: ComponentOptions = { + __hmrId: appId, + render: compileToFunction( + `
+
1
+
+

2

+

3

` + ) + } + createRecord(appId, App) + + render(h(App), root) + expect(serializeInner(root)).toBe( + `
1
1

2

3

` + ) + + // move the

3

into the
1
+ rerender( + appId, + compileToFunction( + `
+
1

3

+
+

2

` + ) + ) + expect(serializeInner(root)).toBe( + `
1

3

1

3

2

` + ) + }) }) diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index d3cfd47c6be..759804b97f1 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -18,10 +18,14 @@ import { createVNode, withDirectives, vModelCheckbox, - renderSlot + renderSlot, + Transition, + createCommentVNode, + vShow } from '@vue/runtime-dom' import { renderToString, SSRContext } from '@vue/server-renderer' -import { PatchFlags } from '../../shared/src' +import { PatchFlags } from '@vue/shared' +import { vShowOldKey } from '../../runtime-dom/src/directives/vShow' function mountWithHydration(html: string, render: () => any) { const container = document.createElement('div') @@ -393,6 +397,28 @@ describe('SSR hydration', () => { ) }) + // #6152 + test('Teleport (disabled + as component root)', () => { + const { container } = mountWithHydration( + '
Parent fragment
Teleport content
', + () => [ + h('div', 'Parent fragment'), + h(() => + h(Teleport, { to: 'body', disabled: true }, [ + h('div', 'Teleport content') + ]) + ) + ] + ) + expect(document.body.innerHTML).toBe('') + expect(container.innerHTML).toBe( + '
Parent fragment
Teleport content
' + ) + expect( + `Hydration completed but contains mismatches.` + ).not.toHaveBeenWarned() + }) + test('Teleport (as component root)', () => { const teleportContainer = document.createElement('div') teleportContainer.id = 'teleport4' @@ -994,6 +1020,74 @@ describe('SSR hydration', () => { expect(`mismatch`).not.toHaveBeenWarned() }) + test('transition appear', () => { + const { vnode, container } = mountWithHydration( + ``, + () => + h( + Transition, + { appear: true }, + { + default: () => h('div', 'foo') + } + ) + ) + expect(container.firstChild).toMatchInlineSnapshot(` +
+ foo +
+ `) + expect(vnode.el).toBe(container.firstChild) + expect(`mismatch`).not.toHaveBeenWarned() + }) + + test('transition appear with v-if', () => { + const show = false + const { vnode, container } = mountWithHydration( + ``, + () => + h( + Transition, + { appear: true }, + { + default: () => (show ? h('div', 'foo') : createCommentVNode('')) + } + ) + ) + expect(container.firstChild).toMatchInlineSnapshot('') + expect(vnode.el).toBe(container.firstChild) + expect(`mismatch`).not.toHaveBeenWarned() + }) + + test('transition appear with v-show', () => { + const show = false + const { vnode, container } = mountWithHydration( + ``, + () => + h( + Transition, + { appear: true }, + { + default: () => + withDirectives(createVNode('div', null, 'foo'), [[vShow, show]]) + } + ) + ) + expect(container.firstChild).toMatchInlineSnapshot(` + + `) + expect((container.firstChild as any)[vShowOldKey]).toBe('') + expect(vnode.el).toBe(container.firstChild) + expect(`mismatch`).not.toHaveBeenWarned() + }) + describe('mismatch handling', () => { test('text node', () => { const { container } = mountWithHydration(`foo`, () => 'bar') diff --git a/packages/runtime-core/__tests__/scheduler.spec.ts b/packages/runtime-core/__tests__/scheduler.spec.ts index 6246a87e8f7..119d0f7080c 100644 --- a/packages/runtime-core/__tests__/scheduler.spec.ts +++ b/packages/runtime-core/__tests__/scheduler.spec.ts @@ -143,6 +143,7 @@ describe('scheduler', () => { queueJob(job1) // cb2 should execute before the job queueJob(cb2) + queueJob(cb3) } cb1.pre = true @@ -152,9 +153,60 @@ describe('scheduler', () => { cb2.pre = true cb2.id = 1 + const cb3 = () => { + calls.push('cb3') + } + cb3.pre = true + cb3.id = 1 + queueJob(cb1) await nextTick() - expect(calls).toEqual(['cb1', 'cb2', 'job1']) + expect(calls).toEqual(['cb1', 'cb2', 'cb3', 'job1']) + }) + + it('should insert jobs after pre jobs with the same id', async () => { + const calls: string[] = [] + const job1 = () => { + calls.push('job1') + } + job1.id = 1 + job1.pre = true + const job2 = () => { + calls.push('job2') + queueJob(job5) + queueJob(job6) + } + job2.id = 2 + job2.pre = true + const job3 = () => { + calls.push('job3') + } + job3.id = 2 + job3.pre = true + const job4 = () => { + calls.push('job4') + } + job4.id = 3 + job4.pre = true + const job5 = () => { + calls.push('job5') + } + job5.id = 2 + const job6 = () => { + calls.push('job6') + } + job6.id = 2 + job6.pre = true + + // We need several jobs to test this properly, otherwise + // findInsertionIndex can yield the correct index by chance + queueJob(job4) + queueJob(job2) + queueJob(job3) + queueJob(job1) + + await nextTick() + expect(calls).toEqual(['job1', 'job2', 'job3', 'job6', 'job5', 'job4']) }) it('preFlushCb inside queueJob', async () => { diff --git a/packages/runtime-core/package.json b/packages/runtime-core/package.json index e4209d719da..c89fd157ef2 100644 --- a/packages/runtime-core/package.json +++ b/packages/runtime-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/runtime-core", - "version": "3.3.4", + "version": "3.4.0-alpha.1", "description": "@vue/runtime-core", "main": "index.js", "module": "dist/runtime-core.esm-bundler.js", @@ -32,7 +32,7 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-core#readme", "dependencies": { - "@vue/shared": "3.3.4", - "@vue/reactivity": "3.3.4" + "@vue/shared": "3.4.0-alpha.1", + "@vue/reactivity": "3.4.0-alpha.1" } } diff --git a/packages/runtime-core/src/apiAsyncComponent.ts b/packages/runtime-core/src/apiAsyncComponent.ts index 342339042ef..535cb83fb5d 100644 --- a/packages/runtime-core/src/apiAsyncComponent.ts +++ b/packages/runtime-core/src/apiAsyncComponent.ts @@ -187,6 +187,7 @@ export function defineAsyncComponent< if (instance.parent && isKeepAlive(instance.parent.vnode)) { // parent is keep-alive, force update so the loaded component's // name is taken into account + instance.parent.effect.dirty = true queueJob(instance.parent.update) } }) diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index 7d4158db34e..fcad3d39d69 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -227,7 +227,7 @@ export function createAppAPI( }) } - const installedPlugins = new Set() + const installedPlugins = new WeakSet() let isMounted = false diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 1b85ba12d19..cedebb01af6 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -75,6 +75,7 @@ export interface WatchOptionsBase extends DebuggerOptions { export interface WatchOptions extends WatchOptionsBase { immediate?: Immediate deep?: boolean + once?: boolean } export type WatchStopHandle = () => void @@ -172,8 +173,16 @@ export function watch = false>( function doWatch( source: WatchSource | WatchSource[] | WatchEffect | object, cb: WatchCallback | null, - { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ + { immediate, deep, flush, once, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ ): WatchStopHandle { + if (cb && once) { + const _cb = cb + cb = (...args) => { + _cb(...args) + unwatch() + } + } + if (__DEV__ && !cb) { if (immediate !== undefined) { warn( @@ -187,6 +196,12 @@ function doWatch( `watch(source, callback, options?) signature.` ) } + if (once !== undefined) { + warn( + `watch() "once" option is only respected when using the ` + + `watch(source, callback, options?) signature.` + ) + } } const warnInvalidSource = (s: unknown) => { @@ -307,7 +322,7 @@ function doWatch( ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE) : INITIAL_WATCHER_VALUE const job: SchedulerJob = () => { - if (!effect.active) { + if (!effect.active || !effect.dirty) { return } if (cb) { @@ -361,7 +376,14 @@ function doWatch( scheduler = () => queueJob(job) } - const effect = new ReactiveEffect(getter, scheduler) + const effect = new ReactiveEffect(getter, NOOP, scheduler) + + const unwatch = () => { + effect.stop() + if (instance && instance.scope) { + remove(instance.scope.effects!, effect) + } + } if (__DEV__) { effect.onTrack = onTrack @@ -384,13 +406,6 @@ function doWatch( effect.run() } - const unwatch = () => { - effect.stop() - if (instance && instance.scope) { - remove(instance.scope.effects!, effect) - } - } - if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch) return unwatch } diff --git a/packages/runtime-core/src/compat/componentAsync.ts b/packages/runtime-core/src/compat/componentAsync.ts index 0a0dee72b63..5b2ac22c233 100644 --- a/packages/runtime-core/src/compat/componentAsync.ts +++ b/packages/runtime-core/src/compat/componentAsync.ts @@ -18,7 +18,10 @@ type LegacyAsyncComponent = ( reject?: (reason?: any) => void ) => LegacyAsyncReturnValue | undefined -const normalizedAsyncComponentMap = new Map() +const normalizedAsyncComponentMap = new WeakMap< + LegacyAsyncComponent, + Component +>() export function convertLegacyAsyncComponent(comp: LegacyAsyncComponent) { if (normalizedAsyncComponentMap.has(comp)) { diff --git a/packages/runtime-core/src/compat/componentFunctional.ts b/packages/runtime-core/src/compat/componentFunctional.ts index 1b1146bbab9..90d24e1ba73 100644 --- a/packages/runtime-core/src/compat/componentFunctional.ts +++ b/packages/runtime-core/src/compat/componentFunctional.ts @@ -8,11 +8,10 @@ import { InternalSlots } from '../componentSlots' import { getCompatListeners } from './instanceListeners' import { compatH } from './renderFn' -const normalizedFunctionalComponentMap = new Map< +const normalizedFunctionalComponentMap = new WeakMap< ComponentOptions, FunctionalComponent >() - export const legacySlotProxyHandlers: ProxyHandler = { get(target, key: string) { const slot = target[key] diff --git a/packages/runtime-core/src/compat/instance.ts b/packages/runtime-core/src/compat/instance.ts index 141f0bf0a0f..e6baeda6a8e 100644 --- a/packages/runtime-core/src/compat/instance.ts +++ b/packages/runtime-core/src/compat/instance.ts @@ -58,6 +58,7 @@ export interface LegacyPublicProperties { export function installCompatInstanceProperties(map: PublicPropertiesMap) { const set = (target: any, key: any, val: any) => { target[key] = val + return target[key] } const del = (target: any, key: any) => { diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index dc575aafff9..7b552c8f92a 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -206,11 +206,9 @@ export type ComponentPublicInstance< > = { $: ComponentInternalInstance $data: D - $props: Prettify< - MakeDefaultsOptional extends true - ? Partial & Omit

- : P & PublicProps - > + $props: MakeDefaultsOptional extends true + ? Partial & Omit & PublicProps, keyof Defaults> + : Prettify

& PublicProps $attrs: Data $refs: Data $slots: UnwrapSlotsType @@ -269,7 +267,12 @@ export const publicPropertiesMap: PublicPropertiesMap = $root: i => getPublicInstance(i.root), $emit: i => i.emit, $options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type), - $forceUpdate: i => i.f || (i.f = () => queueJob(i.update)), + $forceUpdate: i => + i.f || + (i.f = () => { + i.effect.dirty = true + queueJob(i.update) + }), $nextTick: i => i.n || (i.n = nextTick.bind(i.proxy!)), $watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP) } as PublicPropertiesMap) diff --git a/packages/runtime-core/src/componentSlots.ts b/packages/runtime-core/src/componentSlots.ts index afc5f03933b..980ee799186 100644 --- a/packages/runtime-core/src/componentSlots.ts +++ b/packages/runtime-core/src/componentSlots.ts @@ -234,7 +234,7 @@ export const updateSlots = ( // delete stale slots if (needDeletionCheck) { for (const key in slots) { - if (!isInternalKey(key) && !(key in deletionComparisonTarget)) { + if (!isInternalKey(key) && deletionComparisonTarget[key] == null) { delete slots[key] } } diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts index 9cb80b94ef0..ef0632384d6 100644 --- a/packages/runtime-core/src/components/BaseTransition.ts +++ b/packages/runtime-core/src/components/BaseTransition.ts @@ -246,6 +246,7 @@ const BaseTransitionImpl: ComponentOptions = { // #6835 // it also needs to be updated when active is undefined if (instance.update.active !== false) { + instance.effect.dirty = true instance.update() } } diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 727a7f04287..572796e7e5c 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -492,10 +492,12 @@ function createSuspenseBoundary( container } = suspense + // if there's a transition happening we need to wait it to finish. + let delayEnter: boolean | null = false if (suspense.isHydrating) { suspense.isHydrating = false } else if (!resume) { - const delayEnter = + delayEnter = activeBranch && pendingBranch!.transition && pendingBranch!.transition.mode === 'out-in' @@ -503,6 +505,7 @@ function createSuspenseBoundary( activeBranch!.transition!.afterLeave = () => { if (pendingId === suspense.pendingId) { move(pendingBranch!, container, anchor, MoveType.ENTER) + queuePostFlushCb(effects) } } } @@ -539,8 +542,8 @@ function createSuspenseBoundary( } parent = parent.parent } - // no pending parent suspense, flush all jobs - if (!hasUnresolvedAncestor) { + // no pending parent suspense nor transition, flush all jobs + if (!hasUnresolvedAncestor && !delayEnter) { queuePostFlushCb(effects) } suspense.effects = [] diff --git a/packages/runtime-core/src/components/Teleport.ts b/packages/runtime-core/src/components/Teleport.ts index 4659c9b712c..c4de8621284 100644 --- a/packages/runtime-core/src/components/Teleport.ts +++ b/packages/runtime-core/src/components/Teleport.ts @@ -252,7 +252,7 @@ export const TeleportImpl = { parentSuspense: SuspenseBoundary | null, optimized: boolean, { um: unmount, o: { remove: hostRemove } }: RendererInternals, - doRemove: Boolean + doRemove: boolean ) { const { shapeFlag, children, anchor, targetAnchor, target, props } = vnode @@ -260,20 +260,19 @@ export const TeleportImpl = { hostRemove(targetAnchor!) } - // an unmounted teleport should always remove its children if not disabled - if (doRemove || !isTeleportDisabled(props)) { - hostRemove(anchor!) - if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { - for (let i = 0; i < (children as VNode[]).length; i++) { - const child = (children as VNode[])[i] - unmount( - child, - parentComponent, - parentSuspense, - true, - !!child.dynamicChildren - ) - } + // an unmounted teleport should always unmount its children whether it's disabled or not + doRemove && hostRemove(anchor!) + if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { + const shouldRemove = doRemove || !isTeleportDisabled(props) + for (let i = 0; i < (children as VNode[]).length; i++) { + const child = (children as VNode[])[i] + unmount( + child, + parentComponent, + parentSuspense, + shouldRemove, + !!child.dynamicChildren + ) } } }, @@ -427,7 +426,7 @@ function updateCssVars(vnode: VNode) { const ctx = vnode.ctx if (ctx && ctx.ut) { let node = (vnode.children as VNode[])[0].el! - while (node !== vnode.targetAnchor) { + while (node && node !== vnode.targetAnchor) { if (node.nodeType === 1) node.setAttribute('data-v-owner', ctx.uid) node = node.nextSibling } diff --git a/packages/runtime-core/src/h.ts b/packages/runtime-core/src/h.ts index 73b27107b8b..4ca90262f2a 100644 --- a/packages/runtime-core/src/h.ts +++ b/packages/runtime-core/src/h.ts @@ -174,6 +174,14 @@ export function h

( children?: RawChildren | RawSlots ): VNode +// catch all types +export function h(type: string | Component, children?: RawChildren): VNode +export function h

( + type: string | Component

, + props?: (RawProps & P) | ({} extends P ? null : never), + children?: RawChildren | RawSlots +): VNode + // Actual implementation export function h(type: any, propsOrChildren?: any, children?: any): VNode { const l = arguments.length diff --git a/packages/runtime-core/src/hmr.ts b/packages/runtime-core/src/hmr.ts index 1ce66a3da1e..cdf291989bd 100644 --- a/packages/runtime-core/src/hmr.ts +++ b/packages/runtime-core/src/hmr.ts @@ -93,6 +93,7 @@ function rerender(id: string, newRender?: Function) { instance.renderCache = [] // this flag forces child components with slot content to update isHmrUpdating = true + instance.effect.dirty = true instance.update() isHmrUpdating = false }) @@ -137,6 +138,7 @@ function reload(id: string, newComp: HMRComponent) { // 4. Force the parent instance to re-render. This will cause all updated // components to be unmounted and re-mounted. Queue the update so that we // don't end up forcing the same parent to re-render multiple times. + instance.parent.effect.dirty = true queueJob(instance.parent.update) } else if (instance.appContext.reload) { // root instance mounted via createApp() has a reload method diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index a0b34c91448..6dfe532ec31 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -15,7 +15,7 @@ import { ComponentInternalInstance } from './component' import { invokeDirectiveHook } from './directives' import { warn } from './warning' import { PatchFlags, ShapeFlags, isReservedProp, isOn } from '@vue/shared' -import { RendererInternals } from './renderer' +import { needTransition, RendererInternals } from './renderer' import { setRef } from './rendererTemplateRef' import { SuspenseImpl, @@ -143,8 +143,10 @@ export function createHydrationFunctions( __DEV__ && warn( `Hydration text mismatch:` + - `\n- Client: ${JSON.stringify((node as Text).data)}` + - `\n- Server: ${JSON.stringify(vnode.children)}` + `\n- Server rendered: ${JSON.stringify( + (node as Text).data + )}` + + `\n- Client rendered: ${JSON.stringify(vnode.children)}` ) ;(node as Text).data = vnode.children as string } @@ -153,7 +155,17 @@ export function createHydrationFunctions( break case Comment: if (domType !== DOMNodeTypes.COMMENT || isFragmentStart) { - nextNode = onMismatch() + if ((node as Element).tagName.toLowerCase() === 'template') { + const content = (vnode.el! as HTMLTemplateElement).content + .firstChild! + + // replace