Merge branch 'main' into fix/8466

This commit is contained in:
edison 2023-10-19 22:50:41 -05:00 committed by GitHub
commit ff1fa17024
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
133 changed files with 4006 additions and 2822 deletions

View File

@ -74,12 +74,7 @@ module.exports = {
}, },
// Node scripts // Node scripts
{ {
files: [ files: ['scripts/**', '*.{js,ts}', 'packages/**/index.js'],
'scripts/**',
'*.{js,ts}',
'packages/**/index.js',
'packages/size-check/**'
],
rules: { rules: {
'no-restricted-globals': 'off', 'no-restricted-globals': 'off',
'no-restricted-syntax': 'off' 'no-restricted-syntax': 'off'

View File

@ -1,5 +1,8 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Feature Request
url: https://github.com/vuejs/rfcs/discussions
about: Suggest new features for consideration
- name: Discord Chat - name: Discord Chat
url: https://chat.vuejs.org url: https://chat.vuejs.org
about: Ask questions and discuss with other Vue users in real time. about: Ask questions and discuss with other Vue users in real time.

View File

@ -1,39 +0,0 @@
name: "\U0001F680 New feature proposal"
description: Suggest an idea for this project
labels: [":sparkles: feature request"]
body:
- type: markdown
attributes:
value: |
**Before You Start...**
This form is only for submitting feature requests. If you have a usage question
or are unsure if this is really a bug, make sure to:
- Read the [docs](https://vuejs.org/)
- Ask on [Discord Chat](https://chat.vuejs.org/)
- Ask on [GitHub Discussions](https://github.com/vuejs/core/discussions)
- Look for / ask questions on [Stack Overflow](https://stackoverflow.com/questions/ask?tags=vue.js)
Also try to search for your issue - another user may have already requested something similar!
- type: textarea
id: problem-description
attributes:
label: What problem does this feature solve?
description: |
Explain your use case, context, and rationale behind this feature request. More importantly, what is the **end user experience** you are trying to build that led to the need for this feature?
An important design goal of Vue is keeping the API surface small and straightforward. In general, we only consider adding new features that solve a problem that cannot be easily dealt with using existing APIs (i.e. not just an alternative way of doing things that can already be done). The problem should also be common enough to justify the addition.
placeholder: Problem description
validations:
required: true
- type: textarea
id: proposed-API
attributes:
label: What does the proposed API look like?
description: |
Describe how you propose to solve the problem and provide code samples of how the API would work once implemented. Note that you can use [Markdown](https://guides.github.com/features/mastering-markdown/) to format your code blocks.
placeholder: Assumed API
validations:
required: true

View File

@ -57,7 +57,7 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before
## Development Setup ## Development Setup
You will need [Node.js](https://nodejs.org) **version 16+**, and [PNPM](https://pnpm.io) **version 7+**. 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. 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.
@ -185,7 +185,7 @@ Builds and watches `vue/dist/vue-runtime.esm-bundler.js` with all deps inlined u
### `nr dev-compiler` ### `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` ### `nr test`
@ -248,8 +248,6 @@ This repository employs a [monorepo](https://en.wikipedia.org/wiki/Monorepo) set
- `template-explorer`: A development tool for debugging compiler output, continuously deployed at https://template-explorer.vuejs.org/. To run it locally, run [`nr dev-compiler`](#nr-dev-compiler). - `template-explorer`: A development tool for debugging compiler output, continuously deployed at https://template-explorer.vuejs.org/. To run it locally, run [`nr dev-compiler`](#nr-dev-compiler).
- `size-check`: Used for checking built bundle sizes on CI.
### Importing Packages ### Importing Packages
The packages can import each other directly using their package names. Note that when importing a package, the name listed in its `package.json` should be used. Most of the time the `@vue/` prefix is needed: The packages can import each other directly using their package names. Note that when importing a package, the name listed in its `package.json` should be used. Most of the time the `@vue/` prefix is needed:
@ -261,7 +259,7 @@ import { h } from '@vue/runtime-core'
This is made possible via several configurations: This is made possible via several configurations:
- For TypeScript, `compilerOptions.paths` in `tsconfig.json` - For TypeScript, `compilerOptions.paths` in `tsconfig.json`
- Vitest and Rollup share the sae set of aliases from `scripts/aliases.js` - Vitest and Rollup share the same set of aliases from `scripts/aliases.js`
- For plain Node.js, they are linked using [PNPM Workspaces](https://pnpm.io/workspaces). - For plain Node.js, they are linked using [PNPM Workspaces](https://pnpm.io/workspaces).
### Package Dependencies ### Package Dependencies
@ -330,4 +328,4 @@ Funds donated via Patreon go directly to support Evan You's full-time work on Vu
Thank you to all the people who have already contributed to Vue.js! Thank you to all the people who have already contributed to Vue.js!
<a href="https://github.com/vuejs/vue/graphs/contributors"><img src="https://opencollective.com/vuejs/contributors.svg?width=890" /></a> <a href="https://github.com/vuejs/core/graphs/contributors"><img src="https://opencollective.com/vuejs/contributors.svg?width=890" /></a>

View File

@ -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

50
.github/renovate.json5 vendored Normal file
View File

@ -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'
]
}

33
.github/workflows/autofix.yml vendored Normal file
View File

@ -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

33
.github/workflows/canary-minor.yml vendored Normal file
View File

@ -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 }}

View File

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: Release environment: Release
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v2

View File

@ -14,8 +14,10 @@ jobs:
unit-test: unit-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
env:
PUPPETEER_SKIP_DOWNLOAD: 'true'
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v2
@ -26,9 +28,6 @@ jobs:
node-version: 18 node-version: 18
cache: 'pnpm' cache: 'pnpm'
- name: Skip Puppeteer download
run: echo "PUPPETEER_SKIP_DOWNLOAD=1" >> $GITHUB_ENV
- run: pnpm install - run: pnpm install
- name: Run unit tests - name: Run unit tests
@ -37,8 +36,10 @@ jobs:
unit-test-windows: unit-test-windows:
runs-on: windows-latest runs-on: windows-latest
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
env:
PUPPETEER_SKIP_DOWNLOAD: 'true'
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v2
@ -49,9 +50,6 @@ jobs:
node-version: 18 node-version: 18
cache: 'pnpm' cache: 'pnpm'
- name: Skip Puppeteer download
run: echo "PUPPETEER_SKIP_DOWNLOAD=1" >> $env:GITHUB_ENV
- run: pnpm install - run: pnpm install
- name: Run compiler unit tests - name: Run compiler unit tests
@ -64,12 +62,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Setup cache for Chromium binary - name: Setup cache for Chromium binary
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: ~/.cache/puppeteer/chrome path: ~/.cache/puppeteer
key: chromium-${{ hashFiles('pnpm-lock.yaml') }} key: chromium-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install pnpm - name: Install pnpm
@ -82,6 +80,7 @@ jobs:
cache: 'pnpm' cache: 'pnpm'
- run: pnpm install - run: pnpm install
- run: node node_modules/puppeteer/install.js
- name: Run e2e tests - name: Run e2e tests
run: pnpm run test-e2e run: pnpm run test-e2e
@ -89,8 +88,10 @@ jobs:
lint-and-test-dts: lint-and-test-dts:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
env:
PUPPETEER_SKIP_DOWNLOAD: 'true'
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v2
@ -101,36 +102,13 @@ jobs:
node-version: 18 node-version: 18
cache: 'pnpm' cache: 'pnpm'
- name: Skip Puppeteer download
run: echo "PUPPETEER_SKIP_DOWNLOAD=1" >> $GITHUB_ENV
- run: pnpm install - run: pnpm install
- name: Run eslint - name: Run eslint
run: pnpm run lint run: pnpm run lint
# - name: Run prettier - name: Run prettier
# run: pnpm run format-check run: pnpm run format-check
- name: Run type declaration tests - name: Run type declaration tests
run: pnpm run test-dts run: pnpm run test-dts
size:
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
env:
CI_JOB_NUMBER: 1
steps:
- uses: actions/checkout@v3
- name: Install pnpm
uses: pnpm/action-setup@v2
- name: Set node version to 18
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- run: PUPPETEER_SKIP_DOWNLOAD=1 pnpm install
- run: pnpm run size

View File

@ -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'

52
.github/workflows/size-data.yml vendored Normal file
View File

@ -0,0 +1,52 @@
name: size data
on:
push:
branches:
- main
pull_request:
branches:
- main
permissions:
contents: read
env:
PUPPETEER_SKIP_DOWNLOAD: 'true'
jobs:
upload:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v2
- name: Set node version to LTS
uses: actions/setup-node@v3
with:
node-version: lts/*
cache: pnpm
- name: Install dependencies
run: pnpm install
- run: pnpm run size
- name: Upload Size Data
uses: actions/upload-artifact@v3
with:
name: size-data
path: temp/size
- name: Save PR number
if: ${{github.event_name == 'pull_request'}}
run: echo ${{ github.event.number }} > ./pr.txt
- uses: actions/upload-artifact@v3
if: ${{github.event_name == 'pull_request'}}
with:
name: pr-number
path: pr.txt

84
.github/workflows/size-report.yml vendored Normal file
View File

@ -0,0 +1,84 @@
name: size report
on:
workflow_run:
workflows: ['size data']
types:
- completed
permissions:
contents: read
pull-requests: write
issues: write
env:
PUPPETEER_SKIP_DOWNLOAD: 'true'
jobs:
size-report:
runs-on: ubuntu-latest
if: >
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v2
- name: Set node version to LTS
uses: actions/setup-node@v3
with:
node-version: lts/*
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Download PR number
uses: dawidd6/action-download-artifact@v2
with:
name: pr-number
run_id: ${{ github.event.workflow_run.id }}
- name: Read PR Number
id: pr-number
uses: juliangruber/read-file-action@v1
with:
path: ./pr.txt
- name: Download Size Data
uses: dawidd6/action-download-artifact@v2
with:
name: size-data
run_id: ${{ github.event.workflow_run.id }}
path: temp/size
- name: Download Previous Size Data
uses: dawidd6/action-download-artifact@v2
with:
branch: main
workflow: size-data.yml
event: push
name: size-data
path: temp/size-prev
if_no_artifact_found: warn
- name: Compare size
run: pnpm tsx scripts/size-report.ts > size-report.md
- name: Read Size Report
id: size-report
uses: juliangruber/read-file-action@v1
with:
path: ./size-report.md
- name: Create Comment
uses: actions-cool/maintain-one-comment@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
number: ${{ steps.pr-number.outputs.content }}
body: |
${{ steps.size-report.outputs.content }}
<!-- VUE_CORE_SIZE -->
body-include: '<!-- VUE_CORE_SIZE -->'

View File

@ -4,6 +4,6 @@ Vue.js is an MIT-licensed open source project with its ongoing development made
<p align="center"> <p align="center">
<a target="_blank" href="https://sponsors.vuejs.org/backers.svg"> <a target="_blank" href="https://sponsors.vuejs.org/backers.svg">
<img alt="sponsors" src="https://sponsors.vuejs.org/backers.svg"> <img alt="sponsors" src="https://sponsors.vuejs.org/backers.svg?v1">
</a> </a>
</p> </p>

View File

@ -1,19 +1,21 @@
{ {
"private": true, "private": true,
"version": "3.3.4", "version": "3.3.4",
"packageManager": "pnpm@8.4.0", "packageManager": "pnpm@8.9.2",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "node scripts/dev.js", "dev": "node scripts/dev.js",
"build": "node scripts/build.js", "build": "node scripts/build.js",
"build-dts": "tsc -p tsconfig.build.json && rollup -c rollup.dts.config.js", "build-dts": "tsc -p tsconfig.build.json && rollup -c rollup.dts.config.js",
"size": "run-s size-global size-baseline", "clean": "rimraf packages/*/dist temp .eslintcache",
"size-global": "node scripts/build.js vue runtime-dom -f global -p", "size": "run-s \"size-*\" && tsx scripts/usage-size.ts",
"size-baseline": "node scripts/build.js vue -f esm-bundler-runtime && node scripts/build.js runtime-dom runtime-core reactivity shared -f esm-bundler && cd packages/size-check && vite build && node brotli", "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",
"size-esm": "node scripts/build.js runtime-dom runtime-core reactivity shared -f esm-bundler",
"check": "tsc --incremental --noEmit", "check": "tsc --incremental --noEmit",
"lint": "eslint --cache --ext .ts packages/*/{src,__tests__}/**.ts", "lint": "eslint --cache --ext .ts packages/*/{src,__tests__}/**.ts",
"format": "prettier --write --cache --parser typescript \"**/*.[tj]s?(x)\"", "format": "prettier --write --cache \"**/*.[tj]s?(x)\"",
"format-check": "prettier --check --cache --parser typescript \"**/*.[tj]s?(x)\"", "format-check": "prettier --check --cache \"**/*.[tj]s?(x)\"",
"test": "vitest", "test": "vitest",
"test-unit": "vitest -c vitest.unit.config.ts", "test-unit": "vitest -c vitest.unit.config.ts",
"test-e2e": "node scripts/build.js vue -f global -d && vitest -c vitest.e2e.config.ts", "test-e2e": "node scripts/build.js vue -f global -d && vitest -c vitest.e2e.config.ts",
@ -29,13 +31,13 @@
"dev-sfc-serve": "vite packages/sfc-playground --host", "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", "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", "serve": "serve",
"open": "open http://localhost:5000/packages/template-explorer/local.html", "open": "open http://localhost:3000/packages/template-explorer/local.html",
"build-sfc-playground": "run-s build-compiler-cjs build-runtime-esm build-ssr-esm build-sfc-playground-self", "build-sfc-playground": "run-s build-all-cjs build-runtime-esm build-ssr-esm build-sfc-playground-self",
"build-compiler-cjs": "node scripts/build.js compiler reactivity-transform shared -af cjs", "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-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-ssr-esm": "node scripts/build.js compiler-sfc server-renderer -f esm-browser",
"build-sfc-playground-self": "cd packages/sfc-playground && npm run build", "build-sfc-playground-self": "cd packages/sfc-playground && npm run build",
"preinstall": "node ./scripts/preinstall.js", "preinstall": "npx only-allow pnpm",
"postinstall": "simple-git-hooks" "postinstall": "simple-git-hooks"
}, },
"simple-git-hooks": { "simple-git-hooks": {
@ -52,53 +54,57 @@
] ]
}, },
"engines": { "engines": {
"node": ">=16.11.0" "node": ">=18.12.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/parser": "^7.21.3", "@babel/parser": "^7.23.0",
"@babel/types": "^7.21.3", "@babel/types": "^7.23.0",
"@rollup/plugin-alias": "^4.0.3", "@rollup/plugin-alias": "^5.0.1",
"@rollup/plugin-commonjs": "^24.0.1", "@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-json": "^6.0.0", "@rollup/plugin-json": "^6.0.1",
"@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^5.0.2", "@rollup/plugin-replace": "^5.0.4",
"@rollup/plugin-terser": "^0.4.0", "@rollup/plugin-terser": "^0.4.4",
"@types/hash-sum": "^1.0.0", "@types/hash-sum": "^1.0.1",
"@types/node": "^16.4.7", "@types/node": "^18.18.6",
"@typescript-eslint/parser": "^5.56.0", "@typescript-eslint/parser": "^6.8.0",
"@vitest/coverage-istanbul": "^0.29.7", "@vitest/coverage-istanbul": "^0.34.4",
"@vue/consolidate": "0.17.3", "@vue/consolidate": "0.17.3",
"chalk": "^4.1.0", "conventional-changelog-cli": "^4.1.0",
"conventional-changelog-cli": "^2.0.31", "enquirer": "^2.4.1",
"enquirer": "^2.3.2", "esbuild": "^0.19.5",
"esbuild": "^0.17.4", "esbuild-plugin-polyfill-node": "^0.3.0",
"esbuild-plugin-polyfill-node": "^0.2.0", "eslint": "^8.51.0",
"eslint": "^8.33.0", "eslint-plugin-jest": "^27.4.2",
"eslint-plugin-jest": "^27.2.1",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"execa": "^4.0.2", "execa": "^8.0.1",
"jsdom": "^21.1.0", "jsdom": "^22.1.0",
"lint-staged": "^10.2.10", "lint-staged": "^15.0.2",
"lodash": "^4.17.15", "lodash": "^4.17.21",
"magic-string": "^0.30.0", "magic-string": "^0.30.5",
"marked": "^4.0.10", "markdown-table": "^3.0.3",
"minimist": "^1.2.0", "marked": "^9.1.2",
"minimist": "^1.2.8",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^2.7.1", "picocolors": "^1.0.0",
"pug": "^3.0.1", "prettier": "^3.0.3",
"puppeteer": "~19.6.0", "pretty-bytes": "^6.1.1",
"rollup": "^3.20.2", "pug": "^3.0.2",
"rollup-plugin-dts": "^5.3.0", "puppeteer": "~21.2.1",
"rollup-plugin-esbuild": "^5.0.0", "rimraf": "^5.0.5",
"rollup": "^3.29.4",
"rollup-plugin-dts": "^6.1.0",
"rollup-plugin-esbuild": "^6.1.0",
"rollup-plugin-polyfill-node": "^0.12.0", "rollup-plugin-polyfill-node": "^0.12.0",
"semver": "^7.3.2", "semver": "^7.5.4",
"serve": "^12.0.0", "serve": "^14.2.1",
"simple-git-hooks": "^2.8.1", "simple-git-hooks": "^2.9.0",
"terser": "^5.15.1", "terser": "^5.22.0",
"todomvc-app-css": "^2.3.0", "todomvc-app-css": "^2.4.2",
"tslib": "^2.5.0", "tslib": "^2.6.2",
"typescript": "^5.0.0", "tsx": "^3.14.0",
"typescript": "^5.1.6",
"vite": "^4.3.0", "vite": "^4.3.0",
"vitest": "^0.30.1" "vitest": "^0.34.4"
} }
} }

View File

@ -32,12 +32,12 @@
}, },
"homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-core#readme", "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-core#readme",
"dependencies": { "dependencies": {
"@babel/parser": "^7.21.3", "@babel/parser": "^7.23.0",
"@vue/shared": "3.3.4", "@vue/shared": "3.3.4",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/types": "^7.21.3" "@babel/types": "^7.23.0"
} }
} }

View File

@ -32,7 +32,7 @@ export function walkIdentifiers(
root.body[0].type === 'ExpressionStatement' && root.body[0].type === 'ExpressionStatement' &&
root.body[0].expression root.body[0].expression
;(walk as any)(root, { walk(root, {
enter(node: Node & { scopeIds?: Set<string> }, parent: Node | undefined) { enter(node: Node & { scopeIds?: Set<string> }, parent: Node | undefined) {
parent && parentStack.push(parent) parent && parentStack.push(parent)
if ( if (

View File

@ -8,9 +8,9 @@ export function decodeHtmlBrowser(raw: string, asAttr = false): string {
} }
if (asAttr) { if (asAttr) {
decoder.innerHTML = `<div foo="${raw.replace(/"/g, '&quot;')}">` decoder.innerHTML = `<div foo="${raw.replace(/"/g, '&quot;')}">`
return decoder.children[0].getAttribute('foo') as string return decoder.children[0].getAttribute('foo')!
} else { } else {
decoder.innerHTML = raw decoder.innerHTML = raw
return decoder.textContent as string return decoder.textContent!
} }
} }

View File

@ -62,6 +62,24 @@ return { fn }
})" })"
`; `;
exports[`SFC compile <script setup> > <script> and <script setup> co-usage > keep original semi style 1`] = `
"export default {
props: ['item'],
emits: ['change'],
setup(__props, { expose: __expose, emit: __emit }) {
__expose();
console.log('test')
const props = __props;
const emit = __emit;
(function () {})()
return { props, emit }
}
}"
`;
exports[`SFC compile <script setup> > <script> and <script setup> co-usage > script first 1`] = ` exports[`SFC compile <script setup> > <script> and <script setup> co-usage > script first 1`] = `
"import { x } from './x' "import { x } from './x'
@ -612,75 +630,6 @@ return { foo, bar, baz, y, z }
}" }"
`; `;
exports[`SFC compile <script setup> > defineProps/defineEmits in multi-variable declaration (full removal) 1`] = `
"export default {
props: ['item'],
emits: ['a'],
setup(__props, { expose: __expose, emit }) {
__expose();
const props = __props;
return { props, emit }
}
}"
`;
exports[`SFC compile <script setup> > defineProps/defineEmits in multi-variable declaration 1`] = `
"export default {
props: ['item'],
emits: ['a'],
setup(__props, { expose: __expose, emit }) {
__expose();
const props = __props;
const a = 1;
return { props, a, emit }
}
}"
`;
exports[`SFC compile <script setup> > defineProps/defineEmits in multi-variable declaration fix #6757 1`] = `
"export default {
props: ['item'],
emits: ['a'],
setup(__props, { expose: __expose, emit }) {
__expose();
const props = __props;
const a = 1;
return { a, props, emit }
}
}"
`;
exports[`SFC compile <script setup> > defineProps/defineEmits in multi-variable declaration fix #7422 1`] = `
"export default {
props: ['item'],
emits: ['foo'],
setup(__props, { expose: __expose, emit: emits }) {
__expose();
const props = __props;
const a = 0,
b = 0;
return { props, emits, a, b }
}
}"
`;
exports[`SFC compile <script setup> > dev mode import usage check > TS annotations 1`] = ` exports[`SFC compile <script setup> > dev mode import usage check > TS annotations 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
import { Foo, Bar, Baz, Qux, Fred } from './x' import { Foo, Bar, Baz, Qux, Fred } from './x'
@ -745,6 +694,21 @@ return { get vMyDir() { return vMyDir } }
})" })"
`; `;
exports[`SFC compile <script setup> > dev mode import usage check > dynamic arguments 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
import { FooBar, foo, bar, unused } from './x'
export default /*#__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) {
__expose();
return { get FooBar() { return FooBar }, get foo() { return foo }, get bar() { return bar } }
}
})"
`;
exports[`SFC compile <script setup> > dev mode import usage check > js template string interpolations 1`] = ` exports[`SFC compile <script setup> > dev mode import usage check > js template string interpolations 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
import { VAR, VAR2, VAR3 } from './x' import { VAR, VAR2, VAR3 } from './x'
@ -775,6 +739,21 @@ return { get FooBaz() { return FooBaz }, get Last() { return Last } }
})" })"
`; `;
exports[`SFC compile <script setup> > dev mode import usage check > template ref 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
import { foo, bar, Baz } from './foo'
export default /*#__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) {
__expose();
return { get foo() { return foo }, get bar() { return bar }, get Baz() { return Baz } }
}
})"
`;
exports[`SFC compile <script setup> > dev mode import usage check > vue interpolations 1`] = ` exports[`SFC compile <script setup> > dev mode import usage check > vue interpolations 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
import { x, y, z, x$y } from './x' import { x, y, z, x$y } from './x'

View File

@ -68,64 +68,6 @@ describe('SFC compile <script setup>', () => {
assertCode(content) assertCode(content)
}) })
test('defineProps/defineEmits in multi-variable declaration', () => {
const { content } = compile(`
<script setup>
const props = defineProps(['item']),
a = 1,
emit = defineEmits(['a']);
</script>
`)
assertCode(content)
expect(content).toMatch(`const a = 1;`) // test correct removal
expect(content).toMatch(`props: ['item'],`)
expect(content).toMatch(`emits: ['a'],`)
})
// #6757
test('defineProps/defineEmits in multi-variable declaration fix #6757 ', () => {
const { content } = compile(`
<script setup>
const a = 1,
props = defineProps(['item']),
emit = defineEmits(['a']);
</script>
`)
assertCode(content)
expect(content).toMatch(`const a = 1;`) // test correct removal
expect(content).toMatch(`props: ['item'],`)
expect(content).toMatch(`emits: ['a'],`)
})
// #7422
test('defineProps/defineEmits in multi-variable declaration fix #7422', () => {
const { content } = compile(`
<script setup>
const props = defineProps(['item']),
emits = defineEmits(['foo']),
a = 0,
b = 0;
</script>
`)
assertCode(content)
expect(content).toMatch(`props: ['item'],`)
expect(content).toMatch(`emits: ['foo'],`)
expect(content).toMatch(`const a = 0,`)
expect(content).toMatch(`b = 0;`)
})
test('defineProps/defineEmits in multi-variable declaration (full removal)', () => {
const { content } = compile(`
<script setup>
const props = defineProps(['item']),
emit = defineEmits(['a']);
</script>
`)
assertCode(content)
expect(content).toMatch(`props: ['item'],`)
expect(content).toMatch(`emits: ['a'],`)
})
describe('<script> and <script setup> co-usage', () => { describe('<script> and <script setup> co-usage', () => {
test('script first', () => { test('script first', () => {
const { content } = compile(` const { content } = compile(`
@ -156,6 +98,24 @@ describe('SFC compile <script setup>', () => {
assertCode(content) assertCode(content)
}) })
// #7805
test('keep original semi style', () => {
const { content } = compile(`
<script setup>
console.log('test')
const props = defineProps(['item']);
const emit = defineEmits(['change']);
(function () {})()
</script>
`)
assertCode(content)
expect(content).toMatch(`console.log('test')`)
expect(content).toMatch(`const props = __props;`)
expect(content).toMatch(`const emit = __emit;`)
expect(content).toMatch(`(function () {})()`)
})
test('script setup first, named default export', () => { test('script setup first, named default export', () => {
const { content } = compile(` const { content } = compile(`
<script setup> <script setup>
@ -413,6 +373,25 @@ describe('SFC compile <script setup>', () => {
assertCode(content) assertCode(content)
}) })
test('dynamic arguments', () => {
const { content } = compile(`
<script setup lang="ts">
import { FooBar, foo, bar, unused } from './x'
</script>
<template>
<FooBar #[foo.slotName] />
<FooBar #unused />
<div :[bar.attrName]="15"></div>
<div unused="unused"></div>
</template>
`)
expect(content).toMatch(
`return { get FooBar() { return FooBar }, get foo() { return foo }, ` +
`get bar() { return bar } }`
)
assertCode(content)
})
// https://github.com/vuejs/core/issues/4599 // https://github.com/vuejs/core/issues/4599
test('attribute expressions', () => { test('attribute expressions', () => {
const { content } = compile(` const { content } = compile(`
@ -513,6 +492,23 @@ describe('SFC compile <script setup>', () => {
</template> </template>
`) `)
}) })
test('template ref', () => {
const { content } = compile(`
<script setup lang="ts">
import { foo, bar, Baz } from './foo'
</script>
<template>
<div ref="foo"></div>
<div ref=""></div>
<Baz ref="bar" />
</template>
`)
expect(content).toMatch(
'return { get foo() { return foo }, get bar() { return bar }, get Baz() { return Baz } }'
)
assertCode(content)
})
}) })
describe('inlineTemplate mode', () => { describe('inlineTemplate mode', () => {

View File

@ -3,10 +3,10 @@
exports[`defineEmits > basic usage 1`] = ` exports[`defineEmits > basic usage 1`] = `
"export default { "export default {
emits: ['foo', 'bar'], emits: ['foo', 'bar'],
setup(__props, { expose: __expose, emit: myEmit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
const myEmit = __emit
return { myEmit } return { myEmit }
} }
@ -43,10 +43,10 @@ exports[`defineEmits > w/ runtime options 1`] = `
export default /*#__PURE__*/_defineComponent({ export default /*#__PURE__*/_defineComponent({
emits: ['a', 'b'], emits: ['a', 'b'],
setup(__props, { expose: __expose, emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
const emit = __emit
return { emit } return { emit }
} }
@ -60,10 +60,10 @@ export interface Emits { (e: 'foo' | 'bar'): void }
export default /*#__PURE__*/_defineComponent({ export default /*#__PURE__*/_defineComponent({
emits: [\\"foo\\", \\"bar\\"], emits: [\\"foo\\", \\"bar\\"],
setup(__props, { expose: __expose, emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
const emit = __emit
return { emit } return { emit }
} }
@ -77,10 +77,10 @@ export type Emits = { (e: 'foo' | 'bar'): void }
export default /*#__PURE__*/_defineComponent({ export default /*#__PURE__*/_defineComponent({
emits: [\\"foo\\", \\"bar\\"], emits: [\\"foo\\", \\"bar\\"],
setup(__props, { expose: __expose, emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
const emit = __emit
return { emit } return { emit }
} }
@ -94,10 +94,10 @@ interface Emits { (e: 'foo'): void }
export default /*#__PURE__*/_defineComponent({ export default /*#__PURE__*/_defineComponent({
emits: ['foo'], emits: ['foo'],
setup(__props, { expose: __expose, emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
const emit: Emits = __emit
return { emit } return { emit }
} }
@ -111,10 +111,10 @@ interface Emits { (e: 'foo' | 'bar'): void }
export default /*#__PURE__*/_defineComponent({ export default /*#__PURE__*/_defineComponent({
emits: [\\"foo\\", \\"bar\\"], emits: [\\"foo\\", \\"bar\\"],
setup(__props, { expose: __expose, emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
const emit = __emit
return { emit } return { emit }
} }
@ -127,10 +127,10 @@ exports[`defineEmits > w/ type (property syntax string literal) 1`] = `
export default /*#__PURE__*/_defineComponent({ export default /*#__PURE__*/_defineComponent({
emits: [\\"foo:bar\\"], emits: [\\"foo:bar\\"],
setup(__props, { expose: __expose, emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
const emit = __emit
return { emit } return { emit }
} }
@ -143,10 +143,10 @@ exports[`defineEmits > w/ type (property syntax) 1`] = `
export default /*#__PURE__*/_defineComponent({ export default /*#__PURE__*/_defineComponent({
emits: [\\"foo\\", \\"bar\\"], emits: [\\"foo\\", \\"bar\\"],
setup(__props, { expose: __expose, emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
const emit = __emit
return { emit } return { emit }
} }
@ -160,10 +160,10 @@ export type Emits = (e: 'foo' | 'bar') => void
export default /*#__PURE__*/_defineComponent({ export default /*#__PURE__*/_defineComponent({
emits: [\\"foo\\", \\"bar\\"], emits: [\\"foo\\", \\"bar\\"],
setup(__props, { expose: __expose, emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
const emit = __emit
return { emit } return { emit }
} }
@ -177,10 +177,10 @@ type Emits = (e: 'foo' | 'bar') => void
export default /*#__PURE__*/_defineComponent({ export default /*#__PURE__*/_defineComponent({
emits: [\\"foo\\", \\"bar\\"], emits: [\\"foo\\", \\"bar\\"],
setup(__props, { expose: __expose, emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
const emit = __emit
return { emit } return { emit }
} }
@ -194,10 +194,10 @@ type Emits = { (e: 'foo' | 'bar'): void }
export default /*#__PURE__*/_defineComponent({ export default /*#__PURE__*/_defineComponent({
emits: [\\"foo\\", \\"bar\\"], emits: [\\"foo\\", \\"bar\\"],
setup(__props, { expose: __expose, emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
const emit = __emit
return { emit } return { emit }
} }
@ -210,10 +210,10 @@ exports[`defineEmits > w/ type (type literal w/ call signatures) 1`] = `
export default /*#__PURE__*/_defineComponent({ export default /*#__PURE__*/_defineComponent({
emits: [\\"foo\\", \\"bar\\", \\"baz\\"], emits: [\\"foo\\", \\"bar\\", \\"baz\\"],
setup(__props, { expose: __expose, emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
const emit = __emit
return { emit } return { emit }
} }
@ -228,10 +228,10 @@ type BaseEmit = \\"change\\"
export default /*#__PURE__*/_defineComponent({ export default /*#__PURE__*/_defineComponent({
emits: [\\"some\\", \\"emit\\", \\"change\\", \\"another\\"], emits: [\\"some\\", \\"emit\\", \\"change\\", \\"another\\"],
setup(__props, { expose: __expose, emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
const emit = __emit;
return { emit } return { emit }
} }
@ -244,10 +244,10 @@ exports[`defineEmits > w/ type (union) 1`] = `
export default /*#__PURE__*/_defineComponent({ export default /*#__PURE__*/_defineComponent({
emits: [\\"foo\\", \\"bar\\", \\"baz\\"], emits: [\\"foo\\", \\"bar\\", \\"baz\\"],
setup(__props, { expose: __expose, emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
const emit = __emit
return { emit } return { emit }
} }
@ -260,10 +260,10 @@ exports[`defineEmits > w/ type 1`] = `
export default /*#__PURE__*/_defineComponent({ export default /*#__PURE__*/_defineComponent({
emits: [\\"foo\\", \\"bar\\"], emits: [\\"foo\\", \\"bar\\"],
setup(__props, { expose: __expose, emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
const emit = __emit
return { emit } return { emit }
} }
@ -278,10 +278,10 @@ exports[`defineEmits > w/ type from normal script 1`] = `
export default /*#__PURE__*/_defineComponent({ export default /*#__PURE__*/_defineComponent({
emits: [\\"foo\\", \\"bar\\"], emits: [\\"foo\\", \\"bar\\"],
setup(__props, { expose: __expose, emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
const emit = __emit
return { emit } return { emit }
} }

View File

@ -10,9 +10,7 @@ export default {
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
const props = __props; const props = __props
return { props, bar } return { props, bar }
} }
@ -28,9 +26,7 @@ export default /*#__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
const props = __props; const props = __props
return { props } return { props }
} }
@ -48,9 +44,7 @@ export default /*#__PURE__*/_defineComponent({
setup(__props: any, { expose: __expose }) { setup(__props: any, { expose: __expose }) {
__expose(); __expose();
const { foo } = __props; const { foo } = __props
return { } return { }
} }
@ -167,9 +161,7 @@ export default {
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
const props = __props; const props = __props
return { props, get propsModel() { return propsModel } } return { props, get propsModel() { return propsModel } }
} }
@ -203,9 +195,7 @@ export default {
props: {}, props: {},
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
const props = __props
const props = __props;
return { props, get x() { return x } } return { props, get x() { return x } }
} }
@ -304,9 +294,7 @@ export default /*#__PURE__*/_defineComponent({
setup(__props: any, { expose: __expose }) { setup(__props: any, { expose: __expose }) {
__expose(); __expose();
const props = __props; const props = __props
return { props, get defaults() { return defaults } } return { props, get defaults() { return defaults } }
} }
@ -328,9 +316,7 @@ export default /*#__PURE__*/_defineComponent({
setup(__props: any, { expose: __expose }) { setup(__props: any, { expose: __expose }) {
__expose(); __expose();
const props = __props; const props = __props
return { props, get defaults() { return defaults } } return { props, get defaults() { return defaults } }
} }
@ -351,9 +337,7 @@ export default /*#__PURE__*/_defineComponent({
setup(__props: any, { expose: __expose }) { setup(__props: any, { expose: __expose }) {
__expose(); __expose();
const props = __props; const props = __props
return { props, get defaults() { return defaults } } return { props, get defaults() { return defaults } }
} }
@ -375,9 +359,7 @@ export default /*#__PURE__*/_defineComponent({
setup(__props: any, { expose: __expose }) { setup(__props: any, { expose: __expose }) {
__expose(); __expose();
const props = __props; const props = __props;
return { props } return { props }
} }
@ -401,9 +383,7 @@ export default /*#__PURE__*/_defineComponent({
setup(__props: any, { expose: __expose }) { setup(__props: any, { expose: __expose }) {
__expose(); __expose();
const props = __props; const props = __props
return { props } return { props }
} }
@ -424,9 +404,7 @@ export default /*#__PURE__*/_defineComponent({
setup(__props: any, { expose: __expose }) { setup(__props: any, { expose: __expose }) {
__expose(); __expose();
const props = __props; const props = __props
return { props } return { props }
} }
@ -446,9 +424,7 @@ export default /*#__PURE__*/_defineComponent({
setup(__props: any, { expose: __expose }) { setup(__props: any, { expose: __expose }) {
__expose(); __expose();
const props = __props; const props = __props
return { props } return { props }
} }

View File

@ -176,6 +176,61 @@ return () => {}
})" })"
`; `;
exports[`sfc reactive props destructure > defineProps/defineEmits in multi-variable declaration (full removal) 1`] = `
"export default {
props: ['item'],
emits: ['a'],
setup(__props, { emit: __emit }) {
const props = __props,
emit = __emit;
return () => {}
}
}"
`;
exports[`sfc reactive props destructure > multi-variable declaration 1`] = `
"export default {
props: ['item'],
setup(__props) {
const a = 1;
return () => {}
}
}"
`;
exports[`sfc reactive props destructure > multi-variable declaration fix #6757 1`] = `
"export default {
props: ['item'],
setup(__props) {
const a = 1;
return () => {}
}
}"
`;
exports[`sfc reactive props destructure > multi-variable declaration fix #7422 1`] = `
"export default {
props: ['item'],
setup(__props) {
const a = 0,
b = 0;
return () => {}
}
}"
`;
exports[`sfc reactive props destructure > multiple variable declarations 1`] = ` exports[`sfc reactive props destructure > multiple variable declarations 1`] = `
"import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from \\"vue\\" "import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from \\"vue\\"
@ -237,9 +292,7 @@ export default {
props: ['foo', 'bar', 'baz'], props: ['foo', 'bar', 'baz'],
setup(__props) { setup(__props) {
const rest = _createPropsRestProxy(__props, [\\"foo\\",\\"bar\\"]); const rest = _createPropsRestProxy(__props, [\\"foo\\",\\"bar\\"])
return () => {} return () => {}
} }

View File

@ -16,8 +16,9 @@ const myEmit = defineEmits(['foo', 'bar'])
expect(content).not.toMatch('defineEmits') expect(content).not.toMatch('defineEmits')
// should generate correct setup signature // should generate correct setup signature
expect(content).toMatch( expect(content).toMatch(
`setup(__props, { expose: __expose, emit: myEmit }) {` `setup(__props, { expose: __expose, emit: __emit }) {`
) )
expect(content).toMatch('const myEmit = __emit')
// should include context options in default export // should include context options in default export
expect(content).toMatch(`export default { expect(content).toMatch(`export default {
emits: ['foo', 'bar'],`) emits: ['foo', 'bar'],`)
@ -32,7 +33,8 @@ const emit = defineEmits(['a', 'b'])
assertCode(content) assertCode(content)
expect(content).toMatch(`export default /*#__PURE__*/_defineComponent({ expect(content).toMatch(`export default /*#__PURE__*/_defineComponent({
emits: ['a', 'b'], emits: ['a', 'b'],
setup(__props, { expose: __expose, emit }) {`) setup(__props, { expose: __expose, emit: __emit }) {`)
expect(content).toMatch('const emit = __emit')
}) })
test('w/ type', () => { test('w/ type', () => {

View File

@ -282,6 +282,58 @@ describe('sfc reactive props destructure', () => {
}) })
}) })
test('multi-variable declaration', () => {
const { content } = compile(`
<script setup>
const { item } = defineProps(['item']),
a = 1;
</script>
`)
assertCode(content)
expect(content).toMatch(`const a = 1;`)
expect(content).toMatch(`props: ['item'],`)
})
// #6757
test('multi-variable declaration fix #6757 ', () => {
const { content } = compile(`
<script setup>
const a = 1,
{ item } = defineProps(['item']);
</script>
`)
assertCode(content)
expect(content).toMatch(`const a = 1;`)
expect(content).toMatch(`props: ['item'],`)
})
// #7422
test('multi-variable declaration fix #7422', () => {
const { content } = compile(`
<script setup>
const { item } = defineProps(['item']),
a = 0,
b = 0;
</script>
`)
assertCode(content)
expect(content).toMatch(`const a = 0,`)
expect(content).toMatch(`b = 0;`)
expect(content).toMatch(`props: ['item'],`)
})
test('defineProps/defineEmits in multi-variable declaration (full removal)', () => {
const { content } = compile(`
<script setup>
const props = defineProps(['item']),
emit = defineEmits(['a']);
</script>
`)
assertCode(content)
expect(content).toMatch(`props: ['item'],`)
expect(content).toMatch(`emits: ['a'],`)
})
describe('errors', () => { describe('errors', () => {
test('should error on deep destructure', () => { test('should error on deep destructure', () => {
expect(() => expect(() =>

View File

@ -10,7 +10,7 @@ import {
} from '../../src/script/resolveType' } from '../../src/script/resolveType'
import ts from 'typescript' import ts from 'typescript'
registerTS(ts) registerTS(() => ts)
describe('resolveType', () => { describe('resolveType', () => {
test('type literal', () => { test('type literal', () => {

View File

@ -32,29 +32,28 @@
}, },
"homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-sfc#readme", "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-sfc#readme",
"dependencies": { "dependencies": {
"@babel/parser": "^7.20.15", "@babel/parser": "^7.23.0",
"@vue/compiler-core": "3.3.4", "@vue/compiler-core": "3.3.4",
"@vue/compiler-dom": "3.3.4", "@vue/compiler-dom": "3.3.4",
"@vue/compiler-ssr": "3.3.4", "@vue/compiler-ssr": "3.3.4",
"@vue/reactivity-transform": "3.3.4", "@vue/reactivity-transform": "3.3.4",
"@vue/shared": "3.3.4", "@vue/shared": "3.3.4",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"magic-string": "^0.30.0", "magic-string": "^0.30.5",
"postcss": "^8.1.10", "postcss": "^8.4.31",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/types": "^7.21.3", "@babel/types": "^7.23.0",
"@types/estree": "^0.0.48", "@types/estree": "^0.0.52",
"@types/lru-cache": "^5.1.0",
"@vue/consolidate": "^0.17.3", "@vue/consolidate": "^0.17.3",
"hash-sum": "^2.0.0", "hash-sum": "^2.0.0",
"lru-cache": "^5.1.1", "lru-cache": "^10.0.1",
"merge-source-map": "^1.1.0", "merge-source-map": "^1.1.0",
"minimatch": "^9.0.0", "minimatch": "^9.0.3",
"postcss-modules": "^4.0.0", "postcss-modules": "^4.3.1",
"postcss-selector-parser": "^6.0.4", "postcss-selector-parser": "^6.0.13",
"pug": "^3.0.1", "pug": "^3.0.2",
"sass": "^1.26.9" "sass": "^1.69.4"
} }
} }

View File

@ -1,11 +1,10 @@
import LRU from 'lru-cache' import { LRUCache } from 'lru-cache'
export function createCache<T>(size = 500): Map<string, T> & { max?: number } { export function createCache<T extends {}>(
max = 500
): Map<string, T> | LRUCache<string, T> {
if (__GLOBAL__ || __ESM_BROWSER__) { if (__GLOBAL__ || __ESM_BROWSER__) {
return new Map<string, T>() return new Map<string, T>()
} }
const cache = new LRU(size) return new LRUCache({ max })
// @ts-expect-error
cache.delete = cache.del.bind(cache)
return cache as any as Map<string, T>
} }

View File

@ -274,7 +274,7 @@ export function compileScript(
const scriptAst = ctx.scriptAst const scriptAst = ctx.scriptAst
const scriptSetupAst = ctx.scriptSetupAst! const scriptSetupAst = ctx.scriptSetupAst!
// 1.1 walk import delcarations of <script> // 1.1 walk import declarations of <script>
if (scriptAst) { if (scriptAst) {
for (const node of scriptAst.body) { for (const node of scriptAst.body) {
if (node.type === 'ImportDeclaration') { if (node.type === 'ImportDeclaration') {
@ -552,7 +552,11 @@ export function compileScript(
(processDefineSlots(ctx, init, decl.id) || (processDefineSlots(ctx, init, decl.id) ||
processDefineModel(ctx, init, decl.id)) processDefineModel(ctx, init, decl.id))
if (isDefineProps || isDefineEmits) { if (
isDefineProps &&
!ctx.propsDestructureRestId &&
ctx.propsDestructureDecl
) {
if (left === 1) { if (left === 1) {
ctx.s.remove(node.start! + startOffset, node.end! + startOffset) ctx.s.remove(node.start! + startOffset, node.end! + startOffset)
} else { } else {
@ -570,6 +574,12 @@ export function compileScript(
ctx.s.remove(start, end) ctx.s.remove(start, end)
left-- left--
} }
} else if (isDefineEmits) {
ctx.s.overwrite(
startOffset + init.start!,
startOffset + init.end!,
'__emit'
)
} else { } else {
lastNonRemoved = i lastNonRemoved = i
} }
@ -607,8 +617,8 @@ export function compileScript(
node.type.endsWith('Statement') node.type.endsWith('Statement')
) { ) {
const scope: Statement[][] = [scriptSetupAst.body] const scope: Statement[][] = [scriptSetupAst.body]
;(walk as any)(node, { walk(node, {
enter(child: Node, parent: Node) { enter(child: Node, parent: Node | undefined) {
if (isFunctionType(child)) { if (isFunctionType(child)) {
this.skip() this.skip()
} }
@ -633,7 +643,7 @@ export function compileScript(
ctx, ctx,
child, child,
needsSemi, needsSemi,
parent.type === 'ExpressionStatement' parent!.type === 'ExpressionStatement'
) )
} }
}, },
@ -781,22 +791,29 @@ export function compileScript(
// inject user assignment of props // inject user assignment of props
// we use a default __props so that template expressions referencing props // we use a default __props so that template expressions referencing props
// can use it directly // can use it directly
if (ctx.propsIdentifier) { if (ctx.propsDecl) {
ctx.s.prependLeft( if (ctx.propsDestructureRestId) {
startOffset, ctx.s.overwrite(
`\nconst ${ctx.propsIdentifier} = __props;\n` startOffset + ctx.propsCall!.start!,
) startOffset + ctx.propsCall!.end!,
} `${ctx.helper(`createPropsRestProxy`)}(__props, ${JSON.stringify(
if (ctx.propsDestructureRestId) { Object.keys(ctx.propsDestructuredBindings)
ctx.s.prependLeft( )})`
startOffset, )
`\nconst ${ctx.propsDestructureRestId} = ${ctx.helper( ctx.s.overwrite(
`createPropsRestProxy` startOffset + ctx.propsDestructureDecl!.start!,
)}(__props, ${JSON.stringify( startOffset + ctx.propsDestructureDecl!.end!,
Object.keys(ctx.propsDestructuredBindings) ctx.propsDestructureRestId
)});\n` )
) } else if (!ctx.propsDestructureDecl) {
ctx.s.overwrite(
startOffset + ctx.propsCall!.start!,
startOffset + ctx.propsCall!.end!,
'__props'
)
}
} }
// inject temp variables for async context preservation // inject temp variables for async context preservation
if (hasAwait) { if (hasAwait) {
const any = ctx.isTS ? `: any` : `` const any = ctx.isTS ? `: any` : ``
@ -807,10 +824,8 @@ export function compileScript(
ctx.hasDefineExposeCall || !options.inlineTemplate ctx.hasDefineExposeCall || !options.inlineTemplate
? [`expose: __expose`] ? [`expose: __expose`]
: [] : []
if (ctx.emitIdentifier) { if (ctx.emitDecl) {
destructureElements.push( destructureElements.push(`emit: __emit`)
ctx.emitIdentifier === `emit` ? `emit` : `emit: ${ctx.emitIdentifier}`
)
} }
if (destructureElements.length) { if (destructureElements.length) {
args += `, { ${destructureElements.join(', ')} }` args += `, { ${destructureElements.join(', ')} }`

View File

@ -37,7 +37,7 @@ export function rewriteDefaultAST(
// multi-line comments or template strings. fallback to a full parse. // multi-line comments or template strings. fallback to a full parse.
ast.forEach(node => { ast.forEach(node => {
if (node.type === 'ExportDefaultDeclaration') { if (node.type === 'ExportDefaultDeclaration') {
if (node.declaration.type === 'ClassDeclaration') { if (node.declaration.type === 'ClassDeclaration' && node.declaration.id) {
let start: number = let start: number =
node.declaration.decorators && node.declaration.decorators.length > 0 node.declaration.decorators && node.declaration.decorators.length > 0
? node.declaration.decorators[ ? node.declaration.decorators[

View File

@ -1,4 +1,4 @@
import { Node, ObjectPattern, Program } from '@babel/types' import { CallExpression, Node, ObjectPattern, Program } from '@babel/types'
import { SFCDescriptor } from '../parse' import { SFCDescriptor } from '../parse'
import { generateCodeFrame } from '@vue/shared' import { generateCodeFrame } from '@vue/shared'
import { parse as babelParse, ParserPlugin } from '@babel/parser' import { parse as babelParse, ParserPlugin } from '@babel/parser'
@ -38,7 +38,8 @@ export class ScriptCompileContext {
hasDefineModelCall = false hasDefineModelCall = false
// defineProps // defineProps
propsIdentifier: string | undefined propsCall: CallExpression | undefined
propsDecl: Node | undefined
propsRuntimeDecl: Node | undefined propsRuntimeDecl: Node | undefined
propsTypeDecl: Node | undefined propsTypeDecl: Node | undefined
propsDestructureDecl: ObjectPattern | undefined propsDestructureDecl: ObjectPattern | undefined
@ -49,7 +50,7 @@ export class ScriptCompileContext {
// defineEmits // defineEmits
emitsRuntimeDecl: Node | undefined emitsRuntimeDecl: Node | undefined
emitsTypeDecl: Node | undefined emitsTypeDecl: Node | undefined
emitIdentifier: string | undefined emitDecl: Node | undefined
// defineModel // defineModel
modelDecls: Record<string, ModelDecl> = {} modelDecls: Record<string, ModelDecl> = {}

View File

@ -1,4 +1,11 @@
import { Identifier, LVal, Node, RestElement } from '@babel/types' import {
ArrayPattern,
Identifier,
LVal,
Node,
ObjectPattern,
RestElement
} from '@babel/types'
import { isCallOf } from './utils' import { isCallOf } from './utils'
import { ScriptCompileContext } from './context' import { ScriptCompileContext } from './context'
import { import {
@ -34,10 +41,7 @@ export function processDefineEmits(
ctx.emitsTypeDecl = node.typeParameters.params[0] ctx.emitsTypeDecl = node.typeParameters.params[0]
} }
if (declId) { ctx.emitDecl = declId
ctx.emitIdentifier =
declId.type === 'Identifier' ? declId.name : ctx.getString(declId)
}
return true return true
} }
@ -99,7 +103,7 @@ function extractRuntimeEmits(ctx: ScriptCompileContext): Set<string> {
function extractEventNames( function extractEventNames(
ctx: ScriptCompileContext, ctx: ScriptCompileContext,
eventName: Identifier | RestElement, eventName: ArrayPattern | Identifier | ObjectPattern | RestElement,
emits: Set<string> emits: Set<string>
) { ) {
if ( if (

View File

@ -24,13 +24,21 @@ export function processDefineModel(
node: Node, node: Node,
declId?: LVal declId?: LVal
): boolean { ): boolean {
if (!ctx.options.defineModel || !isCallOf(node, DEFINE_MODEL)) { if (!isCallOf(node, DEFINE_MODEL)) {
return false
}
if (!ctx.options.defineModel) {
warnOnce(
`defineModel() is an experimental feature and disabled by default.\n` +
`To enable it, follow the RFC at https://github.com/vuejs/rfcs/discussions/503.`
)
return false return false
} }
warnOnce( warnOnce(
`This project is using defineModel(), which is an experimental ` + `This project is using defineModel(), which is an experimental ` +
` feature. It may receive breaking changes or be removed in the future, so ` + `feature. It may receive breaking changes or be removed in the future, so ` +
`use at your own risk.\n` + `use at your own risk.\n` +
`To stay updated, follow the RFC at https://github.com/vuejs/rfcs/discussions/503.` `To stay updated, follow the RFC at https://github.com/vuejs/rfcs/discussions/503.`
) )

View File

@ -77,15 +77,14 @@ export function processDefineProps(
ctx.propsTypeDecl = node.typeParameters.params[0] ctx.propsTypeDecl = node.typeParameters.params[0]
} }
if (declId) { // handle props destructure
// handle props destructure if (declId && declId.type === 'ObjectPattern') {
if (declId.type === 'ObjectPattern') { processPropsDestructure(ctx, declId)
processPropsDestructure(ctx, declId)
} else {
ctx.propsIdentifier = ctx.getString(declId)
}
} }
ctx.propsCall = node
ctx.propsDecl = declId
return true return true
} }
@ -97,31 +96,33 @@ function processWithDefaults(
if (!isCallOf(node, WITH_DEFAULTS)) { if (!isCallOf(node, WITH_DEFAULTS)) {
return false return false
} }
if (processDefineProps(ctx, node.arguments[0], declId)) { if (!processDefineProps(ctx, node.arguments[0], declId)) {
if (ctx.propsRuntimeDecl) {
ctx.error(
`${WITH_DEFAULTS} can only be used with type-based ` +
`${DEFINE_PROPS} declaration.`,
node
)
}
if (ctx.propsDestructureDecl) {
ctx.error(
`${WITH_DEFAULTS}() is unnecessary when using destructure with ${DEFINE_PROPS}().\n` +
`Prefer using destructure default values, e.g. const { foo = 1 } = defineProps(...).`,
node.callee
)
}
ctx.propsRuntimeDefaults = node.arguments[1]
if (!ctx.propsRuntimeDefaults) {
ctx.error(`The 2nd argument of ${WITH_DEFAULTS} is required.`, node)
}
} else {
ctx.error( ctx.error(
`${WITH_DEFAULTS}' first argument must be a ${DEFINE_PROPS} call.`, `${WITH_DEFAULTS}' first argument must be a ${DEFINE_PROPS} call.`,
node.arguments[0] || node node.arguments[0] || node
) )
} }
if (ctx.propsRuntimeDecl) {
ctx.error(
`${WITH_DEFAULTS} can only be used with type-based ` +
`${DEFINE_PROPS} declaration.`,
node
)
}
if (ctx.propsDestructureDecl) {
ctx.error(
`${WITH_DEFAULTS}() is unnecessary when using destructure with ${DEFINE_PROPS}().\n` +
`Prefer using destructure default values, e.g. const { foo = 1 } = defineProps(...).`,
node.callee
)
}
ctx.propsRuntimeDefaults = node.arguments[1]
if (!ctx.propsRuntimeDefaults) {
ctx.error(`The 2nd argument of ${WITH_DEFAULTS} is required.`, node)
}
ctx.propsCall = node
return true return true
} }

View File

@ -28,13 +28,12 @@ export function processPropsDestructure(
declId: ObjectPattern declId: ObjectPattern
) { ) {
if (!ctx.options.propsDestructure && !ctx.options.reactivityTransform) { if (!ctx.options.propsDestructure && !ctx.options.reactivityTransform) {
ctx.propsIdentifier = ctx.getString(declId)
return return
} }
warnOnce( warnOnce(
`This project is using reactive props destructure, which is an experimental ` + `This project is using reactive props destructure, which is an experimental ` +
` feature. It may receive breaking changes or be removed in the future, so ` + `feature. It may receive breaking changes or be removed in the future, so ` +
`use at your own risk.\n` + `use at your own risk.\n` +
`To stay updated, follow the RFC at https://github.com/vuejs/rfcs/discussions/502.` `To stay updated, follow the RFC at https://github.com/vuejs/rfcs/discussions/502.`
) )
@ -238,7 +237,7 @@ export function transformDestructuredProps(
// check root scope first // check root scope first
const ast = ctx.scriptSetupAst! const ast = ctx.scriptSetupAst!
walkScope(ast, true) walkScope(ast, true)
;(walk as any)(ast, { walk(ast, {
enter(node: Node, parent?: Node) { enter(node: Node, parent?: Node) {
parent && parentStack.push(parent) parent && parentStack.push(parent)

View File

@ -50,6 +50,12 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
if (!isBuiltInDirective(prop.name)) { if (!isBuiltInDirective(prop.name)) {
code += `,v${capitalize(camelize(prop.name))}` code += `,v${capitalize(camelize(prop.name))}`
} }
if (prop.arg && !(prop.arg as SimpleExpressionNode).isStatic) {
code += `,${processExp(
(prop.arg as SimpleExpressionNode).content,
prop.name
)}`
}
if (prop.exp) { if (prop.exp) {
code += `,${processExp( code += `,${processExp(
(prop.exp as SimpleExpressionNode).content, (prop.exp as SimpleExpressionNode).content,
@ -57,6 +63,13 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
)}` )}`
} }
} }
if (
prop.type === NodeTypes.ATTRIBUTE &&
prop.name === 'ref' &&
prop.value?.content
) {
code += `,${prop.value.content}`
}
} }
} else if (node.type === NodeTypes.INTERPOLATION) { } else if (node.type === NodeTypes.INTERPOLATION) {
code += `,${processExp( code += `,${processExp(

View File

@ -725,13 +725,14 @@ function resolveGlobalScope(ctx: TypeResolveContext): TypeScope[] | undefined {
} }
} }
let ts: typeof TS let ts: typeof TS | undefined
let loadTS: (() => typeof TS) | undefined
/** /**
* @private * @private
*/ */
export function registerTS(_ts: any) { export function registerTS(_loadTS: () => typeof TS) {
ts = _ts loadTS = _loadTS
} }
type FS = NonNullable<SFCScriptCompileOptions['fs']> type FS = NonNullable<SFCScriptCompileOptions['fs']>
@ -740,7 +741,10 @@ function resolveFS(ctx: TypeResolveContext): FS | undefined {
if (ctx.fs) { if (ctx.fs) {
return ctx.fs return ctx.fs
} }
const fs = ctx.options.fs || ts.sys if (!ts && loadTS) {
ts = loadTS()
}
const fs = ctx.options.fs || ts?.sys
if (!fs) { if (!fs) {
return return
} }
@ -796,22 +800,25 @@ function importSourceToScope(
} else { } else {
// module or aliased import - use full TS resolution, only supported in Node // module or aliased import - use full TS resolution, only supported in Node
if (!__NODE_JS__) { if (!__NODE_JS__) {
ctx.error( return ctx.error(
`Type import from non-relative sources is not supported in the browser build.`, `Type import from non-relative sources is not supported in the browser build.`,
node, node,
scope scope
) )
} }
if (!ts) { if (!ts) {
ctx.error( if (loadTS) ts = loadTS()
`Failed to resolve import source ${JSON.stringify(source)}. ` + if (!ts) {
`typescript is required as a peer dep for vue in order ` + return ctx.error(
`to support resolving types from module imports.`, `Failed to resolve import source ${JSON.stringify(source)}. ` +
node, `typescript is required as a peer dep for vue in order ` +
scope `to support resolving types from module imports.`,
) node,
scope
)
}
} }
resolved = resolveWithTS(scope.filename, source, fs) resolved = resolveWithTS(scope.filename, source, ts, fs)
} }
if (resolved) { if (resolved) {
resolved = scope.resolvedImportSources[source] = normalizePath(resolved) resolved = scope.resolvedImportSources[source] = normalizePath(resolved)
@ -856,6 +863,7 @@ const tsConfigRefMap = new Map<string, string>()
function resolveWithTS( function resolveWithTS(
containingFile: string, containingFile: string,
source: string, source: string,
ts: typeof TS,
fs: FS fs: FS
): string | undefined { ): string | undefined {
if (!__NODE_JS__) return if (!__NODE_JS__) return
@ -870,7 +878,7 @@ function resolveWithTS(
const normalizedConfigPath = normalizePath(configPath) const normalizedConfigPath = normalizePath(configPath)
const cached = tsConfigCache.get(normalizedConfigPath) const cached = tsConfigCache.get(normalizedConfigPath)
if (!cached) { if (!cached) {
configs = loadTSConfig(configPath, fs).map(config => ({ config })) configs = loadTSConfig(configPath, ts, fs).map(config => ({ config }))
tsConfigCache.set(normalizedConfigPath, configs) tsConfigCache.set(normalizedConfigPath, configs)
} else { } else {
configs = cached configs = cached
@ -935,7 +943,11 @@ function resolveWithTS(
} }
} }
function loadTSConfig(configPath: string, fs: FS): TS.ParsedCommandLine[] { function loadTSConfig(
configPath: string,
ts: typeof TS,
fs: FS
): TS.ParsedCommandLine[] {
// The only case where `fs` is NOT `ts.sys` is during tests. // The only case where `fs` is NOT `ts.sys` is during tests.
// parse config host requires an extra `readDirectory` method // parse config host requires an extra `readDirectory` method
// during tests, which is stubbed. // during tests, which is stubbed.
@ -957,7 +969,7 @@ function loadTSConfig(configPath: string, fs: FS): TS.ParsedCommandLine[] {
if (config.projectReferences) { if (config.projectReferences) {
for (const ref of config.projectReferences) { for (const ref of config.projectReferences) {
tsConfigRefMap.set(ref.path, configPath) tsConfigRefMap.set(ref.path, configPath)
res.unshift(...loadTSConfig(ref.path, fs)) res.unshift(...loadTSConfig(ref.path, ts, fs))
} }
} }
return res return res
@ -1232,7 +1244,7 @@ function recordType(
break break
} }
case 'ClassDeclaration': case 'ClassDeclaration':
types[overwriteId || getId(node.id)] = node if (overwriteId || node.id) types[overwriteId || getId(node.id!)] = node
break break
case 'TSTypeAliasDeclaration': case 'TSTypeAliasDeclaration':
types[node.id.name] = node.typeAnnotation types[node.id.name] = node.typeAnnotation

View File

@ -53,8 +53,9 @@ export function parseCssVars(sfc: SFCDescriptor): string[] {
const vars: string[] = [] const vars: string[] = []
sfc.styles.forEach(style => { sfc.styles.forEach(style => {
let match let match
// ignore v-bind() in comments /* ... */ // ignore v-bind() in comments, eg /* ... */
const content = style.content.replace(/\/\*([\s\S]*?)\*\//g, '') // and // (Less, Sass and Stylus all support the use of // to comment)
const content = style.content.replace(/\/\*([\s\S]*?)\*\/|\/\/.*/g, '')
while ((match = vBindRE.exec(content))) { while ((match = vBindRE.exec(content))) {
const start = match.index + match[0].length const start = match.index + match[0].length
const end = lexBinding(content, start) const end = lexBinding(content, start)

View File

@ -130,9 +130,10 @@ function rewriteSelector(
// DEPRECATED usage // DEPRECATED usage
// .foo ::v-deep .bar -> .foo[xxxxxxx] .bar // .foo ::v-deep .bar -> .foo[xxxxxxx] .bar
warn( warn(
`::v-deep usage as a combinator has ` + `${value} usage as a combinator has been deprecated. ` +
`been deprecated. Use :deep(<inner-selector>) instead.` `Use :deep(<inner-selector>) instead of ${value} <inner-selector>.`
) )
const prev = selector.at(selector.index(n) - 1) const prev = selector.at(selector.index(n) - 1)
if (prev && isSpaceCombinator(prev)) { if (prev && isSpaceCombinator(prev)) {
selector.removeChild(prev) selector.removeChild(prev)

View File

@ -98,8 +98,7 @@ const less: StylePreprocessor = (source, map, options, load = require) => {
const styl: StylePreprocessor = (source, map, options, load = require) => { const styl: StylePreprocessor = (source, map, options, load = require) => {
const nodeStylus = load('stylus') const nodeStylus = load('stylus')
try { try {
const ref = nodeStylus(source) const ref = nodeStylus(source, options)
Object.keys(options).forEach(key => ref.set(key, options[key]))
if (map) ref.set('sourcemap', { inline: false, comment: false }) if (map) ref.set('sourcemap', { inline: false, comment: false })
const result = ref.render() const result = ref.render()

View File

@ -33,6 +33,44 @@ describe('ssr: v-model', () => {
`) `)
}) })
test('<select v-model>', () => {
expect(
compileWithWrapper(
`<select v-model="model"><option value="1"></option></select>`
).code
).toMatchInlineSnapshot(`
"const { ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${
_ssrRenderAttrs(_attrs)
}><select><option value=\\"1\\"\${
(_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
? _ssrLooseContain(_ctx.model, \\"1\\")
: _ssrLooseEqual(_ctx.model, \\"1\\"))) ? \\" selected\\" : \\"\\"
}></option></select></div>\`)
}"
`)
expect(
compileWithWrapper(
`<select multiple v-model="model"><option value="1" selected></option><option value="2"></option></select>`
).code
).toMatchInlineSnapshot(`
"const { ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${
_ssrRenderAttrs(_attrs)
}><select multiple><option value=\\"1\\" selected></option><option value=\\"2\\"\${
(_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
? _ssrLooseContain(_ctx.model, \\"2\\")
: _ssrLooseEqual(_ctx.model, \\"2\\"))) ? \\" selected\\" : \\"\\"
}></option></select></div>\`)
}"
`)
})
test('<input type="radio">', () => { test('<input type="radio">', () => {
expect( expect(
compileWithWrapper(`<input type="radio" value="foo" v-model="bar">`).code compileWithWrapper(`<input type="radio" value="foo" v-model="bar">`).code

View File

@ -18,7 +18,8 @@ import {
import { import {
SSR_LOOSE_EQUAL, SSR_LOOSE_EQUAL,
SSR_LOOSE_CONTAIN, SSR_LOOSE_CONTAIN,
SSR_RENDER_DYNAMIC_MODEL SSR_RENDER_DYNAMIC_MODEL,
SSR_INCLUDE_BOOLEAN_ATTR
} from '../runtimeHelpers' } from '../runtimeHelpers'
import { DirectiveTransformResult } from 'packages/compiler-core/src/transform' import { DirectiveTransformResult } from 'packages/compiler-core/src/transform'
@ -129,8 +130,34 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
checkDuplicatedValue() checkDuplicatedValue()
node.children = [createInterpolation(model, model.loc)] node.children = [createInterpolation(model, model.loc)]
} else if (node.tag === 'select') { } else if (node.tag === 'select') {
// NOOP node.children.forEach(option => {
// select relies on client-side directive to set initial selected state. 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 */
)
)
}
}
})
} else { } else {
context.onError( context.onError(
createDOMCompilerError( createDOMCompilerError(

View File

@ -4,4 +4,4 @@ Tests Typescript types to ensure the types remain as expected.
- This directory is included in the root `tsconfig.json`, where package imports are aliased to `src` directories, so in IDEs and the `pnpm check` script the types are validated against source code. - This directory is included in the root `tsconfig.json`, where package imports are aliased to `src` directories, so in IDEs and the `pnpm check` script the types are validated against source code.
- When running `tsc` with `packages/dts-test/tsconfig.test.json`, packages are resolved using using normal `node` resolution, so the types are validated against actual **built** types. This requires the types to be built first via `pnpm build-types`. - When running `tsc` with `packages/dts-test/tsconfig.test.json`, packages are resolved using normal `node` resolution, so the types are validated against actual **built** types. This requires the types to be built first via `pnpm build-types`.

View File

@ -1363,13 +1363,13 @@ describe('function syntax w/ runtime props', () => {
} }
) )
// @ts-expect-error prop type mismatch
defineComponent( defineComponent(
(_props: { msg: string }) => { (_props: { msg: string }) => {
return () => {} return () => {}
}, },
{ {
props: { props: {
// @ts-expect-error prop type mismatch
msg: Number msg: Number
} }
} }

View File

@ -100,7 +100,8 @@ describe('defineProps w/ union type declaration + withDefaults', () => {
) )
}) })
describe('defineProps w/ generic type declaration + withDefaults', <T extends number, TA extends { describe('defineProps w/ generic type declaration + withDefaults', <T extends
number, TA extends {
a: string a: string
}, TString extends string>() => { }, TString extends string>() => {
const res = withDefaults( const res = withDefaults(
@ -117,10 +118,10 @@ describe('defineProps w/ generic type declaration + withDefaults', <T extends nu
n: 123, n: 123,
generic1: () => [123, 33] as T[], generic1: () => [123, 33] as T[],
generic2: () => ({ x: 123 } as { x: T }), generic2: () => ({ x: 123 }) as { x: T },
generic3: () => 'test' as TString, generic3: () => 'test' as TString,
generic4: () => ({ a: 'test' } as TA) generic4: () => ({ a: 'test' }) as TA
} }
) )
@ -134,6 +135,26 @@ describe('defineProps w/ generic type declaration + withDefaults', <T extends nu
expectType<boolean>(res.bool) expectType<boolean>(res.bool)
}) })
describe('withDefaults w/ boolean type', () => {
const res1 = withDefaults(
defineProps<{
bool?: boolean
}>(),
{ bool: false }
)
expectType<boolean>(res1.bool)
const res2 = withDefaults(
defineProps<{
bool?: boolean
}>(),
{
bool: undefined
}
)
expectType<boolean | undefined>(res2.bool)
})
describe('defineProps w/ runtime declaration', () => { describe('defineProps w/ runtime declaration', () => {
// runtime declaration // runtime declaration
const props = defineProps({ const props = defineProps({

11
packages/global.d.ts vendored
View File

@ -33,6 +33,17 @@ declare module 'file-saver' {
export function saveAs(blob: any, name: any): void export function saveAs(blob: any, name: any): void
} }
declare module 'estree-walker' {
export function walk<T>(
root: T,
options: {
enter?: (node: T, parent: T | undefined) => any
leave?: (node: T, parent: T | undefined) => any
exit?: (node: T) => any
} & ThisType<{ skip: () => void }>
)
}
declare interface String { declare interface String {
/** /**
* @deprecated Please use String.prototype.slice instead of String.prototype.substring in the repository. * @deprecated Please use String.prototype.slice instead of String.prototype.substring in the repository.

View File

@ -28,14 +28,14 @@
}, },
"homepage": "https://github.com/vuejs/core/tree/dev/packages/reactivity-transform#readme", "homepage": "https://github.com/vuejs/core/tree/dev/packages/reactivity-transform#readme",
"dependencies": { "dependencies": {
"@babel/parser": "^7.20.15", "@babel/parser": "^7.23.0",
"@vue/compiler-core": "3.3.4", "@vue/compiler-core": "3.3.4",
"@vue/shared": "3.3.4", "@vue/shared": "3.3.4",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"magic-string": "^0.30.0" "magic-string": "^0.30.5"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.21.3", "@babel/core": "^7.23.2",
"@babel/types": "^7.21.3" "@babel/types": "^7.23.0"
} }
} }

View File

@ -636,7 +636,7 @@ export function transformAST(
// check root scope first // check root scope first
walkScope(ast, true) walkScope(ast, true)
;(walk as any)(ast, { walk(ast, {
enter(node: Node, parent?: Node) { enter(node: Node, parent?: Node) {
parent && parentStack.push(parent) parent && parentStack.push(parent)

View File

@ -259,13 +259,13 @@ describe('reactivity/computed', () => {
const onTrigger = vi.fn((e: DebuggerEvent) => { const onTrigger = vi.fn((e: DebuggerEvent) => {
events.push(e) events.push(e)
}) })
const obj = reactive({ foo: 1 }) const obj = reactive<{ foo?: number }>({ foo: 1 })
const c = computed(() => obj.foo, { onTrigger }) const c = computed(() => obj.foo, { onTrigger })
// computed won't trigger compute until accessed // computed won't trigger compute until accessed
c.value c.value
obj.foo++ obj.foo!++
expect(c.value).toBe(2) expect(c.value).toBe(2)
expect(onTrigger).toHaveBeenCalledTimes(1) expect(onTrigger).toHaveBeenCalledTimes(1)
expect(events[0]).toEqual({ expect(events[0]).toEqual({
@ -277,7 +277,6 @@ describe('reactivity/computed', () => {
newValue: 2 newValue: 2
}) })
// @ts-ignore
delete obj.foo delete obj.foo
expect(c.value).toBeUndefined() expect(c.value).toBeUndefined()
expect(onTrigger).toHaveBeenCalledTimes(2) expect(onTrigger).toHaveBeenCalledTimes(2)

View File

@ -585,6 +585,14 @@ describe('reactivity/effect', () => {
expect(runner.effect.fn).toBe(otherRunner.effect.fn) 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', () => { it('should not run multiple times for a single mutation', () => {
let dummy let dummy
const obj = reactive<Record<string, number>>({}) const obj = reactive<Record<string, number>>({})

View File

@ -23,7 +23,7 @@ describe('reactivity/reactive', () => {
const reactiveObj = reactive(obj) const reactiveObj = reactive(obj)
expect(isReactive(reactiveObj)).toBe(true) expect(isReactive(reactiveObj)).toBe(true)
// read prop of reactiveObject will cause reactiveObj[prop] to be reactive // read prop of reactiveObject will cause reactiveObj[prop] to be reactive
// @ts-ignore // @ts-expect-error
const prototype = reactiveObj['__proto__'] const prototype = reactiveObj['__proto__']
const otherObj = { data: ['a'] } const otherObj = { data: ['a'] }
expect(isReactive(otherObj)).toBe(false) expect(isReactive(otherObj)).toBe(false)
@ -204,7 +204,7 @@ describe('reactivity/reactive', () => {
const dummy = computed(() => observed.a) const dummy = computed(() => observed.a)
expect(dummy.value).toBe(0) expect(dummy.value).toBe(0)
// @ts-ignore // @ts-expect-error
observed.a = bar observed.a = bar
expect(dummy.value).toBe(1) expect(dummy.value).toBe(1)
@ -233,6 +233,9 @@ describe('reactivity/reactive', () => {
// symbol // symbol
const s = Symbol() const s = Symbol()
assertValue(s) assertValue(s)
// bigint
const bn = BigInt('9007199254740991')
assertValue(bn)
// built-ins should work and return same value // built-ins should work and return same value
const p = Promise.resolve() const p = Promise.resolve()

View File

@ -28,19 +28,18 @@ describe('reactivity/ref', () => {
it('should be reactive', () => { it('should be reactive', () => {
const a = ref(1) const a = ref(1)
let dummy let dummy
let calls = 0 const fn = vi.fn(() => {
effect(() => {
calls++
dummy = a.value dummy = a.value
}) })
expect(calls).toBe(1) effect(fn)
expect(fn).toHaveBeenCalledTimes(1)
expect(dummy).toBe(1) expect(dummy).toBe(1)
a.value = 2 a.value = 2
expect(calls).toBe(2) expect(fn).toHaveBeenCalledTimes(2)
expect(dummy).toBe(2) expect(dummy).toBe(2)
// same value should not trigger // same value should not trigger
a.value = 2 a.value = 2
expect(calls).toBe(2) expect(fn).toHaveBeenCalledTimes(2)
}) })
it('should make nested properties reactive', () => { it('should make nested properties reactive', () => {

View File

@ -26,7 +26,6 @@ import {
hasChanged, hasChanged,
isArray, isArray,
isIntegerKey, isIntegerKey,
extend,
makeMap makeMap
} from '@vue/shared' } from '@vue/shared'
import { isRef } from './ref' import { isRef } from './ref'
@ -45,11 +44,6 @@ const builtInSymbols = new Set(
.filter(isSymbol) .filter(isSymbol)
) )
const get = /*#__PURE__*/ createGetter()
const shallowGet = /*#__PURE__*/ createGetter(false, true)
const readonlyGet = /*#__PURE__*/ createGetter(true)
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)
const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations() const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()
function createArrayInstrumentations() { function createArrayInstrumentations() {
@ -91,8 +85,15 @@ function hasOwnProperty(this: object, key: string) {
return obj.hasOwnProperty(key) return obj.hasOwnProperty(key)
} }
function createGetter(isReadonly = false, shallow = false) { class BaseReactiveHandler implements ProxyHandler<Target> {
return function get(target: Target, key: string | symbol, receiver: object) { constructor(
protected readonly _isReadonly = false,
protected readonly _shallow = false
) {}
get(target: Target, key: string | symbol, receiver: object) {
const isReadonly = this._isReadonly,
shallow = this._shallow
if (key === ReactiveFlags.IS_REACTIVE) { if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) { } else if (key === ReactiveFlags.IS_READONLY) {
@ -155,11 +156,12 @@ function createGetter(isReadonly = false, shallow = false) {
} }
} }
const set = /*#__PURE__*/ createSetter() class MutableReactiveHandler extends BaseReactiveHandler {
const shallowSet = /*#__PURE__*/ createSetter(true) constructor(shallow = false) {
super(false, shallow)
}
function createSetter(shallow = false) { set(
return function set(
target: object, target: object,
key: string | symbol, key: string | symbol,
value: unknown, value: unknown,
@ -169,7 +171,7 @@ function createSetter(shallow = false) {
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) { if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
return false return false
} }
if (!shallow) { if (!this._shallow) {
if (!isShallow(value) && !isReadonly(value)) { if (!isShallow(value) && !isReadonly(value)) {
oldValue = toRaw(oldValue) oldValue = toRaw(oldValue)
value = toRaw(value) value = toRaw(value)
@ -197,42 +199,40 @@ function createSetter(shallow = false) {
} }
return result return result
} }
}
function deleteProperty(target: object, key: string | symbol): boolean { deleteProperty(target: object, key: string | symbol): boolean {
const hadKey = hasOwn(target, key) const hadKey = hasOwn(target, key)
const oldValue = (target as any)[key] const oldValue = (target as any)[key]
const result = Reflect.deleteProperty(target, key) const result = Reflect.deleteProperty(target, key)
if (result && hadKey) { if (result && hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue) trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
} }
return result
}
function has(target: object, key: string | symbol): boolean { has(target: object, key: string | symbol): boolean {
const result = Reflect.has(target, key) const result = Reflect.has(target, key)
if (!isSymbol(key) || !builtInSymbols.has(key)) { if (!isSymbol(key) || !builtInSymbols.has(key)) {
track(target, TrackOpTypes.HAS, key) track(target, TrackOpTypes.HAS, key)
}
return result
}
ownKeys(target: object): (string | symbol)[] {
track(
target,
TrackOpTypes.ITERATE,
isArray(target) ? 'length' : ITERATE_KEY
)
return Reflect.ownKeys(target)
} }
return result
} }
function ownKeys(target: object): (string | symbol)[] { class ReadonlyReactiveHandler extends BaseReactiveHandler {
track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY) constructor(shallow = false) {
return Reflect.ownKeys(target) super(true, shallow)
} }
export const mutableHandlers: ProxyHandler<object> = { set(target: object, key: string | symbol) {
get,
set,
deleteProperty,
has,
ownKeys
}
export const readonlyHandlers: ProxyHandler<object> = {
get: readonlyGet,
set(target, key) {
if (__DEV__) { if (__DEV__) {
warn( warn(
`Set operation on key "${String(key)}" failed: target is readonly.`, `Set operation on key "${String(key)}" failed: target is readonly.`,
@ -240,8 +240,9 @@ export const readonlyHandlers: ProxyHandler<object> = {
) )
} }
return true return true
}, }
deleteProperty(target, key) {
deleteProperty(target: object, key: string | symbol) {
if (__DEV__) { if (__DEV__) {
warn( warn(
`Delete operation on key "${String(key)}" failed: target is readonly.`, `Delete operation on key "${String(key)}" failed: target is readonly.`,
@ -252,22 +253,18 @@ export const readonlyHandlers: ProxyHandler<object> = {
} }
} }
export const shallowReactiveHandlers = /*#__PURE__*/ extend( export const mutableHandlers: ProxyHandler<object> =
{}, /*#__PURE__*/ new MutableReactiveHandler()
mutableHandlers,
{ export const readonlyHandlers: ProxyHandler<object> =
get: shallowGet, /*#__PURE__*/ new ReadonlyReactiveHandler()
set: shallowSet
} export const shallowReactiveHandlers = /*#__PURE__*/ new MutableReactiveHandler(
true
) )
// Props handlers are special in the sense that it should not unwrap top-level // Props handlers are special in the sense that it should not unwrap top-level
// refs (in order to allow refs to be explicitly passed down), but should // refs (in order to allow refs to be explicitly passed down), but should
// retain the reactivity of the normal readonly object. // retain the reactivity of the normal readonly object.
export const shallowReadonlyHandlers = /*#__PURE__*/ extend( export const shallowReadonlyHandlers =
{}, /*#__PURE__*/ new ReadonlyReactiveHandler(true)
readonlyHandlers,
{
get: shallowReadonlyGet
}
)

View File

@ -27,7 +27,7 @@ function get(
const rawTarget = toRaw(target) const rawTarget = toRaw(target)
const rawKey = toRaw(key) const rawKey = toRaw(key)
if (!isReadonly) { if (!isReadonly) {
if (key !== rawKey) { if (hasChanged(key, rawKey)) {
track(rawTarget, TrackOpTypes.GET, key) track(rawTarget, TrackOpTypes.GET, key)
} }
track(rawTarget, TrackOpTypes.GET, rawKey) track(rawTarget, TrackOpTypes.GET, rawKey)
@ -50,7 +50,7 @@ function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
const rawTarget = toRaw(target) const rawTarget = toRaw(target)
const rawKey = toRaw(key) const rawKey = toRaw(key)
if (!isReadonly) { if (!isReadonly) {
if (key !== rawKey) { if (hasChanged(key, rawKey)) {
track(rawTarget, TrackOpTypes.HAS, key) track(rawTarget, TrackOpTypes.HAS, key)
} }
track(rawTarget, TrackOpTypes.HAS, rawKey) track(rawTarget, TrackOpTypes.HAS, rawKey)

View File

@ -16,7 +16,7 @@ import { ComputedRefImpl } from './computed'
// which maintains a Set of subscribers, but we simply store them as // which maintains a Set of subscribers, but we simply store them as
// raw Sets to reduce memory overhead. // raw Sets to reduce memory overhead.
type KeyToDepMap = Map<any, Dep> type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>() const targetMap = new WeakMap<object, KeyToDepMap>()
// The number of effects currently being tracked recursively. // The number of effects currently being tracked recursively.
let effectTrackDepth = 0 let effectTrackDepth = 0
@ -181,7 +181,7 @@ export function effect<T = any>(
fn: () => T, fn: () => T,
options?: ReactiveEffectOptions options?: ReactiveEffectOptions
): ReactiveEffectRunner { ): ReactiveEffectRunner {
if ((fn as ReactiveEffectRunner).effect) { if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) {
fn = (fn as ReactiveEffectRunner).effect.fn fn = (fn as ReactiveEffectRunner).effect.fn
} }

View File

@ -138,7 +138,10 @@ class RefImpl<T> {
public dep?: Dep = undefined public dep?: Dep = undefined
public readonly __v_isRef = true public readonly __v_isRef = true
constructor(value: T, public readonly __v_isShallow: boolean) { constructor(
value: T,
public readonly __v_isShallow: boolean
) {
this._rawValue = __v_isShallow ? value : toRaw(value) this._rawValue = __v_isShallow ? value : toRaw(value)
this._value = __v_isShallow ? value : toReactive(value) this._value = __v_isShallow ? value : toReactive(value)
} }
@ -342,7 +345,7 @@ class ObjectRefImpl<T extends object, K extends keyof T> {
get value() { get value() {
const val = this._object[this._key] const val = this._object[this._key]
return val === undefined ? (this._defaultValue as T[K]) : val return val === undefined ? this._defaultValue! : val
} }
set value(newVal) { set value(newVal) {

View File

@ -382,7 +382,7 @@ describe('api: options', () => {
render() { render() {
return this[injectedKey] return this[injectedKey]
} }
} as any) }) as any
const ChildA = defineChild(['a'], 'a') const ChildA = defineChild(['a'], 'a')
const ChildB = defineChild({ b: 'a' }) const ChildB = defineChild({ b: 'a' })

View File

@ -1000,7 +1000,7 @@ describe('api: watch', () => {
}, },
mounted() { mounted() {
// this call runs while Comp is currentInstance, but // this call runs while Comp is currentInstance, but
// the effect for this `$watch` should nontheless be registered with Child // the effect for this `$watch` should nonetheless be registered with Child
this.comp!.$watch( this.comp!.$watch(
() => this.show, () => this.show,
() => void 0 () => void 0
@ -1171,7 +1171,7 @@ describe('api: watch', () => {
expect(instance!.scope.effects.length).toBe(1) expect(instance!.scope.effects.length).toBe(1)
}) })
test('watchEffect should keep running if created in a detatched scope', async () => { test('watchEffect should keep running if created in a detached scope', async () => {
const trigger = ref(0) const trigger = ref(0)
let countWE = 0 let countWE = 0
let countW = 0 let countW = 0

View File

@ -475,4 +475,57 @@ describe('renderer: teleport', () => {
expect(dir.mounted).toHaveBeenCalledTimes(1) expect(dir.mounted).toHaveBeenCalledTimes(1)
expect(dir.unmounted).toHaveBeenCalledTimes(1) expect(dir.unmounted).toHaveBeenCalledTimes(1)
}) })
// #7835
test(`ensure that target changes when disabled are updated correctly when enabled`, async () => {
const root = nodeOps.createElement('div')
const target1 = nodeOps.createElement('div')
const target2 = nodeOps.createElement('div')
const target3 = nodeOps.createElement('div')
const target = ref(target1)
const disabled = ref(true)
const App = {
setup() {
return () =>
h(Fragment, [
h(
Teleport,
{ to: target.value, disabled: disabled.value },
h('div', 'teleported')
)
])
}
}
render(h(App), root)
disabled.value = false
await nextTick()
expect(serializeInner(target1)).toMatchInlineSnapshot(
`"<div>teleported</div>"`
)
expect(serializeInner(target2)).toMatchInlineSnapshot(`""`)
expect(serializeInner(target3)).toMatchInlineSnapshot(`""`)
disabled.value = true
await nextTick()
target.value = target2
await nextTick()
expect(serializeInner(target1)).toMatchInlineSnapshot(`""`)
expect(serializeInner(target2)).toMatchInlineSnapshot(`""`)
expect(serializeInner(target3)).toMatchInlineSnapshot(`""`)
target.value = target3
await nextTick()
expect(serializeInner(target1)).toMatchInlineSnapshot(`""`)
expect(serializeInner(target2)).toMatchInlineSnapshot(`""`)
expect(serializeInner(target3)).toMatchInlineSnapshot(`""`)
disabled.value = false
await nextTick()
expect(serializeInner(target1)).toMatchInlineSnapshot(`""`)
expect(serializeInner(target2)).toMatchInlineSnapshot(`""`)
expect(serializeInner(target3)).toMatchInlineSnapshot(
`"<div>teleported</div>"`
)
})
}) })

View File

@ -116,7 +116,7 @@ describe('api: template refs', () => {
const toggle = ref(true) const toggle = ref(true)
const Comp = defineComponent( const Comp = defineComponent(
() => () => toggle.value ? h('div', { ref: fn }) : null () => () => (toggle.value ? h('div', { ref: fn }) : null)
) )
render(h(Comp), root) render(h(Comp), root)
expect(fn.mock.calls[0][0]).toBe(root.children[0]) expect(fn.mock.calls[0][0]).toBe(root.children[0])

View File

@ -546,4 +546,16 @@ describe('scheduler', () => {
await nextTick() await nextTick()
expect(spy).toHaveBeenCalledTimes(1) expect(spy).toHaveBeenCalledTimes(1)
}) })
it('nextTick should return promise', async () => {
const fn = vi.fn(() => {
return 1
})
const p = nextTick(fn)
expect(p).toBeInstanceOf(Promise)
expect(await p).toBe(1)
expect(fn).toHaveBeenCalledTimes(1)
})
}) })

View File

@ -40,6 +40,7 @@ export interface AsyncComponentOptions<T = any> {
export const isAsyncWrapper = (i: ComponentInternalInstance | VNode): boolean => export const isAsyncWrapper = (i: ComponentInternalInstance | VNode): boolean =>
!!(i.type as ComponentOptions).__asyncLoader !!(i.type as ComponentOptions).__asyncLoader
/*! #__NO_SIDE_EFFECTS__ */
export function defineAsyncComponent< export function defineAsyncComponent<
T extends Component = { new (): ComponentPublicInstance } T extends Component = { new (): ComponentPublicInstance }
>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T { >(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {

View File

@ -221,7 +221,7 @@ export function createAppAPI<HostElement>(
set() { set() {
warn( warn(
`app.config.unwrapInjectedRef has been deprecated. ` + `app.config.unwrapInjectedRef has been deprecated. ` +
`3.3 now alawys unwraps injected refs in Options API.` `3.3 now always unwraps injected refs in Options API.`
) )
} }
}) })

View File

@ -274,6 +274,7 @@ export function defineComponent<
> >
// implementation, close to no-op // implementation, close to no-op
/*! #__NO_SIDE_EFFECTS__ */
export function defineComponent( export function defineComponent(
options: unknown, options: unknown,
extraOptions?: ComponentOptions extraOptions?: ComponentOptions

View File

@ -303,7 +303,13 @@ type PropsWithDefaults<
? T[K] ? T[K]
: NotUndefined<T[K]> : NotUndefined<T[K]>
: never : never
} & { readonly [K in BKeys]-?: boolean } } & {
readonly [K in BKeys]-?: K extends keyof Defaults
? Defaults[K] extends undefined
? boolean | undefined
: boolean
: boolean
}
/** /**
* Vue `<script setup>` compiler macro for providing props default values when * Vue `<script setup>` compiler macro for providing props default values when

View File

@ -58,6 +58,7 @@ export interface LegacyPublicProperties {
export function installCompatInstanceProperties(map: PublicPropertiesMap) { export function installCompatInstanceProperties(map: PublicPropertiesMap) {
const set = (target: any, key: any, val: any) => { const set = (target: any, key: any, val: any) => {
target[key] = val target[key] = val
return target[key]
} }
const del = (target: any, key: any) => { const del = (target: any, key: any) => {

View File

@ -256,7 +256,7 @@ export interface ComponentInternalInstance {
*/ */
ssrRender?: Function | null ssrRender?: Function | null
/** /**
* Object containing values this component provides for its descendents * Object containing values this component provides for its descendants
* @internal * @internal
*/ */
provides: Data provides: Data

View File

@ -22,6 +22,9 @@ import { RendererElement } from '../renderer'
type Hook<T = () => void> = T | T[] type Hook<T = () => void> = T | T[]
const leaveCbKey = Symbol('_leaveCb')
const enterCbKey = Symbol('_enterCb')
export interface BaseTransitionProps<HostElement = RendererElement> { export interface BaseTransitionProps<HostElement = RendererElement> {
mode?: 'in-out' | 'out-in' | 'default' mode?: 'in-out' | 'out-in' | 'default'
appear?: boolean appear?: boolean
@ -89,8 +92,8 @@ export interface TransitionElement {
// in persisted mode (e.g. v-show), the same element is toggled, so the // in persisted mode (e.g. v-show), the same element is toggled, so the
// pending enter/leave callbacks may need to be cancelled if the state is toggled // pending enter/leave callbacks may need to be cancelled if the state is toggled
// before it finishes. // before it finishes.
_enterCb?: PendingCallback [enterCbKey]?: PendingCallback
_leaveCb?: PendingCallback [leaveCbKey]?: PendingCallback
} }
export function useTransitionState(): TransitionState { export function useTransitionState(): TransitionState {
@ -259,9 +262,9 @@ const BaseTransitionImpl: ComponentOptions = {
) )
leavingVNodesCache[String(oldInnerChild.key)] = oldInnerChild leavingVNodesCache[String(oldInnerChild.key)] = oldInnerChild
// early removal callback // early removal callback
el._leaveCb = () => { el[leaveCbKey] = () => {
earlyRemove() earlyRemove()
el._leaveCb = undefined el[leaveCbKey] = undefined
delete enterHooks.delayedLeave delete enterHooks.delayedLeave
} }
enterHooks.delayedLeave = delayedLeave enterHooks.delayedLeave = delayedLeave
@ -366,18 +369,18 @@ export function resolveTransitionHooks(
} }
} }
// for same element (v-show) // for same element (v-show)
if (el._leaveCb) { if (el[leaveCbKey]) {
el._leaveCb(true /* cancelled */) el[leaveCbKey](true /* cancelled */)
} }
// for toggled element with same key (v-if) // for toggled element with same key (v-if)
const leavingVNode = leavingVNodesCache[key] const leavingVNode = leavingVNodesCache[key]
if ( if (
leavingVNode && leavingVNode &&
isSameVNodeType(vnode, leavingVNode) && isSameVNodeType(vnode, leavingVNode) &&
leavingVNode.el!._leaveCb (leavingVNode.el as TransitionElement)[leaveCbKey]
) { ) {
// force early removal (not cancelled) // force early removal (not cancelled)
leavingVNode.el!._leaveCb() ;(leavingVNode.el as TransitionElement)[leaveCbKey]!()
} }
callHook(hook, [el]) callHook(hook, [el])
}, },
@ -396,7 +399,7 @@ export function resolveTransitionHooks(
} }
} }
let called = false let called = false
const done = (el._enterCb = (cancelled?) => { const done = (el[enterCbKey] = (cancelled?) => {
if (called) return if (called) return
called = true called = true
if (cancelled) { if (cancelled) {
@ -407,7 +410,7 @@ export function resolveTransitionHooks(
if (hooks.delayedLeave) { if (hooks.delayedLeave) {
hooks.delayedLeave() hooks.delayedLeave()
} }
el._enterCb = undefined el[enterCbKey] = undefined
}) })
if (hook) { if (hook) {
callAsyncHook(hook, [el, done]) callAsyncHook(hook, [el, done])
@ -418,15 +421,15 @@ export function resolveTransitionHooks(
leave(el, remove) { leave(el, remove) {
const key = String(vnode.key) const key = String(vnode.key)
if (el._enterCb) { if (el[enterCbKey]) {
el._enterCb(true /* cancelled */) el[enterCbKey](true /* cancelled */)
} }
if (state.isUnmounting) { if (state.isUnmounting) {
return remove() return remove()
} }
callHook(onBeforeLeave, [el]) callHook(onBeforeLeave, [el])
let called = false let called = false
const done = (el._leaveCb = (cancelled?) => { const done = (el[leaveCbKey] = (cancelled?) => {
if (called) return if (called) return
called = true called = true
remove() remove()
@ -435,7 +438,7 @@ export function resolveTransitionHooks(
} else { } else {
callHook(onAfterLeave, [el]) callHook(onAfterLeave, [el])
} }
el._leaveCb = undefined el[leaveCbKey] = undefined
if (leavingVNodesCache[key] === vnode) { if (leavingVNodesCache[key] === vnode) {
delete leavingVNodesCache[key] delete leavingVNodesCache[key]
} }

View File

@ -186,6 +186,13 @@ export const TeleportImpl = {
internals, internals,
TeleportMoveTypes.TOGGLE TeleportMoveTypes.TOGGLE
) )
} else {
// #7835
// When `teleport` is disabled, `to` may change, making it always old,
// to ensure the correct `to` when enabled
if (n2.props && n1.props && n2.props.to !== n1.props.to) {
n2.props.to = n1.props.to
}
} }
} else { } else {
// target changed // target changed
@ -393,7 +400,7 @@ function hydrateTeleport(
// Force-casted public typing for h and TSX props inference // Force-casted public typing for h and TSX props inference
export const Teleport = TeleportImpl as unknown as { export const Teleport = TeleportImpl as unknown as {
__isTeleport: true __isTeleport: true
new(): { new (): {
$props: VNodeProps & TeleportProps $props: VNodeProps & TeleportProps
$slots: { $slots: {
default(): VNode[] default(): VNode[]

View File

@ -30,7 +30,7 @@ interface DevtoolsHook {
appRecords: AppRecord[] appRecords: AppRecord[]
/** /**
* Added at https://github.com/vuejs/devtools/commit/f2ad51eea789006ab66942e5a27c0f0986a257f9 * Added at https://github.com/vuejs/devtools/commit/f2ad51eea789006ab66942e5a27c0f0986a257f9
* Returns wether the arg was buffered or not * Returns whether the arg was buffered or not
*/ */
cleanupBuffer?: (matchArg: unknown) => boolean cleanupBuffer?: (matchArg: unknown) => boolean
} }

View File

@ -134,8 +134,10 @@ export function createHydrationFunctions(
__DEV__ && __DEV__ &&
warn( warn(
`Hydration text mismatch:` + `Hydration text mismatch:` +
`\n- Client: ${JSON.stringify((node as Text).data)}` + `\n- Server rendered: ${JSON.stringify(
`\n- Server: ${JSON.stringify(vnode.children)}` (node as Text).data
)}` +
`\n- Client rendered: ${JSON.stringify(vnode.children)}`
) )
;(node as Text).data = vnode.children as string ;(node as Text).data = vnode.children as string
} }
@ -406,8 +408,8 @@ export function createHydrationFunctions(
`Hydration text content mismatch in <${ `Hydration text content mismatch in <${
vnode.type as string vnode.type as string
}>:\n` + }>:\n` +
`- Client: ${el.textContent}\n` + `- Server rendered: ${el.textContent}\n` +
`- Server: ${vnode.children as string}` `- Client rendered: ${vnode.children as string}`
) )
el.textContent = vnode.children as string el.textContent = vnode.children as string
} }

View File

@ -73,8 +73,13 @@ export {
defineSlots, defineSlots,
defineModel, defineModel,
withDefaults, withDefaults,
useModel, useModel
// internal } from './apiSetupHelpers'
/**
* @internal
*/
export {
mergeDefaults, mergeDefaults,
mergeModels, mergeModels,
createPropsRestProxy, createPropsRestProxy,
@ -111,7 +116,9 @@ export { useSSRContext, ssrContextKey } from './helpers/useSsrContext'
export { createRenderer, createHydrationRenderer } from './renderer' export { createRenderer, createHydrationRenderer } from './renderer'
export { queuePostFlushCb } from './scheduler' export { queuePostFlushCb } from './scheduler'
export { warn, assertNumber } from './warning' export { warn } from './warning'
/** @internal */
export { assertNumber } from './warning'
export { export {
handleError, handleError,
callWithErrorHandling, callWithErrorHandling,

View File

@ -584,7 +584,7 @@ function baseCreateRenderer(
slotScopeIds: string[] | null, slotScopeIds: string[] | null,
optimized: boolean optimized: boolean
) => { ) => {
isSVG = isSVG || (n2.type as string) === 'svg' isSVG = isSVG || n2.type === 'svg'
if (n1 == null) { if (n1 == null) {
mountElement( mountElement(
n2, n2,

View File

@ -1,5 +1,5 @@
import { ErrorCodes, callWithErrorHandling } from './errorHandling' import { ErrorCodes, callWithErrorHandling } from './errorHandling'
import { isArray, NOOP } from '@vue/shared' import { Awaited, isArray, NOOP } from '@vue/shared'
import { ComponentInternalInstance, getComponentName } from './component' import { ComponentInternalInstance, getComponentName } from './component'
import { warn } from './warning' import { warn } from './warning'
@ -50,10 +50,10 @@ let currentFlushPromise: Promise<void> | null = null
const RECURSION_LIMIT = 100 const RECURSION_LIMIT = 100
type CountMap = Map<SchedulerJob, number> type CountMap = Map<SchedulerJob, number>
export function nextTick<T = void>( export function nextTick<T = void, R = void>(
this: T, this: T,
fn?: (this: T) => void fn?: (this: T) => R
): Promise<void> { ): Promise<Awaited<R>> {
const p = currentFlushPromise || resolvedPromise const p = currentFlushPromise || resolvedPromise
return fn ? p.then(this ? fn.bind(this) : fn) : p return fn ? p.then(this ? fn.bind(this) : fn) : p
} }

View File

@ -681,7 +681,7 @@ export function cloneVNode<T, U>(
if (__COMPAT__) { if (__COMPAT__) {
defineLegacyVNodeProperties(cloned as VNode) defineLegacyVNodeProperties(cloned as VNode)
} }
return cloned as any return cloned
} }
/** /**

View File

@ -322,7 +322,7 @@ describe('defineCustomElement', () => {
emit('my-click', 1) emit('my-click', 1)
}, },
onMousedown: () => { onMousedown: () => {
emit('myEvent', 1) // validate hypenization emit('myEvent', 1) // validate hyphenation
} }
}) })
} }

View File

@ -6,8 +6,10 @@ function triggerEvent(
event: string, event: string,
process?: (e: any) => any process?: (e: any) => any
) { ) {
const e = document.createEvent('HTMLEvents') const e = new Event(event, {
e.initEvent(event, true, true) bubbles: true,
cancelable: true
})
if (event === 'click') { if (event === 'click') {
;(e as any).button = 0 ;(e as any).button = 0
} }

View File

@ -1,5 +1,5 @@
import { patchProp } from '../src/patchProp' import { patchProp } from '../src/patchProp'
import { ElementWithTransition } from '../src/components/Transition' import { ElementWithTransition, vtcKey } from '../src/components/Transition'
import { svgNS } from '../src/nodeOps' import { svgNS } from '../src/nodeOps'
describe('runtime-dom: class patching', () => { describe('runtime-dom: class patching', () => {
@ -13,12 +13,12 @@ describe('runtime-dom: class patching', () => {
test('transition class', () => { test('transition class', () => {
const el = document.createElement('div') as ElementWithTransition const el = document.createElement('div') as ElementWithTransition
el._vtc = new Set(['bar', 'baz']) el[vtcKey] = new Set(['bar', 'baz'])
patchProp(el, 'class', null, 'foo') patchProp(el, 'class', null, 'foo')
expect(el.className).toBe('foo bar baz') expect(el.className).toBe('foo bar baz')
patchProp(el, 'class', null, null) patchProp(el, 'class', null, null)
expect(el.className).toBe('bar baz') expect(el.className).toBe('bar baz')
delete el._vtc delete el[vtcKey]
patchProp(el, 'class', null, 'foo') patchProp(el, 'class', null, 'foo')
expect(el.className).toBe('foo') expect(el.className).toBe('foo')
}) })

View File

@ -37,6 +37,6 @@
"dependencies": { "dependencies": {
"@vue/shared": "3.3.4", "@vue/shared": "3.3.4",
"@vue/runtime-core": "3.3.4", "@vue/runtime-core": "3.3.4",
"csstype": "^3.1.1" "csstype": "^3.1.2"
} }
} }

View File

@ -140,6 +140,7 @@ export function defineCustomElement(options: {
new (...args: any[]): ComponentPublicInstance new (...args: any[]): ComponentPublicInstance
}): VueElementConstructor }): VueElementConstructor
/*! #__NO_SIDE_EFFECTS__ */
export function defineCustomElement( export function defineCustomElement(
options: any, options: any,
hydrate?: RootHydrateFunction hydrate?: RootHydrateFunction
@ -155,6 +156,7 @@ export function defineCustomElement(
return VueCustomElement return VueCustomElement
} }
/*! #__NO_SIDE_EFFECTS__ */
export const defineSSRCustomElement = ((options: any) => { export const defineSSRCustomElement = ((options: any) => {
// @ts-ignore // @ts-ignore
return defineCustomElement(options, hydrate) return defineCustomElement(options, hydrate)
@ -176,7 +178,7 @@ export class VueElement extends BaseClass {
private _resolved = false private _resolved = false
private _numberProps: Record<string, true> | null = null private _numberProps: Record<string, true> | null = null
private _styles?: HTMLStyleElement[] private _styles?: HTMLStyleElement[]
private _ob?: MutationObserver | null = null
constructor( constructor(
private _def: InnerComponentDef, private _def: InnerComponentDef,
private _props: Record<string, any> = {}, private _props: Record<string, any> = {},
@ -213,6 +215,10 @@ export class VueElement extends BaseClass {
disconnectedCallback() { disconnectedCallback() {
this._connected = false this._connected = false
if (this._ob) {
this._ob.disconnect()
this._ob = null
}
nextTick(() => { nextTick(() => {
if (!this._connected) { if (!this._connected) {
render(null, this.shadowRoot!) render(null, this.shadowRoot!)
@ -233,11 +239,13 @@ export class VueElement extends BaseClass {
} }
// watch future attr changes // watch future attr changes
new MutationObserver(mutations => { this._ob = new MutationObserver(mutations => {
for (const m of mutations) { for (const m of mutations) {
this._setAttr(m.attributeName!) this._setAttr(m.attributeName!)
} }
}).observe(this, { attributes: true }) })
this._ob.observe(this, { attributes: true })
const resolve = (def: InnerComponentDef, isAsync = false) => { const resolve = (def: InnerComponentDef, isAsync = false) => {
const { props, styles } = def const { props, styles } = def

View File

@ -32,12 +32,14 @@ export interface TransitionProps extends BaseTransitionProps<Element> {
leaveToClass?: string leaveToClass?: string
} }
export const vtcKey = Symbol('_vtc')
export interface ElementWithTransition extends HTMLElement { export interface ElementWithTransition extends HTMLElement {
// _vtc = Vue Transition Classes. // _vtc = Vue Transition Classes.
// Store the temporarily-added transition classes on the element // Store the temporarily-added transition classes on the element
// so that we can avoid overwriting them if the element's class is patched // so that we can avoid overwriting them if the element's class is patched
// during the transition. // during the transition.
_vtc?: Set<string> [vtcKey]?: Set<string>
} }
// DOM Transition is a higher-order-component based on the platform-agnostic // DOM Transition is a higher-order-component based on the platform-agnostic
@ -295,18 +297,18 @@ function NumberOf(val: unknown): number {
export function addTransitionClass(el: Element, cls: string) { export function addTransitionClass(el: Element, cls: string) {
cls.split(/\s+/).forEach(c => c && el.classList.add(c)) cls.split(/\s+/).forEach(c => c && el.classList.add(c))
;( ;(
(el as ElementWithTransition)._vtc || (el as ElementWithTransition)[vtcKey] ||
((el as ElementWithTransition)._vtc = new Set()) ((el as ElementWithTransition)[vtcKey] = new Set())
).add(cls) ).add(cls)
} }
export function removeTransitionClass(el: Element, cls: string) { export function removeTransitionClass(el: Element, cls: string) {
cls.split(/\s+/).forEach(c => c && el.classList.remove(c)) cls.split(/\s+/).forEach(c => c && el.classList.remove(c))
const { _vtc } = el as ElementWithTransition const _vtc = (el as ElementWithTransition)[vtcKey]
if (_vtc) { if (_vtc) {
_vtc.delete(cls) _vtc.delete(cls)
if (!_vtc!.size) { if (!_vtc!.size) {
;(el as ElementWithTransition)._vtc = undefined ;(el as ElementWithTransition)[vtcKey] = undefined
} }
} }
} }
@ -445,6 +447,8 @@ function getTimeout(delays: string[], durations: string[]): number {
// If comma is not replaced with a dot, the input will be rounded down // If comma is not replaced with a dot, the input will be rounded down
// (i.e. acting as a floor function) causing unexpected behaviors // (i.e. acting as a floor function) causing unexpected behaviors
function toMs(s: string): number { function toMs(s: string): number {
// #8409 default value for CSS durations can be 'auto'
if (s === 'auto') return 0
return Number(s.slice(0, -1).replace(',', '.')) * 1000 return Number(s.slice(0, -1).replace(',', '.')) * 1000
} }

View File

@ -6,7 +6,8 @@ import {
getTransitionInfo, getTransitionInfo,
resolveTransitionProps, resolveTransitionProps,
TransitionPropsValidators, TransitionPropsValidators,
forceReflow forceReflow,
vtcKey
} from './Transition' } from './Transition'
import { import {
Fragment, Fragment,
@ -29,7 +30,8 @@ import { extend } from '@vue/shared'
const positionMap = new WeakMap<VNode, DOMRect>() const positionMap = new WeakMap<VNode, DOMRect>()
const newPositionMap = new WeakMap<VNode, DOMRect>() const newPositionMap = new WeakMap<VNode, DOMRect>()
const moveCbKey = Symbol('_moveCb')
const enterCbKey = Symbol('_enterCb')
export type TransitionGroupProps = Omit<TransitionProps, 'mode'> & { export type TransitionGroupProps = Omit<TransitionProps, 'mode'> & {
tag?: string tag?: string
moveClass?: string moveClass?: string
@ -80,13 +82,13 @@ const TransitionGroupImpl: ComponentOptions = {
const style = el.style const style = el.style
addTransitionClass(el, moveClass) addTransitionClass(el, moveClass)
style.transform = style.webkitTransform = style.transitionDuration = '' style.transform = style.webkitTransform = style.transitionDuration = ''
const cb = ((el as any)._moveCb = (e: TransitionEvent) => { const cb = ((el as any)[moveCbKey] = (e: TransitionEvent) => {
if (e && e.target !== el) { if (e && e.target !== el) {
return return
} }
if (!e || /transform$/.test(e.propertyName)) { if (!e || /transform$/.test(e.propertyName)) {
el.removeEventListener('transitionend', cb) el.removeEventListener('transitionend', cb)
;(el as any)._moveCb = null ;(el as any)[moveCbKey] = null
removeTransitionClass(el, moveClass) removeTransitionClass(el, moveClass)
} }
}) })
@ -162,11 +164,11 @@ export const TransitionGroup = TransitionGroupImpl as unknown as {
function callPendingCbs(c: VNode) { function callPendingCbs(c: VNode) {
const el = c.el as any const el = c.el as any
if (el._moveCb) { if (el[moveCbKey]) {
el._moveCb() el[moveCbKey]()
} }
if (el._enterCb) { if (el[enterCbKey]) {
el._enterCb() el[enterCbKey]()
} }
} }
@ -198,8 +200,9 @@ function hasCSSTransform(
// all other transition classes applied to ensure only the move class // all other transition classes applied to ensure only the move class
// is applied. // is applied.
const clone = el.cloneNode() as HTMLElement const clone = el.cloneNode() as HTMLElement
if (el._vtc) { const _vtc = el[vtcKey]
el._vtc.forEach(cls => { if (_vtc) {
_vtc.forEach(cls => {
cls.split(/\s+/).forEach(c => c && clone.classList.remove(c)) cls.split(/\s+/).forEach(c => c && clone.classList.remove(c))
}) })
} }

View File

@ -36,7 +36,9 @@ function onCompositionEnd(e: Event) {
} }
} }
type ModelDirective<T> = ObjectDirective<T & { _assign: AssignerFn }> const assignKey = Symbol('_assign')
type ModelDirective<T> = ObjectDirective<T & { [assignKey]: AssignerFn }>
// We are exporting the v-model runtime directly as vnode hooks so that it can // We are exporting the v-model runtime directly as vnode hooks so that it can
// be tree-shaken in case v-model is never used. // be tree-shaken in case v-model is never used.
@ -44,7 +46,7 @@ export const vModelText: ModelDirective<
HTMLInputElement | HTMLTextAreaElement HTMLInputElement | HTMLTextAreaElement
> = { > = {
created(el, { modifiers: { lazy, trim, number } }, vnode) { created(el, { modifiers: { lazy, trim, number } }, vnode) {
el._assign = getModelAssigner(vnode) el[assignKey] = getModelAssigner(vnode)
const castToNumber = const castToNumber =
number || (vnode.props && vnode.props.type === 'number') number || (vnode.props && vnode.props.type === 'number')
addEventListener(el, lazy ? 'change' : 'input', e => { addEventListener(el, lazy ? 'change' : 'input', e => {
@ -56,7 +58,7 @@ export const vModelText: ModelDirective<
if (castToNumber) { if (castToNumber) {
domValue = looseToNumber(domValue) domValue = looseToNumber(domValue)
} }
el._assign(domValue) el[assignKey](domValue)
}) })
if (trim) { if (trim) {
addEventListener(el, 'change', () => { addEventListener(el, 'change', () => {
@ -78,7 +80,7 @@ export const vModelText: ModelDirective<
el.value = value == null ? '' : value el.value = value == null ? '' : value
}, },
beforeUpdate(el, { value, modifiers: { lazy, trim, number } }, vnode) { beforeUpdate(el, { value, modifiers: { lazy, trim, number } }, vnode) {
el._assign = getModelAssigner(vnode) el[assignKey] = getModelAssigner(vnode)
// avoid clearing unresolved text. #2302 // avoid clearing unresolved text. #2302
if ((el as any).composing) return if ((el as any).composing) return
if (document.activeElement === el && el.type !== 'range') { if (document.activeElement === el && el.type !== 'range') {
@ -106,12 +108,12 @@ export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
// #4096 array checkboxes need to be deep traversed // #4096 array checkboxes need to be deep traversed
deep: true, deep: true,
created(el, _, vnode) { created(el, _, vnode) {
el._assign = getModelAssigner(vnode) el[assignKey] = getModelAssigner(vnode)
addEventListener(el, 'change', () => { addEventListener(el, 'change', () => {
const modelValue = (el as any)._modelValue const modelValue = (el as any)._modelValue
const elementValue = getValue(el) const elementValue = getValue(el)
const checked = el.checked const checked = el.checked
const assign = el._assign const assign = el[assignKey]
if (isArray(modelValue)) { if (isArray(modelValue)) {
const index = looseIndexOf(modelValue, elementValue) const index = looseIndexOf(modelValue, elementValue)
const found = index !== -1 const found = index !== -1
@ -138,7 +140,7 @@ export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
// set initial checked on mount to wait for true-value/false-value // set initial checked on mount to wait for true-value/false-value
mounted: setChecked, mounted: setChecked,
beforeUpdate(el, binding, vnode) { beforeUpdate(el, binding, vnode) {
el._assign = getModelAssigner(vnode) el[assignKey] = getModelAssigner(vnode)
setChecked(el, binding, vnode) setChecked(el, binding, vnode)
} }
} }
@ -163,13 +165,13 @@ function setChecked(
export const vModelRadio: ModelDirective<HTMLInputElement> = { export const vModelRadio: ModelDirective<HTMLInputElement> = {
created(el, { value }, vnode) { created(el, { value }, vnode) {
el.checked = looseEqual(value, vnode.props!.value) el.checked = looseEqual(value, vnode.props!.value)
el._assign = getModelAssigner(vnode) el[assignKey] = getModelAssigner(vnode)
addEventListener(el, 'change', () => { addEventListener(el, 'change', () => {
el._assign(getValue(el)) el[assignKey](getValue(el))
}) })
}, },
beforeUpdate(el, { value, oldValue }, vnode) { beforeUpdate(el, { value, oldValue }, vnode) {
el._assign = getModelAssigner(vnode) el[assignKey] = getModelAssigner(vnode)
if (value !== oldValue) { if (value !== oldValue) {
el.checked = looseEqual(value, vnode.props!.value) el.checked = looseEqual(value, vnode.props!.value)
} }
@ -187,7 +189,7 @@ export const vModelSelect: ModelDirective<HTMLSelectElement> = {
.map((o: HTMLOptionElement) => .map((o: HTMLOptionElement) =>
number ? looseToNumber(getValue(o)) : getValue(o) number ? looseToNumber(getValue(o)) : getValue(o)
) )
el._assign( el[assignKey](
el.multiple el.multiple
? isSetModel ? isSetModel
? new Set(selectedVal) ? new Set(selectedVal)
@ -195,7 +197,7 @@ export const vModelSelect: ModelDirective<HTMLSelectElement> = {
: selectedVal[0] : selectedVal[0]
) )
}) })
el._assign = getModelAssigner(vnode) el[assignKey] = getModelAssigner(vnode)
}, },
// set value in mounted & updated because <select> relies on its children // set value in mounted & updated because <select> relies on its children
// <option>s. // <option>s.
@ -203,7 +205,7 @@ export const vModelSelect: ModelDirective<HTMLSelectElement> = {
setSelected(el, value) setSelected(el, value)
}, },
beforeUpdate(el, _binding, vnode) { beforeUpdate(el, _binding, vnode) {
el._assign = getModelAssigner(vnode) el[assignKey] = getModelAssigner(vnode)
}, },
updated(el, { value }) { updated(el, { value }) {
setSelected(el, value) setSelected(el, value)

View File

@ -1,13 +1,15 @@
import { ObjectDirective } from '@vue/runtime-core' import { ObjectDirective } from '@vue/runtime-core'
export const vShowOldKey = Symbol('_vod')
interface VShowElement extends HTMLElement { interface VShowElement extends HTMLElement {
// _vod = vue original display // _vod = vue original display
_vod: string [vShowOldKey]: string
} }
export const vShow: ObjectDirective<VShowElement> = { export const vShow: ObjectDirective<VShowElement> = {
beforeMount(el, { value }, { transition }) { beforeMount(el, { value }, { transition }) {
el._vod = el.style.display === 'none' ? '' : el.style.display el[vShowOldKey] = el.style.display === 'none' ? '' : el.style.display
if (transition && value) { if (transition && value) {
transition.beforeEnter(el) transition.beforeEnter(el)
} else { } else {
@ -41,7 +43,7 @@ export const vShow: ObjectDirective<VShowElement> = {
} }
function setDisplay(el: VShowElement, value: unknown): void { function setDisplay(el: VShowElement, value: unknown): void {
el.style.display = value ? el._vod : 'none' el.style.display = value ? el[vShowOldKey] : 'none'
} }
// SSR vnode transforms, only used when user includes client-oriented render // SSR vnode transforms, only used when user includes client-oriented render

View File

@ -248,8 +248,9 @@ export interface HTMLAttributes extends AriaAttributes, EventHandlers<Events> {
contextmenu?: string contextmenu?: string
dir?: string dir?: string
draggable?: Booleanish draggable?: Booleanish
hidden?: Booleanish hidden?: Booleanish | '' | 'hidden' | 'until-found'
id?: string id?: string
inert?: Booleanish
lang?: string lang?: string
placeholder?: string placeholder?: string
spellcheck?: Booleanish spellcheck?: Booleanish
@ -457,6 +458,7 @@ export interface ImgHTMLAttributes extends HTMLAttributes {
srcset?: string srcset?: string
usemap?: string usemap?: string
width?: Numberish width?: Numberish
loading?: 'lazy' | 'eager'
} }
export interface InsHTMLAttributes extends HTMLAttributes { export interface InsHTMLAttributes extends HTMLAttributes {
@ -464,6 +466,31 @@ export interface InsHTMLAttributes extends HTMLAttributes {
datetime?: string datetime?: string
} }
export type InputTypeHTMLAttribute =
| 'button'
| 'checkbox'
| 'color'
| 'date'
| 'datetime-local'
| 'email'
| 'file'
| 'hidden'
| 'image'
| 'month'
| 'number'
| 'password'
| 'radio'
| 'range'
| 'reset'
| 'search'
| 'submit'
| 'tel'
| 'text'
| 'time'
| 'url'
| 'week'
| (string & {})
export interface InputHTMLAttributes extends HTMLAttributes { export interface InputHTMLAttributes extends HTMLAttributes {
accept?: string accept?: string
alt?: string alt?: string
@ -495,7 +522,7 @@ export interface InputHTMLAttributes extends HTMLAttributes {
size?: Numberish size?: Numberish
src?: string src?: string
step?: Numberish step?: Numberish
type?: string type?: InputTypeHTMLAttribute
value?: any // we support :value to be bound to anything w/ v-model value?: any // we support :value to be bound to anything w/ v-model
width?: Numberish width?: Numberish
} }
@ -677,7 +704,7 @@ export interface TextareaHTMLAttributes extends HTMLAttributes {
minlength?: Numberish minlength?: Numberish
name?: string name?: string
placeholder?: string placeholder?: string
readonly?: boolean readonly?: Booleanish
required?: Booleanish required?: Booleanish
rows?: Numberish rows?: Numberish
value?: string | string[] | number value?: string | string[] | number
@ -749,7 +776,7 @@ export interface SVGAttributes extends AriaAttributes, EventHandlers<Events> {
* @see https://www.w3.org/TR/SVG/styling.html#ElementSpecificStyling * @see https://www.w3.org/TR/SVG/styling.html#ElementSpecificStyling
*/ */
class?: any class?: any
style?: string | CSSProperties style?: StyleValue
color?: string color?: string
height?: Numberish height?: Numberish

View File

@ -1,4 +1,4 @@
import { ElementWithTransition } from '../components/Transition' import { ElementWithTransition, vtcKey } from '../components/Transition'
// compiler should normalize class + :class bindings on the same element // compiler should normalize class + :class bindings on the same element
// into a single binding ['staticClass', dynamic] // into a single binding ['staticClass', dynamic]
@ -6,7 +6,7 @@ export function patchClass(el: Element, value: string | null, isSVG: boolean) {
// directly setting className should be faster than setAttribute in theory // directly setting className should be faster than setAttribute in theory
// if this is an element during a transition, take the temporary transition // if this is an element during a transition, take the temporary transition
// classes into account. // classes into account.
const transitionClasses = (el as ElementWithTransition)._vtc const transitionClasses = (el as ElementWithTransition)[vtcKey]
if (transitionClasses) { if (transitionClasses) {
value = ( value = (
value ? [value, ...transitionClasses] : [...transitionClasses] value ? [value, ...transitionClasses] : [...transitionClasses]

View File

@ -30,15 +30,17 @@ export function removeEventListener(
el.removeEventListener(event, handler, options) el.removeEventListener(event, handler, options)
} }
const veiKey = Symbol('_vei')
export function patchEvent( export function patchEvent(
el: Element & { _vei?: Record<string, Invoker | undefined> }, el: Element & { [veiKey]?: Record<string, Invoker | undefined> },
rawName: string, rawName: string,
prevValue: EventValue | null, prevValue: EventValue | null,
nextValue: EventValue | null, nextValue: EventValue | null,
instance: ComponentInternalInstance | null = null instance: ComponentInternalInstance | null = null
) { ) {
// vei = vue event invokers // vei = vue event invokers
const invokers = el._vei || (el._vei = {}) const invokers = el[veiKey] || (el[veiKey] = {})
const existingInvoker = invokers[rawName] const existingInvoker = invokers[rawName]
if (nextValue && existingInvoker) { if (nextValue && existingInvoker) {
// patch // patch

View File

@ -1,5 +1,6 @@
import { isString, hyphenate, capitalize, isArray } from '@vue/shared' import { isString, hyphenate, capitalize, isArray } from '@vue/shared'
import { camelize, warn } from '@vue/runtime-core' import { camelize, warn } from '@vue/runtime-core'
import { vShowOldKey } from '../directives/vShow'
type Style = string | Record<string, string | string[]> | null type Style = string | Record<string, string | string[]> | null
@ -29,7 +30,7 @@ export function patchStyle(el: Element, prev: Style, next: Style) {
// indicates that the `display` of the element is controlled by `v-show`, // indicates that the `display` of the element is controlled by `v-show`,
// so we always keep the current `display` value regardless of the `style` // so we always keep the current `display` value regardless of the `style`
// value, thus handing over control to `v-show`. // value, thus handing over control to `v-show`.
if ('_vod' in el) { if (vShowOldKey in el) {
style.display = currentDisplay style.display = currentDisplay
} }
} }

View File

@ -107,6 +107,30 @@ describe('ssr: directives', () => {
).toBe(`<input type="radio">`) ).toBe(`<input type="radio">`)
}) })
test('select', async () => {
expect(
await renderToString(
createApp({
data: () => ({ model: 1 }),
template: `<select v-model="model"><option value="0"></option><option value="1"></option></select>`
})
)
).toBe(
`<select><option value="0"></option><option value="1" selected></option></select>`
)
expect(
await renderToString(
createApp({
data: () => ({ model: [0, 1] }),
template: `<select multiple v-model="model"><option value="0"></option><option value="1"></option></select>`
})
)
).toBe(
`<select multiple><option value="0" selected></option><option value="1" selected></option></select>`
)
})
test('checkbox', async () => { test('checkbox', async () => {
expect( expect(
await renderToString( await renderToString(

View File

@ -156,7 +156,7 @@ describe('ssr: scopedId runtime behavior', () => {
}) })
// #3513 // #3513
test('scopeId inheritance across ssr-compiled andn on-ssr compiled parent chain', async () => { test('scopeId inheritance across ssr-compiled and on-ssr compiled parent chain', async () => {
const Child = { const Child = {
ssrRender: (ctx: any, push: any, parent: any, attrs: any) => { ssrRender: (ctx: any, push: any, parent: any, attrs: any) => {
push(`<div${ssrRenderAttrs(attrs)}></div>`) push(`<div${ssrRenderAttrs(attrs)}></div>`)

View File

@ -8,14 +8,14 @@
<title>Vue SFC Playground</title> <title>Vue SFC Playground</title>
<script> <script>
// process shim for old versions of @vue/compiler-sfc dependency // process shim for old versions of @vue/compiler-sfc dependency
window.process = { env: {} } window.process = { env: {} }
const savedPreferDark = localStorage.getItem('vue-sfc-playground-prefer-dark') const savedPreferDark = localStorage.getItem('vue-sfc-playground-prefer-dark')
if ( if (
savedPreferDark === 'true' || savedPreferDark === 'true' ||
(!savedPreferDark && window.matchMedia('(prefers-color-scheme: dark)').matches) (!savedPreferDark && window.matchMedia('(prefers-color-scheme: dark)').matches)
) { ) {
document.documentElement.classList.add('dark') document.documentElement.classList.add('dark')
} }
</script> </script>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</head> </head>

View File

@ -1,6 +1,7 @@
{ {
"name": "@vue/sfc-playground", "name": "@vue/sfc-playground",
"version": "3.3.4", "version": "3.3.4",
"type": "module",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@ -8,13 +9,13 @@
"serve": "vite preview" "serve": "vite preview"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^4.2.3", "@vitejs/plugin-vue": "^4.4.0",
"vite": "^4.3.0" "vite": "^4.5.0"
}, },
"dependencies": { "dependencies": {
"@vue/repl": "^1.4.1", "@vue/repl": "^2.5.8",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"jszip": "^3.6.0", "jszip": "^3.10.1",
"vue": "workspace:*" "vue": "workspace:*"
} }
} }

View File

@ -1,7 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import Header from './Header.vue' import Header from './Header.vue'
import { Repl, ReplStore, SFCOptions } from '@vue/repl' import { Repl, ReplStore, SFCOptions } from '@vue/repl'
import { ref, watchEffect } from 'vue' import Monaco from '@vue/repl/monaco-editor'
import { ref, watchEffect, onMounted } from 'vue'
const setVH = () => { const setVH = () => {
document.documentElement.style.setProperty('--vh', window.innerHeight + `px`) document.documentElement.style.setProperty('--vh', window.innerHeight + `px`)
@ -71,6 +72,15 @@ function toggleSSR() {
useSSRMode.value = !useSSRMode.value useSSRMode.value = !useSSRMode.value
store.setFiles(store.getFiles()) store.setFiles(store.getFiles())
} }
const theme = ref<'dark' | 'light'>('dark')
function toggleTheme(isDark: boolean) {
theme.value = isDark ? 'dark' : 'light'
}
onMounted(() => {
const cls = document.documentElement.classList
toggleTheme(cls.contains('dark'))
})
</script> </script>
<template> <template>
@ -78,10 +88,13 @@ function toggleSSR() {
:store="store" :store="store"
:dev="useDevMode" :dev="useDevMode"
:ssr="useSSRMode" :ssr="useSSRMode"
@toggle-theme="toggleTheme"
@toggle-dev="toggleDevMode" @toggle-dev="toggleDevMode"
@toggle-ssr="toggleSSR" @toggle-ssr="toggleSSR"
/> />
<Repl <Repl
:theme="theme"
:editor="Monaco"
@keydown.ctrl.s.prevent @keydown.ctrl.s.prevent
@keydown.meta.s.prevent @keydown.meta.s.prevent
:ssr="useSSRMode" :ssr="useSSRMode"
@ -108,7 +121,7 @@ body {
} }
.vue-repl { .vue-repl {
height: calc(var(--vh) - var(--nav-height)); height: calc(var(--vh) - var(--nav-height)) !important;
} }
button { button {

View File

@ -1,44 +1,35 @@
<script setup lang="ts"> <script setup lang="ts">
import { downloadProject } from './download/download' import { downloadProject } from './download/download'
import { ref, onMounted } from 'vue' import { ref } from 'vue'
import Sun from './icons/Sun.vue' import Sun from './icons/Sun.vue'
import Moon from './icons/Moon.vue' import Moon from './icons/Moon.vue'
import Share from './icons/Share.vue' import Share from './icons/Share.vue'
import Download from './icons/Download.vue' import Download from './icons/Download.vue'
import GitHub from './icons/GitHub.vue' import GitHub from './icons/GitHub.vue'
import type { ReplStore } from '@vue/repl' import type { ReplStore } from '@vue/repl'
import VersionSelect from './VersionSelect.vue'
const props = defineProps<{ const props = defineProps<{
store: ReplStore store: ReplStore
dev: boolean dev: boolean
ssr: boolean ssr: boolean
}>() }>()
const emit = defineEmits(['toggle-theme', 'toggle-ssr', 'toggle-dev'])
const { store } = props const { store } = props
const currentCommit = __COMMIT__ const currentCommit = __COMMIT__
const activeVersion = ref(`@${currentCommit}`) const vueVersion = ref(`@${currentCommit}`)
const publishedVersions = ref<string[]>()
const expanded = ref(false)
async function toggle() {
expanded.value = !expanded.value
if (!publishedVersions.value) {
publishedVersions.value = await fetchVersions()
}
}
async function setVueVersion(v: string) { async function setVueVersion(v: string) {
activeVersion.value = `loading...` vueVersion.value = `loading...`
await store.setVueVersion(v) await store.setVueVersion(v)
activeVersion.value = `v${v}` vueVersion.value = `v${v}`
expanded.value = false
} }
function resetVueVersion() { function resetVueVersion() {
store.resetVueVersion() store.resetVueVersion()
activeVersion.value = `@${currentCommit}` vueVersion.value = `@${currentCommit}`
expanded.value = false
} }
async function copyLink(e: MouseEvent) { async function copyLink(e: MouseEvent) {
@ -58,45 +49,7 @@ function toggleDark() {
'vue-sfc-playground-prefer-dark', 'vue-sfc-playground-prefer-dark',
String(cls.contains('dark')) String(cls.contains('dark'))
) )
} emit('toggle-theme', cls.contains('dark'))
onMounted(async () => {
window.addEventListener('click', () => {
expanded.value = false
})
window.addEventListener('blur', () => {
if (document.activeElement?.tagName === 'IFRAME') {
expanded.value = false
}
})
})
async function fetchVersions(): Promise<string[]> {
const res = await fetch(
`https://api.github.com/repos/vuejs/core/releases?per_page=100`
)
const releases: any[] = await res.json()
const versions = releases.map(r =>
/^v/.test(r.tag_name) ? r.tag_name.slice(1) : r.tag_name
)
// if the latest version is a pre-release, list all current pre-releases
// otherwise filter out pre-releases
let isInPreRelease = versions[0].includes('-')
const filteredVersions: string[] = []
for (const v of versions) {
if (v.includes('-')) {
if (isInPreRelease) {
filteredVersions.push(v)
}
} else {
filteredVersions.push(v)
isInPreRelease = false
}
if (filteredVersions.length >= 30 || v === '3.0.10') {
break
}
}
return filteredVersions
} }
</script> </script>
@ -107,28 +60,28 @@ async function fetchVersions(): Promise<string[]> {
<span>Vue SFC Playground</span> <span>Vue SFC Playground</span>
</h1> </h1>
<div class="links"> <div class="links">
<div class="version" @click.stop> <VersionSelect
<span class="active-version" @click="toggle"> v-model="store.state.typescriptVersion"
Version pkg="typescript"
<span class="number">{{ activeVersion }}</span> label="TypeScript Version"
</span> />
<ul class="versions" :class="{ expanded }"> <VersionSelect
<li v-if="!publishedVersions"><a>loading versions...</a></li> :model-value="vueVersion"
<li v-for="version of publishedVersions"> @update:model-value="setVueVersion"
<a @click="setVueVersion(version)">v{{ version }}</a> pkg="vue"
</li> label="Vue Version"
<li> >
<a @click="resetVueVersion">This Commit ({{ currentCommit }})</a> <li>
</li> <a @click="resetVueVersion">This Commit ({{ currentCommit }})</a>
<li> </li>
<a <li>
href="https://app.netlify.com/sites/vue-sfc-playground/deploys" <a
target="_blank" href="https://app.netlify.com/sites/vue-sfc-playground/deploys"
>Commits History</a target="_blank"
> >Commits History</a
</li> >
</ul> </li>
</div> </VersionSelect>
<button <button
title="Toggle development production mode" title="Toggle development production mode"
class="toggle-dev" class="toggle-dev"
@ -159,14 +112,14 @@ async function fetchVersions(): Promise<string[]> {
> >
<Download /> <Download />
</button> </button>
<button title="View on GitHub" class="github"> <a
<a href="https://github.com/vuejs/core/tree/main/packages/sfc-playground"
href="https://github.com/vuejs/core/tree/main/packages/sfc-playground" target="_blank"
target="_blank" title="View on GitHub"
> class="github"
<GitHub /> >
</a> <GitHub />
</button> </a>
</div> </div>
</nav> </nav>
</template> </template>
@ -233,33 +186,6 @@ h1 img {
display: flex; display: flex;
} }
.version {
margin-right: 12px;
position: relative;
}
.active-version {
cursor: pointer;
position: relative;
display: inline-flex;
place-items: center;
}
.active-version .number {
color: var(--green);
margin-left: 4px;
}
.active-version::after {
content: '';
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 6px solid #aaa;
margin-left: 8px;
}
.toggle-dev span, .toggle-dev span,
.toggle-ssr span { .toggle-ssr span {
font-size: 12px; font-size: 12px;
@ -300,12 +226,13 @@ h1 img {
} }
.links button, .links button,
.links button a { .links .github {
padding: 1px 6px;
color: var(--btn); color: var(--btn);
} }
.links button:hover, .links button:hover,
.links button:hover a { .links .github:hover {
color: var(--highlight); color: var(--highlight);
} }

View File

@ -0,0 +1,114 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
const expanded = ref(false)
const versions = ref<string[]>()
const version = defineModel()
const props = defineProps<{
pkg: string
label: string
}>()
async function toggle() {
expanded.value = !expanded.value
if (!versions.value) {
versions.value = await fetchVersions()
}
}
async function fetchVersions(): Promise<string[]> {
const res = await fetch(
`https://data.jsdelivr.com/v1/package/npm/${props.pkg}`
)
const { versions } = (await res.json()) as { versions: string[] }
if (props.pkg === 'vue') {
// if the latest version is a pre-release, list all current pre-releases
// otherwise filter out pre-releases
let isInPreRelease = versions[0].includes('-')
const filteredVersions: string[] = []
for (const v of versions) {
if (v.includes('-')) {
if (isInPreRelease) {
filteredVersions.push(v)
}
} else {
filteredVersions.push(v)
isInPreRelease = false
}
if (filteredVersions.length >= 30 || v === '3.0.10') {
break
}
}
return filteredVersions
} else if (props.pkg === 'typescript') {
return versions.filter(v => !v.includes('dev') && !v.includes('insiders'))
}
return versions
}
function setVersion(v: string) {
version.value = v
expanded.value = false
}
onMounted(() => {
window.addEventListener('click', () => {
expanded.value = false
})
window.addEventListener('blur', () => {
if (document.activeElement?.tagName === 'IFRAME') {
expanded.value = false
}
})
})
</script>
<template>
<div class="version" @click.stop>
<span class="active-version" @click="toggle">
{{ label }}
<span class="number">{{ version }}</span>
</span>
<ul class="versions" :class="{ expanded }">
<li v-if="!versions"><a>loading versions...</a></li>
<li v-for="version of versions">
<a @click="setVersion(version)">v{{ version }}</a>
</li>
<div @click="expanded = false">
<slot />
</div>
</ul>
</div>
</template>
<style>
.version {
margin-right: 12px;
position: relative;
}
.active-version {
cursor: pointer;
position: relative;
display: inline-flex;
place-items: center;
}
.active-version .number {
color: var(--green);
margin-left: 4px;
}
.active-version::after {
content: '';
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 6px solid #aaa;
margin-left: 8px;
}
</style>

View File

@ -27,7 +27,11 @@ export async function downloadProject(store: ReplStore) {
const files = store.getFiles() const files = store.getFiles()
for (const file in files) { for (const file in files) {
src.file(file, files[file]) if (file !== 'import-map.json') {
src.file(file, files[file])
} else {
zip.file(file, files[file])
}
} }
const blob = await zip.generateAsync({ type: 'blob' }) const blob = await zip.generateAsync({ type: 'blob' })

View File

@ -10,7 +10,7 @@
"vue": "^3.3.0" "vue": "^3.3.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^4.2.3", "@vitejs/plugin-vue": "^4.4.0",
"vite": "^4.3.0" "vite": "^4.5.0"
} }
} }

View File

@ -2,14 +2,15 @@ import fs from 'fs'
import path from 'path' import path from 'path'
import { defineConfig, Plugin } from 'vite' import { defineConfig, Plugin } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import execa from 'execa' import { execaSync } from 'execa'
const commit = execa.sync('git', ['rev-parse', 'HEAD']).stdout.slice(0, 7) const commit = execaSync('git', ['rev-parse', 'HEAD']).stdout.slice(0, 7)
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
vue({ vue({
script: { script: {
defineModel: true,
fs: { fs: {
fileExists: fs.existsSync, fileExists: fs.existsSync,
readFile: file => fs.readFileSync(file, 'utf-8') readFile: file => fs.readFileSync(file, 'utf-8')

View File

@ -50,7 +50,11 @@ export const isObject = (val: unknown): val is Record<any, any> =>
val !== null && typeof val === 'object' val !== null && typeof val === 'object'
export const isPromise = <T = any>(val: unknown): val is Promise<T> => { export const isPromise = <T = any>(val: unknown): val is Promise<T> => {
return isObject(val) && isFunction(val.then) && isFunction(val.catch) return (
(isObject(val) || isFunction(val)) &&
isFunction((val as any).then) &&
isFunction((val as any).catch)
)
} }
export const objectToString = Object.prototype.toString export const objectToString = Object.prototype.toString
@ -110,16 +114,17 @@ export const hyphenate = cacheStringFunction((str: string) =>
/** /**
* @private * @private
*/ */
export const capitalize = cacheStringFunction( export const capitalize = cacheStringFunction(<T extends string>(str: T) => {
(str: string) => str.charAt(0).toUpperCase() + str.slice(1) return (str.charAt(0).toUpperCase() + str.slice(1)) as Capitalize<T>
) })
/** /**
* @private * @private
*/ */
export const toHandlerKey = cacheStringFunction((str: string) => export const toHandlerKey = cacheStringFunction(<T extends string>(str: T) => {
str ? `on${capitalize(str)}` : `` const s = str ? `on${capitalize(str)}` : ``
) return s as T extends '' ? '' : `on${Capitalize<T>}`
})
// compare whether a value has changed, accounting for NaN. // compare whether a value has changed, accounting for NaN.
export const hasChanged = (value: any, oldValue: any): boolean => export const hasChanged = (value: any, oldValue: any): boolean =>
@ -149,7 +154,7 @@ export const looseToNumber = (val: any): any => {
} }
/** /**
* Only conerces number-like strings * Only concerns number-like strings
* "123-foo" will be returned as-is * "123-foo" will be returned as-is
*/ */
export const toNumber = (val: any): any => { export const toNumber = (val: any): any => {

View File

@ -6,3 +6,6 @@ const GLOBALS_ALLOWED =
'Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console' 'Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console'
export const isGloballyAllowed = /*#__PURE__*/ makeMap(GLOBALS_ALLOWED) export const isGloballyAllowed = /*#__PURE__*/ makeMap(GLOBALS_ALLOWED)
/** @deprecated use `isGloballyAllowed` instead */
export const isGloballyWhitelisted = isGloballyAllowed

Some files were not shown because too many files have changed in this diff Show More