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
{
files: [
'scripts/**',
'*.{js,ts}',
'packages/**/index.js',
'packages/size-check/**'
],
files: ['scripts/**', '*.{js,ts}', 'packages/**/index.js'],
rules: {
'no-restricted-globals': 'off',
'no-restricted-syntax': 'off'

View File

@ -1,5 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Feature Request
url: https://github.com/vuejs/rfcs/discussions
about: Suggest new features for consideration
- name: Discord Chat
url: https://chat.vuejs.org
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
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.
@ -185,7 +185,7 @@ Builds and watches `vue/dist/vue-runtime.esm-bundler.js` with all deps inlined u
### `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`
@ -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).
- `size-check`: Used for checking built bundle sizes on CI.
### Importing Packages
The packages can import each other directly using their package names. Note that when importing a package, the name listed in its `package.json` should be used. Most of the time the `@vue/` prefix is needed:
@ -261,7 +259,7 @@ import { h } from '@vue/runtime-core'
This is made possible via several configurations:
- 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).
### 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!
<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
environment: Release
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v2

View File

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

View File

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

View File

@ -8,9 +8,9 @@ export function decodeHtmlBrowser(raw: string, asAttr = false): string {
}
if (asAttr) {
decoder.innerHTML = `<div foo="${raw.replace(/"/g, '&quot;')}">`
return decoder.children[0].getAttribute('foo') as string
return decoder.children[0].getAttribute('foo')!
} else {
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`] = `
"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`] = `
"import { defineComponent as _defineComponent } from 'vue'
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`] = `
"import { defineComponent as _defineComponent } from 'vue'
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`] = `
"import { defineComponent as _defineComponent } from 'vue'
import { x, y, z, x$y } from './x'

View File

@ -68,64 +68,6 @@ describe('SFC compile <script setup>', () => {
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', () => {
test('script first', () => {
const { content } = compile(`
@ -156,6 +98,24 @@ describe('SFC compile <script setup>', () => {
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', () => {
const { content } = compile(`
<script setup>
@ -413,6 +373,25 @@ describe('SFC compile <script setup>', () => {
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
test('attribute expressions', () => {
const { content } = compile(`
@ -513,6 +492,23 @@ describe('SFC compile <script setup>', () => {
</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', () => {

View File

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

View File

@ -10,9 +10,7 @@ export default {
setup(__props, { expose: __expose }) {
__expose();
const props = __props;
const props = __props
return { props, bar }
}
@ -28,9 +26,7 @@ export default /*#__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) {
__expose();
const props = __props;
const props = __props
return { props }
}
@ -48,9 +44,7 @@ export default /*#__PURE__*/_defineComponent({
setup(__props: any, { expose: __expose }) {
__expose();
const { foo } = __props;
const { foo } = __props
return { }
}
@ -167,9 +161,7 @@ export default {
setup(__props, { expose: __expose }) {
__expose();
const props = __props;
const props = __props
return { props, get propsModel() { return propsModel } }
}
@ -203,9 +195,7 @@ export default {
props: {},
setup(__props, { expose: __expose }) {
__expose();
const props = __props;
const props = __props
return { props, get x() { return x } }
}
@ -304,9 +294,7 @@ export default /*#__PURE__*/_defineComponent({
setup(__props: any, { expose: __expose }) {
__expose();
const props = __props;
const props = __props
return { props, get defaults() { return defaults } }
}
@ -328,9 +316,7 @@ export default /*#__PURE__*/_defineComponent({
setup(__props: any, { expose: __expose }) {
__expose();
const props = __props;
const props = __props
return { props, get defaults() { return defaults } }
}
@ -351,9 +337,7 @@ export default /*#__PURE__*/_defineComponent({
setup(__props: any, { expose: __expose }) {
__expose();
const props = __props;
const props = __props
return { props, get defaults() { return defaults } }
}
@ -375,9 +359,7 @@ export default /*#__PURE__*/_defineComponent({
setup(__props: any, { expose: __expose }) {
__expose();
const props = __props;
const props = __props;
return { props }
}
@ -401,9 +383,7 @@ export default /*#__PURE__*/_defineComponent({
setup(__props: any, { expose: __expose }) {
__expose();
const props = __props;
const props = __props
return { props }
}
@ -424,9 +404,7 @@ export default /*#__PURE__*/_defineComponent({
setup(__props: any, { expose: __expose }) {
__expose();
const props = __props;
const props = __props
return { props }
}
@ -446,9 +424,7 @@ export default /*#__PURE__*/_defineComponent({
setup(__props: any, { expose: __expose }) {
__expose();
const props = __props;
const props = __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`] = `
"import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from \\"vue\\"
@ -237,9 +292,7 @@ export default {
props: ['foo', 'bar', 'baz'],
setup(__props) {
const rest = _createPropsRestProxy(__props, [\\"foo\\",\\"bar\\"]);
const rest = _createPropsRestProxy(__props, [\\"foo\\",\\"bar\\"])
return () => {}
}

View File

@ -16,8 +16,9 @@ const myEmit = defineEmits(['foo', 'bar'])
expect(content).not.toMatch('defineEmits')
// should generate correct setup signature
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
expect(content).toMatch(`export default {
emits: ['foo', 'bar'],`)
@ -32,7 +33,8 @@ const emit = defineEmits(['a', 'b'])
assertCode(content)
expect(content).toMatch(`export default /*#__PURE__*/_defineComponent({
emits: ['a', 'b'],
setup(__props, { expose: __expose, emit }) {`)
setup(__props, { expose: __expose, emit: __emit }) {`)
expect(content).toMatch('const emit = __emit')
})
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', () => {
test('should error on deep destructure', () => {
expect(() =>

View File

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

View File

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

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__) {
return new Map<string, T>()
}
const cache = new LRU(size)
// @ts-expect-error
cache.delete = cache.del.bind(cache)
return cache as any as Map<string, T>
return new LRUCache({ max })
}

View File

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

View File

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

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 { generateCodeFrame } from '@vue/shared'
import { parse as babelParse, ParserPlugin } from '@babel/parser'
@ -38,7 +38,8 @@ export class ScriptCompileContext {
hasDefineModelCall = false
// defineProps
propsIdentifier: string | undefined
propsCall: CallExpression | undefined
propsDecl: Node | undefined
propsRuntimeDecl: Node | undefined
propsTypeDecl: Node | undefined
propsDestructureDecl: ObjectPattern | undefined
@ -49,7 +50,7 @@ export class ScriptCompileContext {
// defineEmits
emitsRuntimeDecl: Node | undefined
emitsTypeDecl: Node | undefined
emitIdentifier: string | undefined
emitDecl: Node | undefined
// defineModel
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 { ScriptCompileContext } from './context'
import {
@ -34,10 +41,7 @@ export function processDefineEmits(
ctx.emitsTypeDecl = node.typeParameters.params[0]
}
if (declId) {
ctx.emitIdentifier =
declId.type === 'Identifier' ? declId.name : ctx.getString(declId)
}
ctx.emitDecl = declId
return true
}
@ -99,7 +103,7 @@ function extractRuntimeEmits(ctx: ScriptCompileContext): Set<string> {
function extractEventNames(
ctx: ScriptCompileContext,
eventName: Identifier | RestElement,
eventName: ArrayPattern | Identifier | ObjectPattern | RestElement,
emits: Set<string>
) {
if (

View File

@ -24,13 +24,21 @@ export function processDefineModel(
node: Node,
declId?: LVal
): 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
}
warnOnce(
`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` +
`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]
}
if (declId) {
// handle props destructure
if (declId.type === 'ObjectPattern') {
processPropsDestructure(ctx, declId)
} else {
ctx.propsIdentifier = ctx.getString(declId)
}
// handle props destructure
if (declId && declId.type === 'ObjectPattern') {
processPropsDestructure(ctx, declId)
}
ctx.propsCall = node
ctx.propsDecl = declId
return true
}
@ -97,31 +96,33 @@ function processWithDefaults(
if (!isCallOf(node, WITH_DEFAULTS)) {
return false
}
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 {
if (!processDefineProps(ctx, node.arguments[0], declId)) {
ctx.error(
`${WITH_DEFAULTS}' first argument must be a ${DEFINE_PROPS} call.`,
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
}

View File

@ -28,13 +28,12 @@ export function processPropsDestructure(
declId: ObjectPattern
) {
if (!ctx.options.propsDestructure && !ctx.options.reactivityTransform) {
ctx.propsIdentifier = ctx.getString(declId)
return
}
warnOnce(
`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` +
`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
const ast = ctx.scriptSetupAst!
walkScope(ast, true)
;(walk as any)(ast, {
walk(ast, {
enter(node: Node, parent?: Node) {
parent && parentStack.push(parent)

View File

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

View File

@ -53,8 +53,9 @@ export function parseCssVars(sfc: SFCDescriptor): string[] {
const vars: string[] = []
sfc.styles.forEach(style => {
let match
// ignore v-bind() in comments /* ... */
const content = style.content.replace(/\/\*([\s\S]*?)\*\//g, '')
// ignore v-bind() in comments, eg /* ... */
// 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))) {
const start = match.index + match[0].length
const end = lexBinding(content, start)

View File

@ -130,9 +130,10 @@ function rewriteSelector(
// DEPRECATED usage
// .foo ::v-deep .bar -> .foo[xxxxxxx] .bar
warn(
`::v-deep usage as a combinator has ` +
`been deprecated. Use :deep(<inner-selector>) instead.`
`${value} usage as a combinator has been deprecated. ` +
`Use :deep(<inner-selector>) instead of ${value} <inner-selector>.`
)
const prev = selector.at(selector.index(n) - 1)
if (prev && isSpaceCombinator(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 nodeStylus = load('stylus')
try {
const ref = nodeStylus(source)
Object.keys(options).forEach(key => ref.set(key, options[key]))
const ref = nodeStylus(source, options)
if (map) ref.set('sourcemap', { inline: false, comment: false })
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">', () => {
expect(
compileWithWrapper(`<input type="radio" value="foo" v-model="bar">`).code

View File

@ -18,7 +18,8 @@ import {
import {
SSR_LOOSE_EQUAL,
SSR_LOOSE_CONTAIN,
SSR_RENDER_DYNAMIC_MODEL
SSR_RENDER_DYNAMIC_MODEL,
SSR_INCLUDE_BOOLEAN_ATTR
} from '../runtimeHelpers'
import { DirectiveTransformResult } from 'packages/compiler-core/src/transform'
@ -129,8 +130,34 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
checkDuplicatedValue()
node.children = [createInterpolation(model, model.loc)]
} else if (node.tag === 'select') {
// NOOP
// select relies on client-side directive to set initial selected state.
node.children.forEach(option => {
if (option.type === NodeTypes.ELEMENT) {
const plainNode = option as PlainElementNode
if (plainNode.props.findIndex(p => p.name === 'selected') === -1) {
const value = findValueBinding(plainNode)
plainNode.ssrCodegenNode!.elements.push(
createConditionalExpression(
createCallExpression(context.helper(SSR_INCLUDE_BOOLEAN_ATTR), [
createConditionalExpression(
createCallExpression(`Array.isArray`, [model]),
createCallExpression(context.helper(SSR_LOOSE_CONTAIN), [
model,
value
]),
createCallExpression(context.helper(SSR_LOOSE_EQUAL), [
model,
value
])
)
]),
createSimpleExpression(' selected', true),
createSimpleExpression('', true),
false /* no newline */
)
)
}
}
})
} else {
context.onError(
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.
- 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(
(_props: { msg: string }) => {
return () => {}
},
{
props: {
// @ts-expect-error prop type mismatch
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
}, TString extends string>() => {
const res = withDefaults(
@ -117,10 +118,10 @@ describe('defineProps w/ generic type declaration + withDefaults', <T extends nu
n: 123,
generic1: () => [123, 33] as T[],
generic2: () => ({ x: 123 } as { x: T }),
generic2: () => ({ x: 123 }) as { x: T },
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)
})
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', () => {
// runtime declaration
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
}
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 {
/**
* @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",
"dependencies": {
"@babel/parser": "^7.20.15",
"@babel/parser": "^7.23.0",
"@vue/compiler-core": "3.3.4",
"@vue/shared": "3.3.4",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.0"
"magic-string": "^0.30.5"
},
"devDependencies": {
"@babel/core": "^7.21.3",
"@babel/types": "^7.21.3"
"@babel/core": "^7.23.2",
"@babel/types": "^7.23.0"
}
}

View File

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

View File

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

View File

@ -585,6 +585,14 @@ describe('reactivity/effect', () => {
expect(runner.effect.fn).toBe(otherRunner.effect.fn)
})
it('should wrap if the passed function is a fake effect', () => {
const fakeRunner = () => {}
fakeRunner.effect = {}
const runner = effect(fakeRunner)
expect(fakeRunner).not.toBe(runner)
expect(runner.effect.fn).toBe(fakeRunner)
})
it('should not run multiple times for a single mutation', () => {
let dummy
const obj = reactive<Record<string, number>>({})

View File

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

View File

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

View File

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

View File

@ -27,7 +27,7 @@ function get(
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
if (!isReadonly) {
if (key !== rawKey) {
if (hasChanged(key, rawKey)) {
track(rawTarget, TrackOpTypes.GET, key)
}
track(rawTarget, TrackOpTypes.GET, rawKey)
@ -50,7 +50,7 @@ function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
if (!isReadonly) {
if (key !== rawKey) {
if (hasChanged(key, rawKey)) {
track(rawTarget, TrackOpTypes.HAS, key)
}
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
// raw Sets to reduce memory overhead.
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.
let effectTrackDepth = 0
@ -181,7 +181,7 @@ export function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions
): ReactiveEffectRunner {
if ((fn as ReactiveEffectRunner).effect) {
if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) {
fn = (fn as ReactiveEffectRunner).effect.fn
}

View File

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

View File

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

View File

@ -1000,7 +1000,7 @@ describe('api: watch', () => {
},
mounted() {
// 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.show,
() => void 0
@ -1171,7 +1171,7 @@ describe('api: watch', () => {
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)
let countWE = 0
let countW = 0

View File

@ -475,4 +475,57 @@ describe('renderer: teleport', () => {
expect(dir.mounted).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 Comp = defineComponent(
() => () => toggle.value ? h('div', { ref: fn }) : null
() => () => (toggle.value ? h('div', { ref: fn }) : null)
)
render(h(Comp), root)
expect(fn.mock.calls[0][0]).toBe(root.children[0])

View File

@ -546,4 +546,16 @@ describe('scheduler', () => {
await nextTick()
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 =>
!!(i.type as ComponentOptions).__asyncLoader
/*! #__NO_SIDE_EFFECTS__ */
export function defineAsyncComponent<
T extends Component = { new (): ComponentPublicInstance }
>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {

View File

@ -221,7 +221,7 @@ export function createAppAPI<HostElement>(
set() {
warn(
`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
/*! #__NO_SIDE_EFFECTS__ */
export function defineComponent(
options: unknown,
extraOptions?: ComponentOptions

View File

@ -303,7 +303,13 @@ type PropsWithDefaults<
? T[K]
: NotUndefined<T[K]>
: 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

View File

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

View File

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

View File

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

View File

@ -186,6 +186,13 @@ export const TeleportImpl = {
internals,
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 {
// target changed
@ -393,7 +400,7 @@ function hydrateTeleport(
// Force-casted public typing for h and TSX props inference
export const Teleport = TeleportImpl as unknown as {
__isTeleport: true
new(): {
new (): {
$props: VNodeProps & TeleportProps
$slots: {
default(): VNode[]

View File

@ -30,7 +30,7 @@ interface DevtoolsHook {
appRecords: AppRecord[]
/**
* 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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -32,12 +32,14 @@ export interface TransitionProps extends BaseTransitionProps<Element> {
leaveToClass?: string
}
export const vtcKey = Symbol('_vtc')
export interface ElementWithTransition extends HTMLElement {
// _vtc = Vue Transition Classes.
// Store the temporarily-added transition classes on the element
// so that we can avoid overwriting them if the element's class is patched
// during the transition.
_vtc?: Set<string>
[vtcKey]?: Set<string>
}
// 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) {
cls.split(/\s+/).forEach(c => c && el.classList.add(c))
;(
(el as ElementWithTransition)._vtc ||
((el as ElementWithTransition)._vtc = new Set())
(el as ElementWithTransition)[vtcKey] ||
((el as ElementWithTransition)[vtcKey] = new Set())
).add(cls)
}
export function removeTransitionClass(el: Element, cls: string) {
cls.split(/\s+/).forEach(c => c && el.classList.remove(c))
const { _vtc } = el as ElementWithTransition
const _vtc = (el as ElementWithTransition)[vtcKey]
if (_vtc) {
_vtc.delete(cls)
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
// (i.e. acting as a floor function) causing unexpected behaviors
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
}

View File

@ -6,7 +6,8 @@ import {
getTransitionInfo,
resolveTransitionProps,
TransitionPropsValidators,
forceReflow
forceReflow,
vtcKey
} from './Transition'
import {
Fragment,
@ -29,7 +30,8 @@ import { extend } from '@vue/shared'
const positionMap = new WeakMap<VNode, DOMRect>()
const newPositionMap = new WeakMap<VNode, DOMRect>()
const moveCbKey = Symbol('_moveCb')
const enterCbKey = Symbol('_enterCb')
export type TransitionGroupProps = Omit<TransitionProps, 'mode'> & {
tag?: string
moveClass?: string
@ -80,13 +82,13 @@ const TransitionGroupImpl: ComponentOptions = {
const style = el.style
addTransitionClass(el, moveClass)
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) {
return
}
if (!e || /transform$/.test(e.propertyName)) {
el.removeEventListener('transitionend', cb)
;(el as any)._moveCb = null
;(el as any)[moveCbKey] = null
removeTransitionClass(el, moveClass)
}
})
@ -162,11 +164,11 @@ export const TransitionGroup = TransitionGroupImpl as unknown as {
function callPendingCbs(c: VNode) {
const el = c.el as any
if (el._moveCb) {
el._moveCb()
if (el[moveCbKey]) {
el[moveCbKey]()
}
if (el._enterCb) {
el._enterCb()
if (el[enterCbKey]) {
el[enterCbKey]()
}
}
@ -198,8 +200,9 @@ function hasCSSTransform(
// all other transition classes applied to ensure only the move class
// is applied.
const clone = el.cloneNode() as HTMLElement
if (el._vtc) {
el._vtc.forEach(cls => {
const _vtc = el[vtcKey]
if (_vtc) {
_vtc.forEach(cls => {
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
// be tree-shaken in case v-model is never used.
@ -44,7 +46,7 @@ export const vModelText: ModelDirective<
HTMLInputElement | HTMLTextAreaElement
> = {
created(el, { modifiers: { lazy, trim, number } }, vnode) {
el._assign = getModelAssigner(vnode)
el[assignKey] = getModelAssigner(vnode)
const castToNumber =
number || (vnode.props && vnode.props.type === 'number')
addEventListener(el, lazy ? 'change' : 'input', e => {
@ -56,7 +58,7 @@ export const vModelText: ModelDirective<
if (castToNumber) {
domValue = looseToNumber(domValue)
}
el._assign(domValue)
el[assignKey](domValue)
})
if (trim) {
addEventListener(el, 'change', () => {
@ -78,7 +80,7 @@ export const vModelText: ModelDirective<
el.value = value == null ? '' : value
},
beforeUpdate(el, { value, modifiers: { lazy, trim, number } }, vnode) {
el._assign = getModelAssigner(vnode)
el[assignKey] = getModelAssigner(vnode)
// avoid clearing unresolved text. #2302
if ((el as any).composing) return
if (document.activeElement === el && el.type !== 'range') {
@ -106,12 +108,12 @@ export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
// #4096 array checkboxes need to be deep traversed
deep: true,
created(el, _, vnode) {
el._assign = getModelAssigner(vnode)
el[assignKey] = getModelAssigner(vnode)
addEventListener(el, 'change', () => {
const modelValue = (el as any)._modelValue
const elementValue = getValue(el)
const checked = el.checked
const assign = el._assign
const assign = el[assignKey]
if (isArray(modelValue)) {
const index = looseIndexOf(modelValue, elementValue)
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
mounted: setChecked,
beforeUpdate(el, binding, vnode) {
el._assign = getModelAssigner(vnode)
el[assignKey] = getModelAssigner(vnode)
setChecked(el, binding, vnode)
}
}
@ -163,13 +165,13 @@ function setChecked(
export const vModelRadio: ModelDirective<HTMLInputElement> = {
created(el, { value }, vnode) {
el.checked = looseEqual(value, vnode.props!.value)
el._assign = getModelAssigner(vnode)
el[assignKey] = getModelAssigner(vnode)
addEventListener(el, 'change', () => {
el._assign(getValue(el))
el[assignKey](getValue(el))
})
},
beforeUpdate(el, { value, oldValue }, vnode) {
el._assign = getModelAssigner(vnode)
el[assignKey] = getModelAssigner(vnode)
if (value !== oldValue) {
el.checked = looseEqual(value, vnode.props!.value)
}
@ -187,7 +189,7 @@ export const vModelSelect: ModelDirective<HTMLSelectElement> = {
.map((o: HTMLOptionElement) =>
number ? looseToNumber(getValue(o)) : getValue(o)
)
el._assign(
el[assignKey](
el.multiple
? isSetModel
? new Set(selectedVal)
@ -195,7 +197,7 @@ export const vModelSelect: ModelDirective<HTMLSelectElement> = {
: selectedVal[0]
)
})
el._assign = getModelAssigner(vnode)
el[assignKey] = getModelAssigner(vnode)
},
// set value in mounted & updated because <select> relies on its children
// <option>s.
@ -203,7 +205,7 @@ export const vModelSelect: ModelDirective<HTMLSelectElement> = {
setSelected(el, value)
},
beforeUpdate(el, _binding, vnode) {
el._assign = getModelAssigner(vnode)
el[assignKey] = getModelAssigner(vnode)
},
updated(el, { value }) {
setSelected(el, value)

View File

@ -1,13 +1,15 @@
import { ObjectDirective } from '@vue/runtime-core'
export const vShowOldKey = Symbol('_vod')
interface VShowElement extends HTMLElement {
// _vod = vue original display
_vod: string
[vShowOldKey]: string
}
export const vShow: ObjectDirective<VShowElement> = {
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) {
transition.beforeEnter(el)
} else {
@ -41,7 +43,7 @@ export const vShow: ObjectDirective<VShowElement> = {
}
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

View File

@ -248,8 +248,9 @@ export interface HTMLAttributes extends AriaAttributes, EventHandlers<Events> {
contextmenu?: string
dir?: string
draggable?: Booleanish
hidden?: Booleanish
hidden?: Booleanish | '' | 'hidden' | 'until-found'
id?: string
inert?: Booleanish
lang?: string
placeholder?: string
spellcheck?: Booleanish
@ -457,6 +458,7 @@ export interface ImgHTMLAttributes extends HTMLAttributes {
srcset?: string
usemap?: string
width?: Numberish
loading?: 'lazy' | 'eager'
}
export interface InsHTMLAttributes extends HTMLAttributes {
@ -464,6 +466,31 @@ export interface InsHTMLAttributes extends HTMLAttributes {
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 {
accept?: string
alt?: string
@ -495,7 +522,7 @@ export interface InputHTMLAttributes extends HTMLAttributes {
size?: Numberish
src?: string
step?: Numberish
type?: string
type?: InputTypeHTMLAttribute
value?: any // we support :value to be bound to anything w/ v-model
width?: Numberish
}
@ -677,7 +704,7 @@ export interface TextareaHTMLAttributes extends HTMLAttributes {
minlength?: Numberish
name?: string
placeholder?: string
readonly?: boolean
readonly?: Booleanish
required?: Booleanish
rows?: Numberish
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
*/
class?: any
style?: string | CSSProperties
style?: StyleValue
color?: string
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
// 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
// if this is an element during a transition, take the temporary transition
// classes into account.
const transitionClasses = (el as ElementWithTransition)._vtc
const transitionClasses = (el as ElementWithTransition)[vtcKey]
if (transitionClasses) {
value = (
value ? [value, ...transitionClasses] : [...transitionClasses]

View File

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

View File

@ -1,5 +1,6 @@
import { isString, hyphenate, capitalize, isArray } from '@vue/shared'
import { camelize, warn } from '@vue/runtime-core'
import { vShowOldKey } from '../directives/vShow'
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`,
// so we always keep the current `display` value regardless of the `style`
// value, thus handing over control to `v-show`.
if ('_vod' in el) {
if (vShowOldKey in el) {
style.display = currentDisplay
}
}

View File

@ -107,6 +107,30 @@ describe('ssr: directives', () => {
).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 () => {
expect(
await renderToString(

View File

@ -156,7 +156,7 @@ describe('ssr: scopedId runtime behavior', () => {
})
// #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 = {
ssrRender: (ctx: any, push: any, parent: any, attrs: any) => {
push(`<div${ssrRenderAttrs(attrs)}></div>`)

View File

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

View File

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

View File

@ -1,7 +1,8 @@
<script setup lang="ts">
import Header from './Header.vue'
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 = () => {
document.documentElement.style.setProperty('--vh', window.innerHeight + `px`)
@ -71,6 +72,15 @@ function toggleSSR() {
useSSRMode.value = !useSSRMode.value
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>
<template>
@ -78,10 +88,13 @@ function toggleSSR() {
:store="store"
:dev="useDevMode"
:ssr="useSSRMode"
@toggle-theme="toggleTheme"
@toggle-dev="toggleDevMode"
@toggle-ssr="toggleSSR"
/>
<Repl
:theme="theme"
:editor="Monaco"
@keydown.ctrl.s.prevent
@keydown.meta.s.prevent
:ssr="useSSRMode"
@ -108,7 +121,7 @@ body {
}
.vue-repl {
height: calc(var(--vh) - var(--nav-height));
height: calc(var(--vh) - var(--nav-height)) !important;
}
button {

View File

@ -1,44 +1,35 @@
<script setup lang="ts">
import { downloadProject } from './download/download'
import { ref, onMounted } from 'vue'
import { ref } from 'vue'
import Sun from './icons/Sun.vue'
import Moon from './icons/Moon.vue'
import Share from './icons/Share.vue'
import Download from './icons/Download.vue'
import GitHub from './icons/GitHub.vue'
import type { ReplStore } from '@vue/repl'
import VersionSelect from './VersionSelect.vue'
const props = defineProps<{
store: ReplStore
dev: boolean
ssr: boolean
}>()
const emit = defineEmits(['toggle-theme', 'toggle-ssr', 'toggle-dev'])
const { store } = props
const currentCommit = __COMMIT__
const activeVersion = ref(`@${currentCommit}`)
const publishedVersions = ref<string[]>()
const expanded = ref(false)
async function toggle() {
expanded.value = !expanded.value
if (!publishedVersions.value) {
publishedVersions.value = await fetchVersions()
}
}
const vueVersion = ref(`@${currentCommit}`)
async function setVueVersion(v: string) {
activeVersion.value = `loading...`
vueVersion.value = `loading...`
await store.setVueVersion(v)
activeVersion.value = `v${v}`
expanded.value = false
vueVersion.value = `v${v}`
}
function resetVueVersion() {
store.resetVueVersion()
activeVersion.value = `@${currentCommit}`
expanded.value = false
vueVersion.value = `@${currentCommit}`
}
async function copyLink(e: MouseEvent) {
@ -58,45 +49,7 @@ function toggleDark() {
'vue-sfc-playground-prefer-dark',
String(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
emit('toggle-theme', cls.contains('dark'))
}
</script>
@ -107,28 +60,28 @@ async function fetchVersions(): Promise<string[]> {
<span>Vue SFC Playground</span>
</h1>
<div class="links">
<div class="version" @click.stop>
<span class="active-version" @click="toggle">
Version
<span class="number">{{ activeVersion }}</span>
</span>
<ul class="versions" :class="{ expanded }">
<li v-if="!publishedVersions"><a>loading versions...</a></li>
<li v-for="version of publishedVersions">
<a @click="setVueVersion(version)">v{{ version }}</a>
</li>
<li>
<a @click="resetVueVersion">This Commit ({{ currentCommit }})</a>
</li>
<li>
<a
href="https://app.netlify.com/sites/vue-sfc-playground/deploys"
target="_blank"
>Commits History</a
>
</li>
</ul>
</div>
<VersionSelect
v-model="store.state.typescriptVersion"
pkg="typescript"
label="TypeScript Version"
/>
<VersionSelect
:model-value="vueVersion"
@update:model-value="setVueVersion"
pkg="vue"
label="Vue Version"
>
<li>
<a @click="resetVueVersion">This Commit ({{ currentCommit }})</a>
</li>
<li>
<a
href="https://app.netlify.com/sites/vue-sfc-playground/deploys"
target="_blank"
>Commits History</a
>
</li>
</VersionSelect>
<button
title="Toggle development production mode"
class="toggle-dev"
@ -159,14 +112,14 @@ async function fetchVersions(): Promise<string[]> {
>
<Download />
</button>
<button title="View on GitHub" class="github">
<a
href="https://github.com/vuejs/core/tree/main/packages/sfc-playground"
target="_blank"
>
<GitHub />
</a>
</button>
<a
href="https://github.com/vuejs/core/tree/main/packages/sfc-playground"
target="_blank"
title="View on GitHub"
class="github"
>
<GitHub />
</a>
</div>
</nav>
</template>
@ -233,33 +186,6 @@ h1 img {
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-ssr span {
font-size: 12px;
@ -300,12 +226,13 @@ h1 img {
}
.links button,
.links button a {
.links .github {
padding: 1px 6px;
color: var(--btn);
}
.links button:hover,
.links button:hover a {
.links .github:hover {
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()
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' })

View File

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

View File

@ -2,14 +2,15 @@ import fs from 'fs'
import path from 'path'
import { defineConfig, Plugin } from 'vite'
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({
plugins: [
vue({
script: {
defineModel: true,
fs: {
fileExists: fs.existsSync,
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'
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
@ -110,16 +114,17 @@ export const hyphenate = cacheStringFunction((str: string) =>
/**
* @private
*/
export const capitalize = cacheStringFunction(
(str: string) => str.charAt(0).toUpperCase() + str.slice(1)
)
export const capitalize = cacheStringFunction(<T extends string>(str: T) => {
return (str.charAt(0).toUpperCase() + str.slice(1)) as Capitalize<T>
})
/**
* @private
*/
export const toHandlerKey = cacheStringFunction((str: string) =>
str ? `on${capitalize(str)}` : ``
)
export const toHandlerKey = cacheStringFunction(<T extends string>(str: T) => {
const s = str ? `on${capitalize(str)}` : ``
return s as T extends '' ? '' : `on${Capitalize<T>}`
})
// compare whether a value has changed, accounting for NaN.
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
*/
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'
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