Merge branch 'main' into fix/10799

This commit is contained in:
edison 2024-10-08 10:43:33 +08:00 committed by GitHub
commit 8ad2053b5b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
225 changed files with 6430 additions and 2588 deletions

View File

@ -58,5 +58,9 @@
// pinned // pinned
// only used in example for e2e tests // only used in example for e2e tests
'marked', 'marked',
// pinned, 5.0+ has exports issues
// https://github.com/vuejs/core/issues/11603
'entities',
], ],
} }

View File

@ -3,6 +3,8 @@ on:
push: push:
branches: branches:
- '**' - '**'
tags:
- '!**'
pull_request: pull_request:
branches: branches:
- main - main
@ -12,3 +14,29 @@ jobs:
test: test:
if: ${{ ! startsWith(github.event.head_commit.message, 'release:') && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository) }} if: ${{ ! startsWith(github.event.head_commit.message, 'release:') && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository) }}
uses: ./.github/workflows/test.yml uses: ./.github/workflows/test.yml
continuous-release:
if: github.repository == 'vuejs/core'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.node-version'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
- name: Install deps
run: pnpm install
- name: Build
run: pnpm build --withTypes
- name: Release
run: pnpx pkg-pr-new publish --compact --pnpm './packages/*'

View File

@ -0,0 +1,21 @@
name: Auto close issues with "can't reproduce" label
on:
schedule:
- cron: '0 0 * * *'
permissions:
issues: write
jobs:
close-issues:
if: github.repository == 'vuejs/core'
runs-on: ubuntu-latest
steps:
- name: can't reproduce
uses: actions-cool/issues-helper@v3
with:
actions: 'close-issues'
token: ${{ secrets.GITHUB_TOKEN }}
labels: "can't reproduce"
inactive-day: 3

View File

@ -9,7 +9,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'vuejs/core' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run') if: github.repository == 'vuejs/core' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run')
steps: steps:
- uses: actions/github-script@v7 - name: Check user permission
uses: actions/github-script@v7
with: with:
script: | script: |
const user = context.payload.sender.login const user = context.payload.sender.login
@ -43,7 +44,8 @@ jobs:
}) })
throw new Error('not allowed') throw new Error('not allowed')
} }
- uses: actions/github-script@v7 - name: Get PR info
uses: actions/github-script@v7
id: get-pr-data id: get-pr-data
with: with:
script: | script: |
@ -56,9 +58,11 @@ jobs:
return { return {
num: context.issue.number, num: context.issue.number,
branchName: pr.head.ref, branchName: pr.head.ref,
repo: pr.head.repo.full_name repo: pr.head.repo.full_name,
commit: pr.head.sha
} }
- uses: actions/github-script@v7 - name: Trigger run
uses: actions/github-script@v7
id: trigger id: trigger
env: env:
COMMENT: ${{ github.event.comment.body }} COMMENT: ${{ github.event.comment.body }}
@ -80,6 +84,7 @@ jobs:
prNumber: '' + prData.num, prNumber: '' + prData.num,
branchName: prData.branchName, branchName: prData.branchName,
repo: prData.repo, repo: prData.repo,
suite: suite === '' ? '-' : suite suite: suite === '' ? '-' : suite,
commit: prData.commit
} }
}) })

View File

@ -18,6 +18,7 @@ env:
jobs: jobs:
upload: upload:
if: github.repository == 'vuejs/core'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

View File

@ -18,6 +18,7 @@ jobs:
size-report: size-report:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > if: >
github.repository == 'vuejs/core' &&
github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success' github.event.workflow_run.conclusion == 'success'
steps: steps:
@ -65,7 +66,7 @@ jobs:
if_no_artifact_found: warn if_no_artifact_found: warn
- name: Prepare report - name: Prepare report
run: pnpm tsx scripts/size-report.ts > size-report.md run: node scripts/size-report.js > size-report.md
- name: Read Size Report - name: Read Size Report
id: size-report id: size-report

View File

@ -1,42 +0,0 @@
# upload built packages as artifacts for faster ecosystem-ci
name: upload-built-packages
on:
workflow_run:
workflows: ['ci']
branches: [main, minor]
types:
- completed
jobs:
build-and-upload:
if: >
github.repository == 'vuejs/core' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.node-version'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
- name: Install deps
run: pnpm install
- name: Build
run: pnpm build --withTypes
- name: Upload
uses: actions/upload-artifact@v4
with:
name: packages
path: packages/*/dist/*

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["vitest.explorer"]
}

View File

@ -1,3 +1,318 @@
## [3.5.11](https://github.com/vuejs/core/compare/v3.5.10...v3.5.11) (2024-10-03)
### Bug Fixes
* **compiler-sfc:** do not skip `TSSatisfiesExpression` when transforming props destructure ([#12062](https://github.com/vuejs/core/issues/12062)) ([2328b05](https://github.com/vuejs/core/commit/2328b051f4efa1f1394b7d4e73b7c3f76e430e7c)), closes [#12061](https://github.com/vuejs/core/issues/12061)
* **reactivity:** prevent overwriting `next` property during batch processing ([#12075](https://github.com/vuejs/core/issues/12075)) ([d3f5e6e](https://github.com/vuejs/core/commit/d3f5e6e5319b4ffaa55ca9a2ea3d95d78e76fa58)), closes [#12072](https://github.com/vuejs/core/issues/12072)
* **scheduler:** job ordering when the post queue is flushing ([#12090](https://github.com/vuejs/core/issues/12090)) ([577edca](https://github.com/vuejs/core/commit/577edca8e7795436efd710d1c289ea8ea2642b0e))
* **types:** correctly infer `TypeProps` when it is `any` ([#12073](https://github.com/vuejs/core/issues/12073)) ([57315ab](https://github.com/vuejs/core/commit/57315ab9688c9741a271d1075bbd28cbe5f71e2f)), closes [#12058](https://github.com/vuejs/core/issues/12058)
* **types:** should not intersect `PublicProps` with `Props` ([#12077](https://github.com/vuejs/core/issues/12077)) ([6f85894](https://github.com/vuejs/core/commit/6f8589437635706f825ccec51800effba1d2bf5f))
* **types:** infer the first generic type of `Ref` correctly ([#12094](https://github.com/vuejs/core/issues/12094)) ([c97bb84](https://github.com/vuejs/core/commit/c97bb84d0b0a16b012f886b6498e924415ed63e5))
## [3.5.10](https://github.com/vuejs/core/compare/v3.5.9...v3.5.10) (2024-09-27)
### Bug Fixes
* **custom-element:** properly set kebab-case props on Vue custom elements ([ea3efa0](https://github.com/vuejs/core/commit/ea3efa09e008918c1d9ba7226833a8b1a7a57244)), closes [#12030](https://github.com/vuejs/core/issues/12030) [#12032](https://github.com/vuejs/core/issues/12032)
* **reactivity:** fix nested batch edge case ([93c95dd](https://github.com/vuejs/core/commit/93c95dd4cd416503f43a98a1455f62658d22b0b2))
* **reactivity:** only clear notified flags for computed in first batch iteration ([aa9ef23](https://github.com/vuejs/core/commit/aa9ef2386a0cd39a174e5a887ec2b1a3525034fc)), closes [#12045](https://github.com/vuejs/core/issues/12045)
* **types/ref:** handle nested refs in UnwrapRef ([#12049](https://github.com/vuejs/core/issues/12049)) ([e2c19c2](https://github.com/vuejs/core/commit/e2c19c20cfee9788519a80c0e53e216b78505994)), closes [#12044](https://github.com/vuejs/core/issues/12044)
## [3.5.9](https://github.com/vuejs/core/compare/v3.5.8...v3.5.9) (2024-09-26)
### Bug Fixes
* **reactivity:** fix property dep removal regression ([6001e5c](https://github.com/vuejs/core/commit/6001e5c81a05c894586f9287fbd991677bdd0455)), closes [#12020](https://github.com/vuejs/core/issues/12020) [#12021](https://github.com/vuejs/core/issues/12021)
* **reactivity:** fix recursive sync watcher on computed edge case ([10ff159](https://github.com/vuejs/core/commit/10ff15924053d9bd95ad706f78ce09e288213fcf)), closes [#12033](https://github.com/vuejs/core/issues/12033) [#12037](https://github.com/vuejs/core/issues/12037)
* **runtime-core:** avoid rendering plain object as VNode ([#12038](https://github.com/vuejs/core/issues/12038)) ([cb34b28](https://github.com/vuejs/core/commit/cb34b28a4a9bf868be4785b001c526163eda342e)), closes [#12035](https://github.com/vuejs/core/issues/12035) [vitejs/vite-plugin-vue#353](https://github.com/vitejs/vite-plugin-vue/issues/353)
* **runtime-core:** make useId() always return a string ([a177092](https://github.com/vuejs/core/commit/a177092754642af2f98c33a4feffe8f198c3c950))
* **types:** correct type inference of union event names ([#12022](https://github.com/vuejs/core/issues/12022)) ([4da6881](https://github.com/vuejs/core/commit/4da688141d9e7c15b622c289deaa81b11845b2c7))
* **vue:** properly cache runtime compilation ([#12019](https://github.com/vuejs/core/issues/12019)) ([fa0ba24](https://github.com/vuejs/core/commit/fa0ba24b3ace02d7ecab65e57c2bea89a2550dcb))
## [3.5.8](https://github.com/vuejs/core/compare/v3.5.7...v3.5.8) (2024-09-22)
### Bug Fixes
* **reactivity:** do not remove dep from depsMap when cleaning up deps of computed ([#11995](https://github.com/vuejs/core/issues/11995)) ([0267a58](https://github.com/vuejs/core/commit/0267a588017eee4951ac2a877fe1ccae84cad905))
## [3.5.7](https://github.com/vuejs/core/compare/v3.5.6...v3.5.7) (2024-09-20)
### Bug Fixes
* **compile-core:** fix v-model with newlines edge case ([#11960](https://github.com/vuejs/core/issues/11960)) ([6224288](https://github.com/vuejs/core/commit/62242886d705ece88dbcad45bb78072ecccad0ca)), closes [#8306](https://github.com/vuejs/core/issues/8306)
* **compiler-sfc:** initialize scope with null prototype object ([#11963](https://github.com/vuejs/core/issues/11963)) ([215e154](https://github.com/vuejs/core/commit/215e15407294bf667261360218f975b88c99c2e5))
* **hydration:** avoid observing non-Element node ([#11954](https://github.com/vuejs/core/issues/11954)) ([7257e6a](https://github.com/vuejs/core/commit/7257e6a34200409b3fc347d3bb807e11e2785974)), closes [#11952](https://github.com/vuejs/core/issues/11952)
* **reactivity:** do not remove dep from depsMap when unsubbed by computed ([960706e](https://github.com/vuejs/core/commit/960706eebf73f08ebc9d5dd853a05def05e2c153))
* **reactivity:** fix dev-only memory leak by updating dep.subsHead on sub removal ([5c8b76e](https://github.com/vuejs/core/commit/5c8b76ed6cfbbcee4cbaac0b72beab7291044e4f)), closes [#11956](https://github.com/vuejs/core/issues/11956)
* **reactivity:** fix memory leak from dep instances of garbage collected objects ([235ea47](https://github.com/vuejs/core/commit/235ea4772ed2972914cf142da8b7ac1fb04f7585)), closes [#11979](https://github.com/vuejs/core/issues/11979) [#11971](https://github.com/vuejs/core/issues/11971)
* **reactivity:** fix triggerRef call on ObjectRefImpl returned by toRef ([#11986](https://github.com/vuejs/core/issues/11986)) ([b030c8b](https://github.com/vuejs/core/commit/b030c8bc7327877efb98aa3d9a58eb287a6ff07a)), closes [#11982](https://github.com/vuejs/core/issues/11982)
* **scheduler:** ensure recursive jobs can't be queued twice ([#11955](https://github.com/vuejs/core/issues/11955)) ([d18d6aa](https://github.com/vuejs/core/commit/d18d6aa1b20dc57a8103c51ec4d61e8e53ed936d))
* **ssr:** don't render comments in TransitionGroup ([#11961](https://github.com/vuejs/core/issues/11961)) ([a2f6ede](https://github.com/vuejs/core/commit/a2f6edeb02faedbb673c4bc5c6a59d9a79a37d07)), closes [#11958](https://github.com/vuejs/core/issues/11958)
* **transition:** respect `duration` setting even when it is `0` ([#11967](https://github.com/vuejs/core/issues/11967)) ([f927a4a](https://github.com/vuejs/core/commit/f927a4ae6f7c453f70ba89498ee0c737dc9866fd))
* **types:** correct type inference of all-optional props ([#11644](https://github.com/vuejs/core/issues/11644)) ([9eca65e](https://github.com/vuejs/core/commit/9eca65ee9871d1ac878755afa9a3eb1b02030350)), closes [#11733](https://github.com/vuejs/core/issues/11733) [vuejs/language-tools#4704](https://github.com/vuejs/language-tools/issues/4704)
### Performance Improvements
* **hydration:** avoid observer if element is in viewport ([#11639](https://github.com/vuejs/core/issues/11639)) ([e075dfa](https://github.com/vuejs/core/commit/e075dfad5c7649c6045e3711687ec888e7aa1a39))
## [3.5.6](https://github.com/vuejs/core/compare/v3.5.5...v3.5.6) (2024-09-16)
### Bug Fixes
* **compile-dom:** should be able to stringify mathML ([#11891](https://github.com/vuejs/core/issues/11891)) ([85c138c](https://github.com/vuejs/core/commit/85c138ced108268f7656b568dfd3036a1e0aae34))
* **compiler-sfc:** preserve old behavior when using withDefaults with desutructure ([8492c3c](https://github.com/vuejs/core/commit/8492c3c49a922363d6c77ef192c133a8fbce6514)), closes [#11930](https://github.com/vuejs/core/issues/11930)
* **reactivity:** avoid exponential perf cost and reduce call stack depth for deeply chained computeds ([#11944](https://github.com/vuejs/core/issues/11944)) ([c74bb8c](https://github.com/vuejs/core/commit/c74bb8c2dd9e82aaabb0a2a2b368e900929b513b)), closes [#11928](https://github.com/vuejs/core/issues/11928)
* **reactivity:** rely on dirty check only when computed has deps ([#11931](https://github.com/vuejs/core/issues/11931)) ([aa5dafd](https://github.com/vuejs/core/commit/aa5dafd2b55d42d6a29316a3bc91aea85c676a0b)), closes [#11929](https://github.com/vuejs/core/issues/11929)
* **watch:** `once` option should be ignored by watchEffect ([#11884](https://github.com/vuejs/core/issues/11884)) ([49fa673](https://github.com/vuejs/core/commit/49fa673493d93b77ddba2165ab6545bae84fd1ae))
* **watch:** unwatch should be callable during SSR ([#11925](https://github.com/vuejs/core/issues/11925)) ([2d6adf7](https://github.com/vuejs/core/commit/2d6adf78a047eed091db277ffbd9df0822fb0bdd)), closes [#11924](https://github.com/vuejs/core/issues/11924)
## [3.5.5](https://github.com/vuejs/core/compare/v3.5.4...v3.5.5) (2024-09-13)
### Bug Fixes
* **compiler-core:** fix handling of delimiterOpen in VPre ([#11915](https://github.com/vuejs/core/issues/11915)) ([706d4ac](https://github.com/vuejs/core/commit/706d4ac1d0210b2d9134b3228280187fe02fc971)), closes [#11913](https://github.com/vuejs/core/issues/11913)
* **compiler-dom:** fix stringify static edge for partially eligible chunks in cached parent ([1d99d61](https://github.com/vuejs/core/commit/1d99d61c1bd77f9ea6743f6214a82add8346a121)), closes [#11879](https://github.com/vuejs/core/issues/11879) [#11890](https://github.com/vuejs/core/issues/11890)
* **compiler-dom:** should ignore leading newline in <textarea> per spec ([3c4bf76](https://github.com/vuejs/core/commit/3c4bf7627649ec1e3220f8c4e4163c20d2afb367))
* **compiler-sfc:** nested css supports atrule and comment ([#11899](https://github.com/vuejs/core/issues/11899)) ([0e7bc71](https://github.com/vuejs/core/commit/0e7bc717e6640644f062957ec5031506f0dab215)), closes [#11896](https://github.com/vuejs/core/issues/11896)
* **custom-element:** handle nested customElement mount w/ shadowRoot false ([#11861](https://github.com/vuejs/core/issues/11861)) ([f2d8019](https://github.com/vuejs/core/commit/f2d801918841e7673ff3f048d0d895592a2f7e23)), closes [#11851](https://github.com/vuejs/core/issues/11851) [#11871](https://github.com/vuejs/core/issues/11871)
* **hmr:** reload async child wrapped in Suspense + KeepAlive ([#11907](https://github.com/vuejs/core/issues/11907)) ([10a2c60](https://github.com/vuejs/core/commit/10a2c6053bd30d160d0214bb3566f540187e6874)), closes [#11868](https://github.com/vuejs/core/issues/11868)
* **hydration:** fix mismatch of leading newline in `<textarea>` and `<pre>` ([a5f3c2e](https://github.com/vuejs/core/commit/a5f3c2eb4d2e7fae93ff93ce865b269f01cc825e)), closes [#11873](https://github.com/vuejs/core/issues/11873) [#11874](https://github.com/vuejs/core/issues/11874)
* **reactivity:** properly clean up deps, fix memory leak ([8ea5d6d](https://github.com/vuejs/core/commit/8ea5d6d6981ab7febda0be43c3c92b18869c3a2a)), closes [#11901](https://github.com/vuejs/core/issues/11901)
* **runtime-core:** properly update async component nested in KeepAlive ([#11917](https://github.com/vuejs/core/issues/11917)) ([7fe6c79](https://github.com/vuejs/core/commit/7fe6c795a1fc7ddcea5ad91a56141561192373ac)), closes [#11916](https://github.com/vuejs/core/issues/11916)
* **TransitionGroup:** not warn unkeyed text children with whitespece preserve ([#11888](https://github.com/vuejs/core/issues/11888)) ([7571f20](https://github.com/vuejs/core/commit/7571f20bc3d1854377a146f41d211e05bb68cd47)), closes [#11885](https://github.com/vuejs/core/issues/11885)
## [3.5.4](https://github.com/vuejs/core/compare/v3.5.3...v3.5.4) (2024-09-10)
### Bug Fixes
* **compiler-sfc:** correct scoped injection for nesting selector ([#11854](https://github.com/vuejs/core/issues/11854)) ([b1de75e](https://github.com/vuejs/core/commit/b1de75ed04626b6423085dfde91fb0cb481a25e8)), closes [#10567](https://github.com/vuejs/core/issues/10567)
* **reactivity:** fix markRaw error on already marked object ([#11864](https://github.com/vuejs/core/issues/11864)) ([67d6596](https://github.com/vuejs/core/commit/67d6596d40b1807b9cd8eb0d9282932ea77be3c0)), closes [#11862](https://github.com/vuejs/core/issues/11862)
* Revert "fix: Revert "fix(reactivity): self-referencing computed should refresh"" ([e596378](https://github.com/vuejs/core/commit/e596378e0be728dad7d60938449f3fa557ca2ec9))
* **runtime-core:** handle shallow reactive arrays in renderList correctly ([#11870](https://github.com/vuejs/core/issues/11870)) ([ced59ab](https://github.com/vuejs/core/commit/ced59ab8f2f2e89c13119bab3a0c25a1a1f1c3d6)), closes [#11869](https://github.com/vuejs/core/issues/11869)
* **types:** correctly infer `TypeEmits` with both tuple and function syntax ([#11840](https://github.com/vuejs/core/issues/11840)) ([dad6738](https://github.com/vuejs/core/commit/dad673809929c084dcb8e42640eb7daa675d4ea4)), closes [#11836](https://github.com/vuejs/core/issues/11836)
### Performance Improvements
* **reactivity:** trigger deps directly instead of storing in an array first ([#11695](https://github.com/vuejs/core/issues/11695)) ([f80d447](https://github.com/vuejs/core/commit/f80d447c17662556e9e3f99f6d199967f4c8cf3d))
## [3.5.3](https://github.com/vuejs/core/compare/v3.5.2...v3.5.3) (2024-09-06)
### Bug Fixes
* **hydration:** check __asyncHydrate presence for vue3-lazy-hydration compat ([#11825](https://github.com/vuejs/core/issues/11825)) ([8e6c337](https://github.com/vuejs/core/commit/8e6c3378676be05cea7f53664442acdfb86784f9)), closes [#11793](https://github.com/vuejs/core/issues/11793)
* Revert "fix(reactivity): self-referencing computed should refresh" ([35c760f](https://github.com/vuejs/core/commit/35c760f82f749f7c6e3f9bfead8221ce498e892f))
* **ssr:** respect app.config.warnHandler during ssr ([bf3d9a2](https://github.com/vuejs/core/commit/bf3d9a2af41659a743706306fc798b3d215df5af)), closes [#11830](https://github.com/vuejs/core/issues/11830)
* **Transition:** handle KeepAlive child unmount in Transition out-in mode ([#11833](https://github.com/vuejs/core/issues/11833)) ([6b7901d](https://github.com/vuejs/core/commit/6b7901d28ed3a6a9242c666cc1b8e3c0b0b0fe62)), closes [#11775](https://github.com/vuejs/core/issues/11775)
* **useId:** make generated IDs selector compatible ([babfb4c](https://github.com/vuejs/core/commit/babfb4cbcbf98601d76c1d7653eae8d250ce2710)), closes [#11828](https://github.com/vuejs/core/issues/11828)
## [3.5.2](https://github.com/vuejs/core/compare/v3.5.1...v3.5.2) (2024-09-05)
### Bug Fixes
* **reactivity:** make toRaw work on proxies created by proxyRef ([46c3ab1](https://github.com/vuejs/core/commit/46c3ab1d714024894fa1d33e495d5d35c7817d4d))
* **reactivity:** pass oldValue to computed getter ([#11813](https://github.com/vuejs/core/issues/11813)) ([98864a7](https://github.com/vuejs/core/commit/98864a7ef5c8080c407166c8221488a4eacbbc81)), closes [#11812](https://github.com/vuejs/core/issues/11812)
* **reactivity:** prevent endless recursion in computed getters ([#11797](https://github.com/vuejs/core/issues/11797)) ([716275d](https://github.com/vuejs/core/commit/716275d1b1d2383d8ef0306fcd94558d4d9170f2))
* **reactivity:** self-referencing computed should refresh ([e84c4a6](https://github.com/vuejs/core/commit/e84c4a608e9dc96fb2a4a29d538bcc64f26103a2)), closes [/github.com/vuejs/core/pull/11797#issuecomment-2330738633](https://github.com//github.com/vuejs/core/pull/11797/issues/issuecomment-2330738633)
* **scheduler:** prevent duplicate jobs being queued ([#11826](https://github.com/vuejs/core/issues/11826)) ([df56cc5](https://github.com/vuejs/core/commit/df56cc528793b1d6131a1e64095dd5cb95c56bee)), closes [#11712](https://github.com/vuejs/core/issues/11712) [#11807](https://github.com/vuejs/core/issues/11807)
* **suspense:** avoid updating anchor if activeBranch has not been rendered to the actual container ([#11818](https://github.com/vuejs/core/issues/11818)) ([3c0d531](https://github.com/vuejs/core/commit/3c0d531fa7fe762bfe46fbe63f318adc95221795)), closes [#11806](https://github.com/vuejs/core/issues/11806)
* **Transition:** handle KeepAlive child unmount in Transition out-in mode ([#11778](https://github.com/vuejs/core/issues/11778)) ([3116553](https://github.com/vuejs/core/commit/311655352931863dfcf520b8cf29cebc5b7e1e00)), closes [#11775](https://github.com/vuejs/core/issues/11775)
* **types:** add HTMLDialogElement missing close event ([#11811](https://github.com/vuejs/core/issues/11811)) ([3634f7a](https://github.com/vuejs/core/commit/3634f7a4c1649ad2e7e969eb4512512868c61d01))
* **types:** added name attribute support to details tag ([#11823](https://github.com/vuejs/core/issues/11823)) ([c74176e](https://github.com/vuejs/core/commit/c74176ec7b4d1d34159ce21d600c04b157ac5549)), closes [#11821](https://github.com/vuejs/core/issues/11821)
* **types:** fix defineComponent props inference when setup() has explicit annotation ([fca20a3](https://github.com/vuejs/core/commit/fca20a39aa4a6f98c8f972bd435ebb7dc535648a)), closes [#11803](https://github.com/vuejs/core/issues/11803)
* **useTemplateRef:** properly fix readonly warning in dev and ensure prod behavior consistency ([9b7797d](https://github.com/vuejs/core/commit/9b7797d0d1fc773e979e042673d5b9b3151c40fc)), closes [#11808](https://github.com/vuejs/core/issues/11808) [#11816](https://github.com/vuejs/core/issues/11816) [#11810](https://github.com/vuejs/core/issues/11810)
### Features
* **compiler-core:** parse modifiers as expression to provide location data ([#11819](https://github.com/vuejs/core/issues/11819)) ([3f13203](https://github.com/vuejs/core/commit/3f13203564164eeb2945bdc0b9ef755c37477d75))
## [3.5.1](https://github.com/vuejs/core/compare/v3.5.0...v3.5.1) (2024-09-04)
### Bug Fixes
* **build:** improve built-in components treeshakability ([4eee630](https://github.com/vuejs/core/commit/4eee630b3122a10d0baf9b91358cfffa92d6fd81))
* **reactivity:** handle non-array arguments in reactive `concat` method ([#11794](https://github.com/vuejs/core/issues/11794)) ([475977a](https://github.com/vuejs/core/commit/475977a6f76b77392610e0a3ec2b0e076d1e1d59)), closes [#11792](https://github.com/vuejs/core/issues/11792)
* **Transition:** avoid applying transition hooks on comment vnode ([#11788](https://github.com/vuejs/core/issues/11788)) ([51912f8](https://github.com/vuejs/core/commit/51912f8a02e35f172f6d30ed7a2f3a92c1407cf9)), closes [#11782](https://github.com/vuejs/core/issues/11782)
* **types:** avoid using intersection type in `Readonly<...>` to fix JSDoc emit ([#11799](https://github.com/vuejs/core/issues/11799)) ([7518bc1](https://github.com/vuejs/core/commit/7518bc19dc73ba46dcf1eef6e23f9e6e75552675))
* **useTemplateRef:** fix readonly warning when useTemplateRef has same variable name as template ref ([bc63df0](https://github.com/vuejs/core/commit/bc63df01992fdbf0b6749ad234153725697ed896)), closes [#11795](https://github.com/vuejs/core/issues/11795) [#11802](https://github.com/vuejs/core/issues/11802) [#11804](https://github.com/vuejs/core/issues/11804)
# [3.5.0](https://github.com/vuejs/core/compare/v3.5.0-rc.1...v3.5.0) (2024-09-03)
## Aggregated Features List for 3.5 (alpha to stable)
### Reactivity
- **reactivity**: Refactor reactivity system to use version counting and doubly-linked list tracking ([#10397](https://github.com/vuejs/core/pull/10397)) ([05eb4e0](https://github.com/vuejs/core/commit/05eb4e0fefd585125dd60b7f8fe9c36928d921aa))
- **reactivity**: Optimize array tracking ([#9511](https://github.com/vuejs/core/pull/9511)) ([70196a4](https://github.com/vuejs/core/commit/70196a40cc078f50fcc1110c38c06fbcc70b205e))
- **compiler-sfc:** enable reactive props destructure by default ([d2dac0e](https://github.com/vuejs/core/commit/d2dac0e359c47d1ed0aa77eda488e76fd6466d2d))
- **reactivity:** `onEffectCleanup` API ([2cc5615](https://github.com/vuejs/core/commit/2cc5615590de77126e8df46136de0240dbde5004)), closes [#10173](https://github.com/vuejs/core/issues/10173)
- **reactivity:** add `failSilently` argument for `onScopeDispose` ([9a936aa](https://github.com/vuejs/core/commit/9a936aaec489c79433a32791ecf5ddb1739a62bd))
- **reactivity/watch:** base `watch`, `getCurrentWatcher`, and `onWatcherCleanup` ([#9927](https://github.com/vuejs/core/issues/9927)) ([205e5b5](https://github.com/vuejs/core/commit/205e5b5e277243c3af2c937d9bd46cf671296b72))
- **reactivity/watch:** add pause/resume for ReactiveEffect, EffectScope, and WatchHandle ([#9651](https://github.com/vuejs/core/issues/9651)) ([267093c](https://github.com/vuejs/core/commit/267093c31490050bfcf3ff2b30a2aefee2dad582))
- **watch:** support passing number to `deep` option to control the watch depth ([#9572](https://github.com/vuejs/core/issues/9572)) ([22f7d96](https://github.com/vuejs/core/commit/22f7d96757956ebe0baafe52256aa327908cc51c))
- **types:** export `MultiWatchSources` type ([#9563](https://github.com/vuejs/core/issues/9563)) ([998dca5](https://github.com/vuejs/core/commit/998dca59f140420280803233f41707580688562c))
- **types:** allow computed getter and setter types to be unrelated ([#11472](https://github.com/vuejs/core/issues/11472)) ([a01675e](https://github.com/vuejs/core/commit/a01675ef8f99b5acd6832c53051f4415b18609f2)), closes [#7271](https://github.com/vuejs/core/issues/7271)
### SSR
- **runtime-core:** `useId()` and `app.config.idPrefix` ([#11404](https://github.com/vuejs/core/issues/11404)) ([73ef156](https://github.com/vuejs/core/commit/73ef1561f6905d69f968c094d0180c61824f1247))
- **hydration:** lazy hydration strategies for async components ([#11458](https://github.com/vuejs/core/issues/11458)) ([d14a11c](https://github.com/vuejs/core/commit/d14a11c1cdcee88452f17ce97758743c863958f4))
- **hydration:** support suppressing hydration mismatch via data-allow-mismatch ([94fb2b8](https://github.com/vuejs/core/commit/94fb2b8106a66bcca1a3f922a246a29fdd1274b1))
### Custom Element
- **custom-element:** `useHost()` helper ([775103a](https://github.com/vuejs/core/commit/775103af37df69d34c79f12c4c1776c47d07f0a0))
- **custom-element:** `useShadowRoot()` helper ([5a1a89b](https://github.com/vuejs/core/commit/5a1a89bd6178cc2f84ba91da7d72aee4c6ec1282)), closes [#6113](https://github.com/vuejs/core/issues/6113) [#8195](https://github.com/vuejs/core/issues/8195)
- **custom-element:** expose `this.$host` in Options API ([1ef8f46](https://github.com/vuejs/core/commit/1ef8f46af0cfdec2fed66376772409e0aa25ad50))
- **custom-element:** inject child components styles to custom element shadow root ([#11517](https://github.com/vuejs/core/issues/11517)) ([56c76a8](https://github.com/vuejs/core/commit/56c76a8b05c45f782ed3a16ec77c6292b71a17f1)), closes [#4662](https://github.com/vuejs/core/issues/4662) [#7941](https://github.com/vuejs/core/issues/7941) [#7942](https://github.com/vuejs/core/issues/7942)
- **custom-element:** support configurable app instance in defineCustomElement ([6758c3c](https://github.com/vuejs/core/commit/6758c3cd0427f97394d95168c655dae3b7fa62cd)), closes [#4356](https://github.com/vuejs/core/issues/4356) [#4635](https://github.com/vuejs/core/issues/4635)
- **custom-element:** support css `:host` selector by applying css vars on host element ([#8830](https://github.com/vuejs/core/issues/8830)) ([03a9ea2](https://github.com/vuejs/core/commit/03a9ea2b88df0842a820e09f7445c4b9189e3fcb)), closes [#8826](https://github.com/vuejs/core/issues/8826)
- **custom-element:** support emit with options ([e181bff](https://github.com/vuejs/core/commit/e181bff6dc39d5cef92000c10291243c7d6e4d08)), closes [#7605](https://github.com/vuejs/core/issues/7605)
- **custom-element:** support expose on customElement ([#6256](https://github.com/vuejs/core/issues/6256)) ([af838c1](https://github.com/vuejs/core/commit/af838c1b5ec23552e52e64ffa7db0eb0246c3624)), closes [#5540](https://github.com/vuejs/core/issues/5540)
- **custom-element:** support `nonce` option for injected style tags ([bb4a02a](https://github.com/vuejs/core/commit/bb4a02a70c30e739a3c705b3d96d09258d7d7ded)), closes [#6530](https://github.com/vuejs/core/issues/6530)
- **custom-element:** support passing custom-element-specific options via 2nd argument of defineCustomElement ([60a88a2](https://github.com/vuejs/core/commit/60a88a2b129714186cf6ba66f30f31d733d0311e))
- **custom-element:** support `shadowRoot: false` in `defineCustomElement()` ([37d2ce5](https://github.com/vuejs/core/commit/37d2ce5d8e0fac4a00064f02b05f91f69b2d5d5e)), closes [#4314](https://github.com/vuejs/core/issues/4314) [#4404](https://github.com/vuejs/core/issues/4404)
### Teleport
- **teleport:** support deferred Teleport ([#11387](https://github.com/vuejs/core/issues/11387)) ([59a3e88](https://github.com/vuejs/core/commit/59a3e88903b10ac2278170a44d5a03f24fef23ef)), closes [#2015](https://github.com/vuejs/core/issues/2015) [#11386](https://github.com/vuejs/core/issues/11386)
- **teleport/transition:** support directly nesting Teleport inside Transition ([#6548](https://github.com/vuejs/core/issues/6548)) ([0e6e3c7](https://github.com/vuejs/core/commit/0e6e3c7eb0e5320b7c1818e025cb4a490fede9c0)), closes [#5836](https://github.com/vuejs/core/issues/5836)
### Misc
- **runtime-core:** `useTemplateRef()` ([3ba70e4](https://github.com/vuejs/core/commit/3ba70e49b5856c53611c314d4855d679a546a7df))
- **runtime-core:** add `app.onUnmount()` for registering cleanup functions ([#4619](https://github.com/vuejs/core/issues/4619)) ([582a3a3](https://github.com/vuejs/core/commit/582a3a382b1adda565bac576b913a88d9e8d7a9e)), closes [#4516](https://github.com/vuejs/core/issues/4516)
- **runtime-core:** add `app.config.throwUnhandledErrorInProduction` ([f476b7f](https://github.com/vuejs/core/commit/f476b7f030f2dd427ca655fcea36f4933a4b4da0)), closes [#7876](https://github.com/vuejs/core/issues/7876)
- **runtime-dom:** Trusted Types compatibility ([#10844](https://github.com/vuejs/core/issues/10844)) ([6d4eb94](https://github.com/vuejs/core/commit/6d4eb94853ed1b2b1675bdd7d5ba9c75cc6daed5))
- **compiler-core:** support `Symbol` global in template expressions ([#9069](https://github.com/vuejs/core/issues/9069)) ([a501a85](https://github.com/vuejs/core/commit/a501a85a7c910868e01a5c70a2abea4e9d9e87f3))
- **types:** export more emit related types ([#11017](https://github.com/vuejs/core/issues/11017)) ([189573d](https://github.com/vuejs/core/commit/189573dcee2a16bd3ed36ff5589d43f535e5e733))
* **types:** add loading prop to iframe ([#11767](https://github.com/vuejs/core/issues/11767)) ([d86fe0e](https://github.com/vuejs/core/commit/d86fe0ec002901dc359a0e85f3a421b4a8538d68))
### Internals
- **reactivity:** store value cache on CustomRefs impls ([#11539](https://github.com/vuejs/core/issues/11539)) ([e044b6e](https://github.com/vuejs/core/commit/e044b6e737efc9433d1d84590036b82280da6292))
- **types:** provide internal options for directly using user types in language tools ([#10801](https://github.com/vuejs/core/issues/10801)) ([75c8cf6](https://github.com/vuejs/core/commit/75c8cf63a1ef30ac84f91282d66ad3f57c6612e9))
- **types:** provide internal options for using refs type in language tools ([#11492](https://github.com/vuejs/core/issues/11492)) ([5ffd1a8](https://github.com/vuejs/core/commit/5ffd1a89455807d5069eb2c28eba0379641dca76))
## Bug Fixes
* **compiler-sfc:** fix import usage check for kebab-case same name shorthand binding ([0f7c0e5](https://github.com/vuejs/core/commit/0f7c0e5dc0eedada7a5194db87fd0a7dbd1d3354)), closes [#11745](https://github.com/vuejs/core/issues/11745) [#11754](https://github.com/vuejs/core/issues/11754)
* **cssVars:** correctly escape double quotes in SSR ([#11784](https://github.com/vuejs/core/issues/11784)) ([7b5b6e0](https://github.com/vuejs/core/commit/7b5b6e0275f35748dca6d7eb842f8ab2364c6b9a)), closes [#11779](https://github.com/vuejs/core/issues/11779)
* **deps:** update dependency postcss to ^8.4.44 ([#11774](https://github.com/vuejs/core/issues/11774)) ([cb843e0](https://github.com/vuejs/core/commit/cb843e0be31f9e563ccfc30eca0c06f2a224b505))
* **hydration:** escape css var name to avoid mismatch ([#11739](https://github.com/vuejs/core/issues/11739)) ([ca12e77](https://github.com/vuejs/core/commit/ca12e776bc53aaa31f2df6bb6edc6be1b2f10c37)), closes [#11735](https://github.com/vuejs/core/issues/11735)
* **hydration:** handle text nodes with 0 during hydration ([#11772](https://github.com/vuejs/core/issues/11772)) ([c756da2](https://github.com/vuejs/core/commit/c756da24b2d8635cf52b4c7d3abf5bf938852cc5)), closes [#11771](https://github.com/vuejs/core/issues/11771)
* **reactivity:** correctly handle method calls on user-extended arrays ([#11760](https://github.com/vuejs/core/issues/11760)) ([9817c80](https://github.com/vuejs/core/commit/9817c80187bec6a3344c74d65fac92262de0fcdd)), closes [#11759](https://github.com/vuejs/core/issues/11759)
* **runtime-dom:** avoid unnecessary prop patch for checkbox ([#11657](https://github.com/vuejs/core/issues/11657)) ([c3ce9fe](https://github.com/vuejs/core/commit/c3ce9fe3d8fc27d864ce7148cd36da882cfc21ab)), closes [#11647](https://github.com/vuejs/core/issues/11647)
* **runtime-dom:** prevent unnecessary DOM update from v-model ([#11656](https://github.com/vuejs/core/issues/11656)) ([b1be9bd](https://github.com/vuejs/core/commit/b1be9bd64f2c7c4286fecb25bad5d5edd49efce9)), closes [#11647](https://github.com/vuejs/core/issues/11647)
* **server-renderer:** Fix call to serverPrefetch in server renderer with an async setup ([#10893](https://github.com/vuejs/core/issues/10893)) ([6039e25](https://github.com/vuejs/core/commit/6039e25e04a8c1db5821955f011d57f1615807ab))
* **server-renderer:** render `className` during SSR ([#11722](https://github.com/vuejs/core/issues/11722)) ([52cdb0f](https://github.com/vuejs/core/commit/52cdb0f991dc154ae32a2900874d5dbc4e078565))
* **types/defineModel:** allow getter and setter types to be unrelated ([#11699](https://github.com/vuejs/core/issues/11699)) ([fe07f70](https://github.com/vuejs/core/commit/fe07f7073617df358c2f8cbc3de433359e873c96)), closes [#11697](https://github.com/vuejs/core/issues/11697)
# [3.5.0-rc.1](https://github.com/vuejs/core/compare/v3.5.0-beta.3...v3.5.0-rc.1) (2024-08-29)
### Bug Fixes
* **compiler-sfc:** skip circular tsconfig project reference ([#11680](https://github.com/vuejs/core/issues/11680)) ([9c4c2e5](https://github.com/vuejs/core/commit/9c4c2e51b045218d0c5ca64b4fb58b17d5d580cc)), closes [#11382](https://github.com/vuejs/core/issues/11382)
* **custom-element:** handle keys set on custom elements ([#11655](https://github.com/vuejs/core/issues/11655)) ([f1d1831](https://github.com/vuejs/core/commit/f1d1831f07fe52d5681a5ec9ec310572463abf26)), closes [#11641](https://github.com/vuejs/core/issues/11641)
* **deps:** update dependency monaco-editor to ^0.51.0 ([#11713](https://github.com/vuejs/core/issues/11713)) ([434f8a9](https://github.com/vuejs/core/commit/434f8a97c77f68aeae050e9e4e1f54f63bc4bd26))
* **keep-alive:** reset keep alive flag when the component is removed from include ([#11718](https://github.com/vuejs/core/issues/11718)) ([29c321b](https://github.com/vuejs/core/commit/29c321bfd33f9197244dec3d027077e63b2cdf2f)), closes [#11717](https://github.com/vuejs/core/issues/11717)
* **reactivity:** avoid infinite recursion when mutating ref wrapped in reactive ([313e4bf](https://github.com/vuejs/core/commit/313e4bf55214ac1e334a99c329a3ba5daca4f156)), closes [#11696](https://github.com/vuejs/core/issues/11696)
* **reactivity:** ensure watcher with once: true are properly removed from effect scope ([#11665](https://github.com/vuejs/core/issues/11665)) ([fbc0c42](https://github.com/vuejs/core/commit/fbc0c42bcf6dea5a6ae664223fa19d4375ca39f0))
* **runtime-dom:** setting innerHTML when patching props should go through trusted types ([d875de5](https://github.com/vuejs/core/commit/d875de54e9e03e0768fe550aa4c4886a4baf3bd7))
* **types:** GlobalDirective / GlobalComponents should not be records ([42e8df6](https://github.com/vuejs/core/commit/42e8df62030e7f2c287d9103f045e67b34a63e3b))
# [3.5.0-beta.3](https://github.com/vuejs/core/compare/v3.5.0-beta.2...v3.5.0-beta.3) (2024-08-20)
### Bug Fixes
* **reactivity:** extended methods respect reactive ([#11629](https://github.com/vuejs/core/issues/11629)) ([9de1d10](https://github.com/vuejs/core/commit/9de1d101f98bf6081f41038f6974826f190330a0)), closes [#11628](https://github.com/vuejs/core/issues/11628)
* **runtime-core:** correct type inference for PascalCase emits ([#11579](https://github.com/vuejs/core/issues/11579)) ([d7d0371](https://github.com/vuejs/core/commit/d7d0371e74707ee601020f67de88e091cdae2673)), closes [vuejs/language-tools#4269](https://github.com/vuejs/language-tools/issues/4269)
* **runtime-core:** ensure suspense content inherit scopeId ([#10652](https://github.com/vuejs/core/issues/10652)) ([ac2a410](https://github.com/vuejs/core/commit/ac2a410e46392db63ca4ed2db3c0fa71ebe1e855)), closes [#5148](https://github.com/vuejs/core/issues/5148)
* **runtime-core:** pre jobs without an id should run first ([#7746](https://github.com/vuejs/core/issues/7746)) ([b332f80](https://github.com/vuejs/core/commit/b332f80f0edb018229a23b43b93bb402b6368a3c))
* **ssr:** apply ssr props to the the fallback vnode-based branch in ssr ([#7247](https://github.com/vuejs/core/issues/7247)) ([98b83e8](https://github.com/vuejs/core/commit/98b83e86d16c635547a1e735e5fb675aea2f0f1b)), closes [#6123](https://github.com/vuejs/core/issues/6123)
* **types/custom-element:** `defineCustomElement` with required props ([#11578](https://github.com/vuejs/core/issues/11578)) ([5e0f6d5](https://github.com/vuejs/core/commit/5e0f6d5f8fe7c4eb8f247357c3e2e281726f36db))
* **types:** strip non-prop default values from return type of withDefaults ([#9998](https://github.com/vuejs/core/issues/9998)) ([44973bb](https://github.com/vuejs/core/commit/44973bb3e790db7d8aa7af4eda21c80cac73a8de)), closes [#9899](https://github.com/vuejs/core/issues/9899)
* **watch:** handle errors in computed used as watch source ([#11626](https://github.com/vuejs/core/issues/11626)) ([8bcaad4](https://github.com/vuejs/core/commit/8bcaad4a32cf0f1f89e0259f6a53036620b7fe9f)), closes [#11624](https://github.com/vuejs/core/issues/11624)
### Features
* **reactivity:** base `watch`, `getCurrentWatcher`, and `onWatcherCleanup` ([#9927](https://github.com/vuejs/core/issues/9927)) ([205e5b5](https://github.com/vuejs/core/commit/205e5b5e277243c3af2c937d9bd46cf671296b72))
### Performance Improvements
* **runtime-core:** use `apply` to avoid spreading. ([#5985](https://github.com/vuejs/core/issues/5985)) ([bb6babc](https://github.com/vuejs/core/commit/bb6babca8f206615d4e246457cd54d21bb3bc5a4))
# [3.5.0-beta.2](https://github.com/vuejs/core/compare/v3.5.0-beta.1...v3.5.0-beta.2) (2024-08-15)
### Bug Fixes
* **build:** revert entities to 4.5 to avoid runtime resolution errors ([e9e0815](https://github.com/vuejs/core/commit/e9e08155bf8d00c3327ed7371330eb2ae467e560)), closes [#11603](https://github.com/vuejs/core/issues/11603)
* **compiler-core:** use ast-based check for function expressions when possible ([5861229](https://github.com/vuejs/core/commit/58612294757480974e667652ede5bbcf72b1089d)), closes [#11615](https://github.com/vuejs/core/issues/11615)
* **compiler-sfc:** fix prefixIdentifier default value ([3d6f015](https://github.com/vuejs/core/commit/3d6f01571b3fb61b32da599d0419eff4e3ebb231))
* **compiler-sfc:** handle keyof operator with index object ([#11581](https://github.com/vuejs/core/issues/11581)) ([fe00815](https://github.com/vuejs/core/commit/fe008152c0612ff3ecc7ad88e7e66a06b1b2bc3f))
* **custom-element:** keep instance.isCE for backwards compat ([e19fc27](https://github.com/vuejs/core/commit/e19fc270428b59456fee43224990138c4d6ccb2d))
* **deps:** update dependency postcss to ^8.4.41 ([#11585](https://github.com/vuejs/core/issues/11585)) ([4c4e12a](https://github.com/vuejs/core/commit/4c4e12ae28d67d616924b0601e68adc551959971))
* **keep-alive:** ensure include/exclude regexp work with global flag ([#11595](https://github.com/vuejs/core/issues/11595)) ([3653bc0](https://github.com/vuejs/core/commit/3653bc0f45d6fedf84e29b64ca52584359c383c0))
* **reactivity:** ensure extended method arguments are not lost ([#11574](https://github.com/vuejs/core/issues/11574)) ([4085def](https://github.com/vuejs/core/commit/4085def1bae42d01ee3c22c731cc4a02096464ee)), closes [#11570](https://github.com/vuejs/core/issues/11570)
* **reactivity:** sync watch should be executed correctly ([#11589](https://github.com/vuejs/core/issues/11589)) ([3bda3e8](https://github.com/vuejs/core/commit/3bda3e83fd9e2fbe451a1c79dae82ff6a7467683)), closes [#11577](https://github.com/vuejs/core/issues/11577)
* **types/computed:** ensure type safety for `WritableComputedRef` ([#11608](https://github.com/vuejs/core/issues/11608)) ([5cf5a16](https://github.com/vuejs/core/commit/5cf5a1620d9a97382d386c277265d9dd051fe484))
* **types:** add fallback stub for DOM types when DOM lib is absent ([#11598](https://github.com/vuejs/core/issues/11598)) ([fee6697](https://github.com/vuejs/core/commit/fee669764fbf475adce9e47a7a73b4937ab31ffc))
### Features
* **deprecated:** remove deprecated parseExpressions option ([#11597](https://github.com/vuejs/core/issues/11597)) ([4e7d5db](https://github.com/vuejs/core/commit/4e7d5db4d276a5d4aaf3af7d43cfd28c171db307))
# [3.5.0-beta.1](https://github.com/vuejs/core/compare/v3.4.37...v3.5.0-beta.1) (2024-08-08) # [3.5.0-beta.1](https://github.com/vuejs/core/compare/v3.4.37...v3.5.0-beta.1) (2024-08-08)
@ -119,7 +434,7 @@
## Previous Changelogs ## Previous Changelogs
### 3.4.x (2023-10-28 - 2024-08-08) ### 3.4.x (2023-10-28 - 2024-08-15)
See [3.4 changelog](./changelogs/CHANGELOG-3.4.md) See [3.4 changelog](./changelogs/CHANGELOG-3.4.md)

View File

@ -10,6 +10,6 @@ Please note that we do not consider XSS via template expressions a valid attack
We would like to thank the following security researchers for responsibly disclosing security issues to us. We would like to thank the following security researchers for responsibly disclosing security issues to us.
- Jeet Pal - [@jeetpal2007](https://github.com/jeetpal2007) | [Email](jeetpal2007@gmail.com) | [LinkedIn](https://in.linkedin.com/in/jeet-pal-22601a290) - Jeet Pal - [@jeetpal2007](https://github.com/jeetpal2007) | [Email](mailto:jeetpal2007@gmail.com) | [LinkedIn](https://in.linkedin.com/in/jeet-pal-22601a290)
- Mix - [@mnixry](https://github.com/mnixry) - Mix - [@mnixry](https://github.com/mnixry)
- Aviv Keller - [@RedYetiDev](https://github.com/redyetidev) | [LinkedIn](https://www.linkedin.com/in/redyetidev) <redyetidev@gmail.com> - Aviv Keller - [@RedYetiDev](https://github.com/redyetidev) | [LinkedIn](https://www.linkedin.com/in/redyetidev) <redyetidev@gmail.com>

View File

@ -1,3 +1,15 @@
## [3.4.38](https://github.com/vuejs/core/compare/v3.4.37...v3.4.38) (2024-08-15)
### Bug Fixes
* **build:** revert entities to 4.5 to avoid runtime resolution errors ([f349af7](https://github.com/vuejs/core/commit/f349af7b65b9f8605d8b7bafcc06c25ab1f2daf0)), closes [#11603](https://github.com/vuejs/core/issues/11603)
* **compiler-core:** use ast-based check for function expressions when possible ([236cac3](https://github.com/vuejs/core/commit/236cac3ff285890b8468dc827c463d87a91e516f)), closes [#11615](https://github.com/vuejs/core/issues/11615)
* **compiler-sfc:** handle keyof operator with index object ([#11581](https://github.com/vuejs/core/issues/11581)) ([74d26db](https://github.com/vuejs/core/commit/74d26dbbe3cf2f70d1b772284eec6743ea946f6d))
* **types:** add fallback stub for DOM types when DOM lib is absent ([#11598](https://github.com/vuejs/core/issues/11598)) ([4db0085](https://github.com/vuejs/core/commit/4db0085de316e1b773f474597915f9071d6ae6c6))
## [3.4.37](https://github.com/vuejs/core/compare/v3.4.36...v3.4.37) (2024-08-08) ## [3.4.37](https://github.com/vuejs/core/compare/v3.4.36...v3.4.37) (2024-08-08)
@ -12,6 +24,7 @@
* Revert "fix(types/ref): correct type inference for nested refs ([#11536](https://github.com/vuejs/core/issues/11536))" ([3a56315](https://github.com/vuejs/core/commit/3a56315f94bc0e11cfbb288b65482ea8fc3a39b4)) * Revert "fix(types/ref): correct type inference for nested refs ([#11536](https://github.com/vuejs/core/issues/11536))" ([3a56315](https://github.com/vuejs/core/commit/3a56315f94bc0e11cfbb288b65482ea8fc3a39b4))
* **runtime-core:** fix warning for missing event handler ([#11489](https://github.com/vuejs/core/issues/11489)) ([e359ff0](https://github.com/vuejs/core/commit/e359ff0046286aee03fe31656c023677be457e07)), closes [#4803](https://github.com/vuejs/core/issues/4803) [#8268](https://github.com/vuejs/core/issues/8268) * **runtime-core:** fix warning for missing event handler ([#11489](https://github.com/vuejs/core/issues/11489)) ([e359ff0](https://github.com/vuejs/core/commit/e359ff0046286aee03fe31656c023677be457e07)), closes [#4803](https://github.com/vuejs/core/issues/4803) [#8268](https://github.com/vuejs/core/issues/8268)
* **runtime-core:** prioritize using the provides from currentApp in nested createApp ([#11502](https://github.com/vuejs/core/issues/11502)) ([7e75de0](https://github.com/vuejs/core/commit/7e75de002f08076a02c9361a58fa1d0af1772964)), closes [#11488](https://github.com/vuejs/core/issues/11488) * **runtime-core:** prioritize using the provides from currentApp in nested createApp ([#11502](https://github.com/vuejs/core/issues/11502)) ([7e75de0](https://github.com/vuejs/core/commit/7e75de002f08076a02c9361a58fa1d0af1772964)), closes [#11488](https://github.com/vuejs/core/issues/11488)
* Note: this change will break `inject` calls inside Pinia stores that expects to be able to inject provided values from the component using the store. This is expected because the usage is relying on previously incorrect behavior.
* **runtime-dom:** apply css vars before mount ([#11538](https://github.com/vuejs/core/issues/11538)) ([fdc2a31](https://github.com/vuejs/core/commit/fdc2a31dbd4196d6432be16767a1bfdab1240d49)), closes [#11533](https://github.com/vuejs/core/issues/11533) * **runtime-dom:** apply css vars before mount ([#11538](https://github.com/vuejs/core/issues/11538)) ([fdc2a31](https://github.com/vuejs/core/commit/fdc2a31dbd4196d6432be16767a1bfdab1240d49)), closes [#11533](https://github.com/vuejs/core/issues/11533)
* **ssr:** ensure content is valid when rendering normal slot ([#11491](https://github.com/vuejs/core/issues/11491)) ([6c90324](https://github.com/vuejs/core/commit/6c903248703e2413c6197b9ad4d535f31c8eac39)), closes [#11326](https://github.com/vuejs/core/issues/11326) * **ssr:** ensure content is valid when rendering normal slot ([#11491](https://github.com/vuejs/core/issues/11491)) ([6c90324](https://github.com/vuejs/core/commit/6c903248703e2413c6197b9ad4d535f31c8eac39)), closes [#11326](https://github.com/vuejs/core/issues/11326)
* **types/ref:** correct type inference for nested refs ([#11536](https://github.com/vuejs/core/issues/11536)) ([536f623](https://github.com/vuejs/core/commit/536f62332c455ba82ef2979ba634b831f91928ba)), closes [#11532](https://github.com/vuejs/core/issues/11532) [#11537](https://github.com/vuejs/core/issues/11537) * **types/ref:** correct type inference for nested refs ([#11536](https://github.com/vuejs/core/issues/11536)) ([536f623](https://github.com/vuejs/core/commit/536f62332c455ba82ef2979ba634b831f91928ba)), closes [#11532](https://github.com/vuejs/core/issues/11532) [#11537](https://github.com/vuejs/core/issues/11537)

View File

@ -1,6 +1,6 @@
import importX from 'eslint-plugin-import-x' import importX from 'eslint-plugin-import-x'
import tseslint from 'typescript-eslint' import tseslint from 'typescript-eslint'
import vitest from 'eslint-plugin-vitest' import vitest from '@vitest/eslint-plugin'
import { builtinModules } from 'node:module' import { builtinModules } from 'node:module'
const DOMGlobals = ['window', 'document'] const DOMGlobals = ['window', 'document']

View File

@ -1,14 +1,14 @@
{ {
"private": true, "private": true,
"version": "3.5.0-beta.1", "version": "3.5.11",
"packageManager": "pnpm@9.7.0", "packageManager": "pnpm@9.12.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "node scripts/dev.js", "dev": "node scripts/dev.js",
"build": "node scripts/build.js", "build": "node scripts/build.js",
"build-dts": "tsc -p tsconfig.build-browser.json && tsc -p tsconfig.build-node.json && rollup -c rollup.dts.config.js", "build-dts": "tsc -p tsconfig.build.json --noCheck && rollup -c rollup.dts.config.js",
"clean": "rimraf packages/*/dist temp .eslintcache", "clean": "rimraf --glob packages/*/dist temp .eslintcache",
"size": "run-s \"size-*\" && tsx scripts/usage-size.ts", "size": "run-s \"size-*\" && node scripts/usage-size.js",
"size-global": "node scripts/build.js vue runtime-dom -f global -p --size", "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-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", "size-esm": "node scripts/build.js runtime-dom runtime-core reactivity shared -f esm-bundler",
@ -17,11 +17,11 @@
"format": "prettier --write --cache .", "format": "prettier --write --cache .",
"format-check": "prettier --check --cache .", "format-check": "prettier --check --cache .",
"test": "vitest", "test": "vitest",
"test-unit": "vitest -c vitest.unit.config.ts", "test-unit": "vitest --project unit",
"test-e2e": "node scripts/build.js vue -f global -d && vitest -c vitest.e2e.config.ts", "test-e2e": "node scripts/build.js vue -f global -d && vitest --project e2e",
"test-dts": "run-s build-dts test-dts-only", "test-dts": "run-s build-dts test-dts-only",
"test-dts-only": "tsc -p packages-private/dts-built-test/tsconfig.json && tsc -p ./packages-private/dts-test/tsconfig.test.json", "test-dts-only": "tsc -p packages-private/dts-built-test/tsconfig.json && tsc -p ./packages-private/dts-test/tsconfig.test.json",
"test-coverage": "vitest -c vitest.unit.config.ts --coverage", "test-coverage": "vitest run --project unit --coverage",
"test-bench": "vitest bench", "test-bench": "vitest bench",
"release": "node scripts/release.js", "release": "node scripts/release.js",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
@ -32,7 +32,7 @@
"dev-sfc-serve": "vite packages-private/sfc-playground --host", "dev-sfc-serve": "vite packages-private/sfc-playground --host",
"dev-sfc-run": "run-p \"dev compiler-sfc -f esm-browser\" \"dev vue -if esm-bundler-runtime\" \"dev vue -ipf esm-browser-runtime\" \"dev server-renderer -if esm-bundler\" dev-sfc-serve", "dev-sfc-run": "run-p \"dev compiler-sfc -f esm-browser\" \"dev vue -if esm-bundler-runtime\" \"dev vue -ipf esm-browser-runtime\" \"dev server-renderer -if esm-bundler\" dev-sfc-serve",
"serve": "serve", "serve": "serve",
"open": "open http://localhost:3000/packages/template-explorer/local.html", "open": "open http://localhost:3000/packages-private/template-explorer/local.html",
"build-sfc-playground": "run-s build-all-cjs build-runtime-esm build-browser-esm build-ssr-esm build-sfc-playground-self", "build-sfc-playground": "run-s build-all-cjs build-runtime-esm build-browser-esm build-ssr-esm build-sfc-playground-self",
"build-all-cjs": "node scripts/build.js vue runtime compiler reactivity shared -af cjs", "build-all-cjs": "node scripts/build.js vue runtime compiler reactivity shared -af cjs",
"build-runtime-esm": "node scripts/build.js runtime reactivity shared -af esm-bundler && node scripts/build.js vue -f esm-bundler-runtime && node scripts/build.js vue -f esm-browser-runtime", "build-runtime-esm": "node scripts/build.js runtime reactivity shared -af esm-bundler && node scripts/build.js vue -f esm-bundler-runtime && node scripts/build.js vue -f esm-browser-runtime",
@ -61,40 +61,40 @@
"devDependencies": { "devDependencies": {
"@babel/parser": "catalog:", "@babel/parser": "catalog:",
"@babel/types": "catalog:", "@babel/types": "catalog:",
"@rollup/plugin-alias": "^5.1.0", "@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-commonjs": "^26.0.1", "@rollup/plugin-commonjs": "^28.0.0",
"@rollup/plugin-json": "^6.1.0", "@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-replace": "5.0.4", "@rollup/plugin-replace": "5.0.4",
"@swc/core": "^1.7.10", "@swc/core": "^1.7.28",
"@types/hash-sum": "^1.0.2", "@types/hash-sum": "^1.0.2",
"@types/node": "^20.14.15", "@types/node": "^20.16.10",
"@types/semver": "^7.5.8", "@types/semver": "^7.5.8",
"@types/serve-handler": "^6.1.4", "@types/serve-handler": "^6.1.4",
"@vitest/coverage-istanbul": "^2.0.5", "@vitest/coverage-v8": "^2.1.1",
"@vue/consolidate": "1.0.0", "@vue/consolidate": "1.0.0",
"conventional-changelog-cli": "^5.0.0", "conventional-changelog-cli": "^5.0.0",
"enquirer": "^2.4.1", "enquirer": "^2.4.1",
"esbuild": "^0.23.0", "esbuild": "^0.24.0",
"esbuild-plugin-polyfill-node": "^0.3.0", "esbuild-plugin-polyfill-node": "^0.3.0",
"eslint": "^9.9.0", "eslint": "^9.12.0",
"eslint-plugin-import-x": "^3.1.0", "eslint-plugin-import-x": "^4.3.1",
"eslint-plugin-vitest": "^0.5.4", "@vitest/eslint-plugin": "^1.0.1",
"estree-walker": "catalog:", "estree-walker": "catalog:",
"jsdom": "^24.1.1", "jsdom": "^25.0.0",
"lint-staged": "^15.2.8", "lint-staged": "^15.2.10",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"magic-string": "^0.30.11", "magic-string": "^0.30.11",
"markdown-table": "^3.0.3", "markdown-table": "^3.0.3",
"marked": "13.0.3", "marked": "13.0.3",
"npm-run-all2": "^6.2.2", "npm-run-all2": "^6.2.3",
"picocolors": "^1.0.1", "picocolors": "^1.1.0",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"pug": "^3.0.3", "pug": "^3.0.3",
"puppeteer": "~23.0.2", "puppeteer": "~23.3.0",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"rollup": "^4.20.0", "rollup": "^4.24.0",
"rollup-plugin-dts": "^6.1.1", "rollup-plugin-dts": "^6.1.1",
"rollup-plugin-esbuild": "^6.1.1", "rollup-plugin-esbuild": "^6.1.1",
"rollup-plugin-polyfill-node": "^0.13.0", "rollup-plugin-polyfill-node": "^0.13.0",
@ -103,12 +103,11 @@
"serve-handler": "^6.1.5", "serve-handler": "^6.1.5",
"simple-git-hooks": "^2.11.1", "simple-git-hooks": "^2.11.1",
"todomvc-app-css": "^2.4.3", "todomvc-app-css": "^2.4.3",
"tslib": "^2.6.3", "tslib": "^2.7.0",
"tsx": "^4.17.0", "typescript": "~5.6.2",
"typescript": "~5.5.4", "typescript-eslint": "^8.8.0",
"typescript-eslint": "^8.0.1",
"vite": "catalog:", "vite": "catalog:",
"vitest": "^2.0.5" "vitest": "^2.1.1"
}, },
"pnpm": { "pnpm": {
"peerDependencyRules": { "peerDependencyRules": {

View File

@ -3,12 +3,17 @@ import { expectType } from './utils'
const app = createApp({}) const app = createApp({})
app.directive<HTMLElement, string>('custom', { app.directive<HTMLElement, string, 'prevent' | 'stop', 'arg1' | 'arg2'>(
mounted(el, binding) { 'custom',
expectType<HTMLElement>(el) {
expectType<string>(binding.value) mounted(el, binding) {
expectType<HTMLElement>(el)
expectType<string>(binding.value)
expectType<{ prevent: boolean; stop: boolean }>(binding.modifiers)
expectType<'arg1' | 'arg2'>(binding.arg!)
// @ts-expect-error not any // @ts-expect-error not any
expectType<number>(binding.value) expectType<number>(binding.value)
},
}, },
}) )

View File

@ -480,6 +480,26 @@ describe('type inference w/ options API', () => {
}) })
}) })
// #4051
describe('type inference w/ empty prop object', () => {
const MyComponent = defineComponent({
props: {},
setup(props) {
return {}
},
render() {},
})
expectType<JSX.Element>(<MyComponent />)
// AllowedComponentProps
expectType<JSX.Element>(<MyComponent class={'foo'} />)
// ComponentCustomProps
expectType<JSX.Element>(<MyComponent custom={1} />)
// VNodeProps
expectType<JSX.Element>(<MyComponent key="1" />)
// @ts-expect-error
expectError(<MyComponent other="other" />)
})
describe('with mixins', () => { describe('with mixins', () => {
const MixinA = defineComponent({ const MixinA = defineComponent({
emits: ['bar'], emits: ['bar'],
@ -906,12 +926,15 @@ describe('emits', () => {
emits: { emits: {
click: (n: number) => typeof n === 'number', click: (n: number) => typeof n === 'number',
input: (b: string) => b.length > 1, input: (b: string) => b.length > 1,
Focus: (f: boolean) => !!f,
}, },
setup(props, { emit }) { setup(props, { emit }) {
expectType<((n: number) => boolean) | undefined>(props.onClick) expectType<((n: number) => boolean) | undefined>(props.onClick)
expectType<((b: string) => boolean) | undefined>(props.onInput) expectType<((b: string) => boolean) | undefined>(props.onInput)
expectType<((f: boolean) => boolean) | undefined>(props.onFocus)
emit('click', 1) emit('click', 1)
emit('input', 'foo') emit('input', 'foo')
emit('Focus', true)
// @ts-expect-error // @ts-expect-error
emit('nope') emit('nope')
// @ts-expect-error // @ts-expect-error
@ -922,6 +945,10 @@ describe('emits', () => {
emit('input') emit('input')
// @ts-expect-error // @ts-expect-error
emit('input', 1) emit('input', 1)
// @ts-expect-error
emit('focus')
// @ts-expect-error
emit('focus', true)
}, },
created() { created() {
this.$emit('click', 1) this.$emit('click', 1)
@ -936,6 +963,10 @@ describe('emits', () => {
this.$emit('input') this.$emit('input')
// @ts-expect-error // @ts-expect-error
this.$emit('input', 1) this.$emit('input', 1)
// @ts-expect-error
this.$emit('focus')
// @ts-expect-error
this.$emit('focus', true)
}, },
mounted() { mounted() {
// #3599 // #3599
@ -954,6 +985,10 @@ describe('emits', () => {
this.$emit('input') this.$emit('input')
// @ts-expect-error // @ts-expect-error
this.$emit('input', 1) this.$emit('input', 1)
// @ts-expect-error
this.$emit('focus')
// @ts-expect-error
this.$emit('focus', true)
}) })
}, },
}) })
@ -1006,6 +1041,18 @@ describe('emits', () => {
}, },
}) })
// #11803 manual props annotation in setup()
const Hello = defineComponent({
name: 'HelloWorld',
inheritAttrs: false,
props: { foo: String },
emits: {
customClick: (args: string) => typeof args === 'string',
},
setup(props: { foo?: string }) {},
})
;<Hello onCustomClick={() => {}} />
// without emits // without emits
defineComponent({ defineComponent({
setup(props, { emit }) { setup(props, { emit }) {
@ -1775,6 +1822,15 @@ describe('__typeRefs backdoor, object syntax', () => {
expectType<number>(refs.child.$refs.foo) expectType<number>(refs.child.$refs.foo)
}) })
describe('__typeEl backdoor', () => {
const Comp = defineComponent({
__typeEl: {} as HTMLAnchorElement,
})
const c = new Comp()
expectType<HTMLAnchorElement>(c.$el)
})
defineComponent({ defineComponent({
props: { props: {
foo: [String, null], foo: [String, null],

View File

@ -99,4 +99,37 @@ describe('defineCustomElement using defineComponent return type', () => {
expectType<number | undefined>(instance.a) expectType<number | undefined>(instance.a)
instance.a = 42 instance.a = 42
}) })
test('with required props', () => {
const Comp1Vue = defineComponent({
props: {
a: { type: Number, required: true },
},
})
const Comp = defineCustomElement(Comp1Vue)
expectType<VueElementConstructor>(Comp)
const instance = new Comp()
expectType<number>(instance.a)
instance.a = 42
})
test('with default props', () => {
const Comp1Vue = defineComponent({
props: {
a: {
type: Number,
default: 1,
validator: () => true,
},
},
emits: ['click'],
})
const Comp = defineCustomElement(Comp1Vue)
expectType<VueElementConstructor>(Comp)
const instance = new Comp()
expectType<number>(instance.a)
instance.a = 42
})
}) })

View File

@ -189,6 +189,24 @@ describe('allow getter and setter types to be unrelated', <T>() => {
f.value = ref(1) f.value = ref(1)
}) })
describe('correctly unwraps nested refs', () => {
const obj = {
n: 24,
ref: ref(24),
nestedRef: ref({ n: ref(0) }),
}
const a = ref(obj)
expectType<number>(a.value.n)
expectType<number>(a.value.ref)
expectType<number>(a.value.nestedRef.n)
const b = reactive({ a })
expectType<number>(b.a.n)
expectType<number>(b.a.ref)
expectType<number>(b.a.nestedRef.n)
})
// computed // computed
describe('allow computed getter and setter types to be unrelated', () => { describe('allow computed getter and setter types to be unrelated', () => {
const obj = ref({ const obj = ref({

View File

@ -227,6 +227,19 @@ describe('withDefaults w/ boolean type', () => {
expectType<boolean | undefined>(res2.bool) expectType<boolean | undefined>(res2.bool)
}) })
describe('withDefaults w/ defineProp type is different from the defaults type', () => {
const res1 = withDefaults(
defineProps<{
bool?: boolean
}>(),
{ bool: false, value: false },
)
expectType<boolean>(res1.bool)
// @ts-expect-error
res1.value
})
describe('defineProps w/ runtime declaration', () => { describe('defineProps w/ runtime declaration', () => {
// runtime declaration // runtime declaration
const props = defineProps({ const props = defineProps({
@ -414,6 +427,51 @@ describe('defineModel', () => {
defineModel<string>({ default: 123 }) defineModel<string>({ default: 123 })
// @ts-expect-error unknown props option // @ts-expect-error unknown props option
defineModel({ foo: 123 }) defineModel({ foo: 123 })
// unrelated getter and setter types
{
const modelVal = defineModel({
get(_: string[]): string {
return ''
},
set(_: number) {
return 1
},
})
expectType<string | undefined>(modelVal.value)
modelVal.value = 1
modelVal.value = undefined
// @ts-expect-error
modelVal.value = 'foo'
const [modelVal2] = modelVal
expectType<string | undefined>(modelVal2.value)
modelVal2.value = 1
modelVal2.value = undefined
// @ts-expect-error
modelVal.value = 'foo'
const count = defineModel('count', {
get(_: string[]): string {
return ''
},
set(_: number) {
return ''
},
})
expectType<string | undefined>(count.value)
count.value = 1
count.value = undefined
// @ts-expect-error
count.value = 'foo'
const [count2] = count
expectType<string | undefined>(count2.value)
count2.value = 1
count2.value = undefined
// @ts-expect-error
count2.value = 'foo'
}
}) })
describe('useModel', () => { describe('useModel', () => {

View File

@ -121,3 +121,5 @@ expectType<JSX.Element>(
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
/>, />,
) )
// details
expectType<JSX.Element>(<details name="details" />)

8
packages-private/global.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
/// <reference types="vite/client" />
// Global compile-time constants
declare var __COMMIT__: string
declare module 'file-saver' {
export function saveAs(blob: any, name: any): void
}

View File

@ -13,7 +13,7 @@
"vite": "catalog:" "vite": "catalog:"
}, },
"dependencies": { "dependencies": {
"@vue/repl": "^4.3.1", "@vue/repl": "^4.4.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"vue": "workspace:*" "vue": "workspace:*"

View File

@ -14,6 +14,12 @@ setVH()
const useSSRMode = ref(false) const useSSRMode = ref(false)
const AUTO_SAVE_STORAGE_KEY = 'vue-sfc-playground-auto-save'
const initAutoSave: boolean = JSON.parse(
localStorage.getItem(AUTO_SAVE_STORAGE_KEY) ?? 'true',
)
const autoSave = ref(initAutoSave)
const { productionMode, vueVersion, importMap } = useVueImportMap({ const { productionMode, vueVersion, importMap } = useVueImportMap({
runtimeDev: import.meta.env.PROD runtimeDev: import.meta.env.PROD
? `${location.origin}/vue.runtime.esm-browser.js` ? `${location.origin}/vue.runtime.esm-browser.js`
@ -89,6 +95,11 @@ function toggleSSR() {
useSSRMode.value = !useSSRMode.value useSSRMode.value = !useSSRMode.value
} }
function toggleAutoSave() {
autoSave.value = !autoSave.value
localStorage.setItem(AUTO_SAVE_STORAGE_KEY, String(autoSave.value))
}
function reloadPage() { function reloadPage() {
replRef.value?.reload() replRef.value?.reload()
} }
@ -111,9 +122,11 @@ onMounted(() => {
:store="store" :store="store"
:prod="productionMode" :prod="productionMode"
:ssr="useSSRMode" :ssr="useSSRMode"
:autoSave="autoSave"
@toggle-theme="toggleTheme" @toggle-theme="toggleTheme"
@toggle-prod="toggleProdMode" @toggle-prod="toggleProdMode"
@toggle-ssr="toggleSSR" @toggle-ssr="toggleSSR"
@toggle-autosave="toggleAutoSave"
@reload-page="reloadPage" @reload-page="reloadPage"
/> />
<Repl <Repl
@ -123,6 +136,8 @@ onMounted(() => {
@keydown.ctrl.s.prevent @keydown.ctrl.s.prevent
@keydown.meta.s.prevent @keydown.meta.s.prevent
:ssr="useSSRMode" :ssr="useSSRMode"
:model-value="autoSave"
:editorOptions="{ autoSaveText: false }"
:store="store" :store="store"
:showCompileOutput="true" :showCompileOutput="true"
:autoResize="true" :autoResize="true"

View File

@ -14,11 +14,13 @@ const props = defineProps<{
store: ReplStore store: ReplStore
prod: boolean prod: boolean
ssr: boolean ssr: boolean
autoSave: boolean
}>() }>()
const emit = defineEmits([ const emit = defineEmits([
'toggle-theme', 'toggle-theme',
'toggle-ssr', 'toggle-ssr',
'toggle-prod', 'toggle-prod',
'toggle-autosave',
'reload-page', 'reload-page',
]) ])
@ -107,6 +109,14 @@ function toggleDark() {
> >
<span>{{ ssr ? 'SSR ON' : 'SSR OFF' }}</span> <span>{{ ssr ? 'SSR ON' : 'SSR OFF' }}</span>
</button> </button>
<button
title="Toggle editor auto save mode"
class="toggle-autosave"
:class="{ enabled: autoSave }"
@click="$emit('toggle-autosave')"
>
<span>{{ autoSave ? 'AutoSave ON' : 'AutoSave OFF' }}</span>
</button>
<button title="Toggle dark mode" class="toggle-dark" @click="toggleDark"> <button title="Toggle dark mode" class="toggle-dark" @click="toggleDark">
<Sun class="light" /> <Sun class="light" />
<Moon class="dark" /> <Moon class="dark" />
@ -199,7 +209,8 @@ h1 img {
} }
.toggle-prod span, .toggle-prod span,
.toggle-ssr span { .toggle-ssr span,
.toggle-autosave span {
font-size: 12px; font-size: 12px;
border-radius: 4px; border-radius: 4px;
padding: 4px 6px; padding: 4px 6px;
@ -214,11 +225,13 @@ h1 img {
background: var(--purple); background: var(--purple);
} }
.toggle-ssr span { .toggle-ssr span,
.toggle-autosave span {
background-color: var(--btn-bg); background-color: var(--btn-bg);
} }
.toggle-ssr.enabled span { .toggle-ssr.enabled span,
.toggle-autosave.enabled span {
color: #fff; color: #fff;
background-color: var(--green); background-color: var(--green);
} }

View File

@ -11,7 +11,7 @@
"vue": "^3.4.0" "vue": "^3.4.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.1.2", "@vitejs/plugin-vue": "^5.1.4",
"vite": "^5.4.0" "vite": "^5.4.8"
} }
} }

View File

@ -11,7 +11,7 @@
"enableNonBrowserBranches": true "enableNonBrowserBranches": true
}, },
"dependencies": { "dependencies": {
"monaco-editor": "^0.50.0", "monaco-editor": "^0.52.0",
"source-map-js": "^1.2.0" "source-map-js": "^1.2.1"
} }
} }

View File

@ -0,0 +1,7 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"isolatedDeclarations": false
},
"include": ["."]
}

View File

@ -1358,7 +1358,27 @@ describe('compiler: parse', () => {
name: 'on', name: 'on',
rawName: 'v-on.enter', rawName: 'v-on.enter',
arg: undefined, arg: undefined,
modifiers: ['enter'], modifiers: [
{
constType: 3,
content: 'enter',
isStatic: true,
loc: {
end: {
column: 16,
line: 1,
offset: 15,
},
source: 'enter',
start: {
column: 11,
line: 1,
offset: 10,
},
},
type: 4,
},
],
exp: undefined, exp: undefined,
loc: { loc: {
start: { offset: 5, line: 1, column: 6 }, start: { offset: 5, line: 1, column: 6 },
@ -1377,7 +1397,46 @@ describe('compiler: parse', () => {
name: 'on', name: 'on',
rawName: 'v-on.enter.exact', rawName: 'v-on.enter.exact',
arg: undefined, arg: undefined,
modifiers: ['enter', 'exact'], modifiers: [
{
constType: 3,
content: 'enter',
isStatic: true,
loc: {
end: {
column: 16,
line: 1,
offset: 15,
},
source: 'enter',
start: {
column: 11,
line: 1,
offset: 10,
},
},
type: 4,
},
{
constType: 3,
content: 'exact',
isStatic: true,
loc: {
end: {
column: 22,
line: 1,
offset: 21,
},
source: 'exact',
start: {
column: 17,
line: 1,
offset: 16,
},
},
type: 4,
},
],
exp: undefined, exp: undefined,
loc: { loc: {
start: { offset: 5, line: 1, column: 6 }, start: { offset: 5, line: 1, column: 6 },
@ -1406,7 +1465,46 @@ describe('compiler: parse', () => {
source: 'click', source: 'click',
}, },
}, },
modifiers: ['enter', 'exact'], modifiers: [
{
constType: 3,
content: 'enter',
isStatic: true,
loc: {
end: {
column: 22,
line: 1,
offset: 21,
},
source: 'enter',
start: {
column: 17,
line: 1,
offset: 16,
},
},
type: 4,
},
{
constType: 3,
content: 'exact',
isStatic: true,
loc: {
end: {
column: 28,
line: 1,
offset: 27,
},
source: 'exact',
start: {
column: 23,
line: 1,
offset: 22,
},
},
type: 4,
},
],
exp: undefined, exp: undefined,
loc: { loc: {
start: { offset: 5, line: 1, column: 6 }, start: { offset: 5, line: 1, column: 6 },
@ -1435,7 +1533,27 @@ describe('compiler: parse', () => {
source: '[a.b]', source: '[a.b]',
}, },
}, },
modifiers: ['camel'], modifiers: [
{
constType: 3,
content: 'camel',
isStatic: true,
loc: {
end: {
column: 22,
line: 1,
offset: 21,
},
source: 'camel',
start: {
column: 17,
line: 1,
offset: 16,
},
},
type: 4,
},
],
exp: undefined, exp: undefined,
loc: { loc: {
start: { offset: 5, line: 1, column: 6 }, start: { offset: 5, line: 1, column: 6 },
@ -1530,7 +1648,27 @@ describe('compiler: parse', () => {
source: 'a', source: 'a',
}, },
}, },
modifiers: ['prop'], modifiers: [
{
constType: 0,
content: 'prop',
isStatic: false,
loc: {
end: {
column: 1,
line: 1,
offset: 0,
},
source: '',
start: {
column: 1,
line: 1,
offset: 0,
},
},
type: 4,
},
],
exp: { exp: {
type: NodeTypes.SIMPLE_EXPRESSION, type: NodeTypes.SIMPLE_EXPRESSION,
content: 'b', content: 'b',
@ -1569,7 +1707,27 @@ describe('compiler: parse', () => {
source: 'a', source: 'a',
}, },
}, },
modifiers: ['sync'], modifiers: [
{
constType: 3,
content: 'sync',
isStatic: true,
loc: {
end: {
column: 13,
line: 1,
offset: 12,
},
source: 'sync',
start: {
column: 9,
line: 1,
offset: 8,
},
},
type: 4,
},
],
exp: { exp: {
type: NodeTypes.SIMPLE_EXPRESSION, type: NodeTypes.SIMPLE_EXPRESSION,
content: 'b', content: 'b',
@ -1649,7 +1807,27 @@ describe('compiler: parse', () => {
source: 'a', source: 'a',
}, },
}, },
modifiers: ['enter'], modifiers: [
{
constType: 3,
content: 'enter',
isStatic: true,
loc: {
end: {
column: 14,
line: 1,
offset: 13,
},
source: 'enter',
start: {
column: 9,
line: 1,
offset: 8,
},
},
type: 4,
},
],
exp: { exp: {
type: NodeTypes.SIMPLE_EXPRESSION, type: NodeTypes.SIMPLE_EXPRESSION,
content: 'b', content: 'b',
@ -1841,6 +2019,21 @@ describe('compiler: parse', () => {
children: [{ type: NodeTypes.TEXT, content: `{{ number ` }], children: [{ type: NodeTypes.TEXT, content: `{{ number ` }],
}, },
]) ])
const ast3 = baseParse(`<div v-pre><textarea>{{ foo </textarea></div>`, {
parseMode: 'html',
})
expect((ast3.children[0] as ElementNode).children).toMatchObject([
{
type: NodeTypes.ELEMENT,
children: [
{
type: NodeTypes.TEXT,
content: `{{ foo `,
},
],
},
])
}) })
test('self-closing v-pre', () => { test('self-closing v-pre', () => {
@ -2176,6 +2369,7 @@ describe('compiler: parse', () => {
test('should remove leading newline character immediately following the pre element start tag', () => { test('should remove leading newline character immediately following the pre element start tag', () => {
const ast = parse(`<pre>\n foo bar </pre>`, { const ast = parse(`<pre>\n foo bar </pre>`, {
isPreTag: tag => tag === 'pre', isPreTag: tag => tag === 'pre',
isIgnoreNewlineTag: tag => tag === 'pre',
}) })
expect(ast.children).toHaveLength(1) expect(ast.children).toHaveLength(1)
const preElement = ast.children[0] as ElementNode const preElement = ast.children[0] as ElementNode

View File

@ -3,6 +3,8 @@ import {
ElementTypes, ElementTypes,
Namespaces, Namespaces,
NodeTypes, NodeTypes,
type Property,
type SimpleExpressionNode,
type VNodeCall, type VNodeCall,
locStub, locStub,
} from '../src' } from '../src'
@ -22,7 +24,10 @@ const bracketsRE = /^\[|\]$/g
// e.g. // e.g.
// - createObjectMatcher({ 'foo': '[bar]' }) matches { foo: bar } // - createObjectMatcher({ 'foo': '[bar]' }) matches { foo: bar }
// - createObjectMatcher({ '[foo]': 'bar' }) matches { [foo]: "bar" } // - createObjectMatcher({ '[foo]': 'bar' }) matches { [foo]: "bar" }
export function createObjectMatcher(obj: Record<string, any>) { export function createObjectMatcher(obj: Record<string, any>): {
type: NodeTypes
properties: Partial<Property>[]
} {
return { return {
type: NodeTypes.JS_OBJECT_EXPRESSION, type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: Object.keys(obj).map(key => ({ properties: Object.keys(obj).map(key => ({
@ -31,7 +36,7 @@ export function createObjectMatcher(obj: Record<string, any>) {
type: NodeTypes.SIMPLE_EXPRESSION, type: NodeTypes.SIMPLE_EXPRESSION,
content: key.replace(bracketsRE, ''), content: key.replace(bracketsRE, ''),
isStatic: !leadingBracketRE.test(key), isStatic: !leadingBracketRE.test(key),
}, } as SimpleExpressionNode,
value: isString(obj[key]) value: isString(obj[key])
? { ? {
type: NodeTypes.SIMPLE_EXPRESSION, type: NodeTypes.SIMPLE_EXPRESSION,
@ -78,7 +83,7 @@ type Flags = PatchFlags | ShapeFlags
export function genFlagText( export function genFlagText(
flag: Flags | Flags[], flag: Flags | Flags[],
names: { [k: number]: string } = PatchFlagNames, names: { [k: number]: string } = PatchFlagNames,
) { ): string {
if (isArray(flag)) { if (isArray(flag)) {
let f = 0 let f = 0
flag.forEach(ff => { flag.forEach(ff => {

View File

@ -191,7 +191,7 @@ exports[`compiler: cacheStatic transform > prefixIdentifiers > hoist class with
const { createElementVNode: _createElementVNode } = _Vue const { createElementVNode: _createElementVNode } = _Vue
const _hoisted_1 = { const _hoisted_1 = {
class: /*#__PURE__*/_normalizeClass({ foo: true }) class: /*@__PURE__*/_normalizeClass({ foo: true })
} }
return function render(_ctx, _cache) { return function render(_ctx, _cache) {

View File

@ -13,6 +13,7 @@ import {
type ForNode, type ForNode,
type InterpolationNode, type InterpolationNode,
NodeTypes, NodeTypes,
type RootNode,
type SimpleExpressionNode, type SimpleExpressionNode,
} from '../../src/ast' } from '../../src/ast'
import { ErrorCodes } from '../../src/errors' import { ErrorCodes } from '../../src/errors'
@ -24,7 +25,10 @@ import { createObjectMatcher } from '../testUtils'
export function parseWithForTransform( export function parseWithForTransform(
template: string, template: string,
options: CompilerOptions = {}, options: CompilerOptions = {},
) { ): {
root: RootNode
node: ForNode & { codegenNode: ForCodegenNode }
} {
const ast = parse(template, options) const ast = parse(template, options)
transform(ast, { transform(ast, {
nodeTransforms: [ nodeTransforms: [

View File

@ -285,6 +285,21 @@ describe('compiler: transform v-on', () => {
}, },
], ],
}) })
const { node: node2 } = parseWithVOn(
`<div @click="(e: (number | string)[]) => foo(e)"/>`,
)
expect((node2.codegenNode as VNodeCall).props).toMatchObject({
properties: [
{
key: { content: `onClick` },
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `(e: (number | string)[]) => foo(e)`,
},
},
],
})
}) })
test('should NOT wrap as function if expression is already function expression (async)', () => { test('should NOT wrap as function if expression is already function expression (async)', () => {

View File

@ -1,5 +1,5 @@
import type { TransformContext } from '../src' import type { ExpressionNode, TransformContext } from '../src'
import type { Position } from '../src/ast' import { type Position, createSimpleExpression } from '../src/ast'
import { import {
advancePositionWithClone, advancePositionWithClone,
isMemberExpressionBrowser, isMemberExpressionBrowser,
@ -41,7 +41,8 @@ describe('advancePositionWithClone', () => {
}) })
describe('isMemberExpression', () => { describe('isMemberExpression', () => {
function commonAssertions(fn: (str: string) => boolean) { function commonAssertions(raw: (exp: ExpressionNode) => boolean) {
const fn = (str: string) => raw(createSimpleExpression(str))
// should work // should work
expect(fn('obj.foo')).toBe(true) expect(fn('obj.foo')).toBe(true)
expect(fn('obj[foo]')).toBe(true) expect(fn('obj[foo]')).toBe(true)
@ -78,13 +79,16 @@ describe('isMemberExpression', () => {
test('browser', () => { test('browser', () => {
commonAssertions(isMemberExpressionBrowser) commonAssertions(isMemberExpressionBrowser)
expect(isMemberExpressionBrowser('123[a]')).toBe(false) expect(isMemberExpressionBrowser(createSimpleExpression('123[a]'))).toBe(
false,
)
}) })
test('node', () => { test('node', () => {
const ctx = { expressionPlugins: ['typescript'] } as any as TransformContext const ctx = { expressionPlugins: ['typescript'] } as any as TransformContext
const fn = (str: string) => isMemberExpressionNode(str, ctx) const fn = (str: string) =>
commonAssertions(fn) isMemberExpressionNode(createSimpleExpression(str), ctx)
commonAssertions(exp => isMemberExpressionNode(exp, ctx))
// TS-specific checks // TS-specific checks
expect(fn('foo as string')).toBe(true) expect(fn('foo as string')).toBe(true)

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/compiler-core", "name": "@vue/compiler-core",
"version": "3.5.0-beta.1", "version": "3.5.11",
"description": "@vue/compiler-core", "description": "@vue/compiler-core",
"main": "index.js", "main": "index.js",
"module": "dist/compiler-core.esm-bundler.js", "module": "dist/compiler-core.esm-bundler.js",
@ -48,7 +48,7 @@
"dependencies": { "dependencies": {
"@babel/parser": "catalog:", "@babel/parser": "catalog:",
"@vue/shared": "workspace:*", "@vue/shared": "workspace:*",
"entities": "^5.0.0", "entities": "^4.5.0",
"estree-walker": "catalog:", "estree-walker": "catalog:",
"source-map-js": "catalog:" "source-map-js": "catalog:"
}, },

View File

@ -203,7 +203,7 @@ export interface DirectiveNode extends Node {
rawName?: string rawName?: string
exp: ExpressionNode | undefined exp: ExpressionNode | undefined
arg: ExpressionNode | undefined arg: ExpressionNode | undefined
modifiers: string[] modifiers: SimpleExpressionNode[]
/** /**
* optional property to cache the expression parse result for v-for * optional property to cache the expression parse result for v-for
*/ */

View File

@ -99,7 +99,7 @@ interface MappingItem {
name: string | null name: string | null
} }
const PURE_ANNOTATION = `/*#__PURE__*/` const PURE_ANNOTATION = `/*@__PURE__*/`
const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}` const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}`
@ -725,7 +725,7 @@ function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
!__BROWSER__ && genReturnStatement(node, context) !__BROWSER__ && genReturnStatement(node, context)
break break
/* istanbul ignore next */ /* v8 ignore start */
case NodeTypes.IF_BRANCH: case NodeTypes.IF_BRANCH:
// noop // noop
break break
@ -736,6 +736,7 @@ function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
const exhaustiveCheck: never = node const exhaustiveCheck: never = node
return exhaustiveCheck return exhaustiveCheck
} }
/* v8 ignore stop */
} }
} }

View File

@ -68,7 +68,7 @@ export function baseCompile(
): CodegenResult { ): CodegenResult {
const onError = options.onError || defaultOnError const onError = options.onError || defaultOnError
const isModuleMode = options.mode === 'module' const isModuleMode = options.mode === 'module'
/* istanbul ignore if */ /* v8 ignore start */
if (__BROWSER__) { if (__BROWSER__) {
if (options.prefixIdentifiers === true) { if (options.prefixIdentifiers === true) {
onError(createCompilerError(ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED)) onError(createCompilerError(ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED))
@ -76,6 +76,7 @@ export function baseCompile(
onError(createCompilerError(ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED)) onError(createCompilerError(ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED))
} }
} }
/* v8 ignore stop */
const prefixIdentifiers = const prefixIdentifiers =
!__BROWSER__ && (options.prefixIdentifiers === true || isModuleMode) !__BROWSER__ && (options.prefixIdentifiers === true || isModuleMode)

View File

@ -52,6 +52,11 @@ export interface ParserOptions
* e.g. elements that should preserve whitespace inside, e.g. `<pre>` * e.g. elements that should preserve whitespace inside, e.g. `<pre>`
*/ */
isPreTag?: (tag: string) => boolean isPreTag?: (tag: string) => boolean
/**
* Elements that should ignore the first newline token per parinsg spec
* e.g. `<textarea>` and `<pre>`
*/
isIgnoreNewlineTag?: (tag: string) => boolean
/** /**
* Platform-specific built-in components e.g. `<Transition>` * Platform-specific built-in components e.g. `<Transition>`
*/ */

View File

@ -44,7 +44,7 @@ import {
isSimpleIdentifier, isSimpleIdentifier,
isStaticArgOf, isStaticArgOf,
} from './utils' } from './utils'
import { decodeHTML } from 'entities/dist/decode.js' import { decodeHTML } from 'entities/lib/decode.js'
import { import {
type ParserOptions as BabelOptions, type ParserOptions as BabelOptions,
parse, parse,
@ -72,6 +72,7 @@ export const defaultParserOptions: MergedParserOptions = {
getNamespace: () => Namespaces.HTML, getNamespace: () => Namespaces.HTML,
isVoidTag: NO, isVoidTag: NO,
isPreTag: NO, isPreTag: NO,
isIgnoreNewlineTag: NO,
isCustomElement: NO, isCustomElement: NO,
onError: defaultOnError, onError: defaultOnError,
onWarn: defaultOnWarn, onWarn: defaultOnWarn,
@ -225,7 +226,7 @@ const tokenizer = new Tokenizer(stack, {
rawName: raw, rawName: raw,
exp: undefined, exp: undefined,
arg: undefined, arg: undefined,
modifiers: raw === '.' ? ['prop'] : [], modifiers: raw === '.' ? [createSimpleExpression('prop')] : [],
loc: getLoc(start), loc: getLoc(start),
} }
if (name === 'pre') { if (name === 'pre') {
@ -273,7 +274,8 @@ const tokenizer = new Tokenizer(stack, {
setLocEnd(arg.loc, end) setLocEnd(arg.loc, end)
} }
} else { } else {
;(currentProp as DirectiveNode).modifiers.push(mod) const exp = createSimpleExpression(mod, true, getLoc(start, end))
;(currentProp as DirectiveNode).modifiers.push(exp)
} }
}, },
@ -379,7 +381,9 @@ const tokenizer = new Tokenizer(stack, {
if ( if (
__COMPAT__ && __COMPAT__ &&
currentProp.name === 'bind' && currentProp.name === 'bind' &&
(syncIndex = currentProp.modifiers.indexOf('sync')) > -1 && (syncIndex = currentProp.modifiers.findIndex(
mod => mod.content === 'sync',
)) > -1 &&
checkCompatEnabled( checkCompatEnabled(
CompilerDeprecationTypes.COMPILER_V_BIND_SYNC, CompilerDeprecationTypes.COMPILER_V_BIND_SYNC,
currentOptions, currentOptions,
@ -630,7 +634,7 @@ function onCloseTag(el: ElementNode, end: number, isImplied = false) {
} }
// refine element type // refine element type
const { tag, ns } = el const { tag, ns, children } = el
if (!inVPre) { if (!inVPre) {
if (tag === 'slot') { if (tag === 'slot') {
el.tagType = ElementTypes.SLOT el.tagType = ElementTypes.SLOT
@ -643,8 +647,18 @@ function onCloseTag(el: ElementNode, end: number, isImplied = false) {
// whitespace management // whitespace management
if (!tokenizer.inRCDATA) { if (!tokenizer.inRCDATA) {
el.children = condenseWhitespace(el.children, el.tag) el.children = condenseWhitespace(children, tag)
} }
if (ns === Namespaces.HTML && currentOptions.isIgnoreNewlineTag(tag)) {
// remove leading newline for <textarea> and <pre> per html spec
// https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody
const first = children[0]
if (first && first.type === NodeTypes.TEXT) {
first.content = first.content.replace(/^\r?\n/, '')
}
}
if (ns === Namespaces.HTML && currentOptions.isPreTag(tag)) { if (ns === Namespaces.HTML && currentOptions.isPreTag(tag)) {
inPre-- inPre--
} }
@ -866,14 +880,6 @@ function condenseWhitespace(
} }
} }
} }
if (inPre && tag && currentOptions.isPreTag(tag)) {
// remove leading newline per html spec
// https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
const first = nodes[0]
if (first && first.type === NodeTypes.TEXT) {
first.content = first.content.replace(/^\r?\n/, '')
}
}
return removedWhitespace ? nodes.filter(Boolean) : nodes return removedWhitespace ? nodes.filter(Boolean) : nodes
} }

View File

@ -36,7 +36,7 @@ import {
EntityDecoder, EntityDecoder,
fromCodePoint, fromCodePoint,
htmlDecodeTree, htmlDecodeTree,
} from 'entities/dist/decode.js' } from 'entities/lib/decode.js'
export enum ParseMode { export enum ParseMode {
BASE, BASE,
@ -438,7 +438,7 @@ export default class Tokenizer {
// We have to parse entities in <title> and <textarea> tags. // We have to parse entities in <title> and <textarea> tags.
if (!__BROWSER__ && c === CharCodes.Amp) { if (!__BROWSER__ && c === CharCodes.Amp) {
this.startEntity() this.startEntity()
} else if (c === this.delimiterOpen[0]) { } else if (!this.inVPre && c === this.delimiterOpen[0]) {
// We also need to handle interpolation // We also need to handle interpolation
this.state = State.InterpolationOpen this.state = State.InterpolationOpen
this.delimiterIndex = 0 this.delimiterIndex = 0

View File

@ -23,7 +23,6 @@ import {
import { import {
EMPTY_OBJ, EMPTY_OBJ,
NOOP, NOOP,
PatchFlagNames,
PatchFlags, PatchFlags,
camelize, camelize,
capitalize, capitalize,
@ -222,7 +221,7 @@ export function createTransformContext(
return `_${helperNameMap[context.helper(name)]}` return `_${helperNameMap[context.helper(name)]}`
}, },
replaceNode(node) { replaceNode(node) {
/* istanbul ignore if */ /* v8 ignore start */
if (__DEV__) { if (__DEV__) {
if (!context.currentNode) { if (!context.currentNode) {
throw new Error(`Node being replaced is already removed.`) throw new Error(`Node being replaced is already removed.`)
@ -231,9 +230,11 @@ export function createTransformContext(
throw new Error(`Cannot replace root node.`) throw new Error(`Cannot replace root node.`)
} }
} }
/* v8 ignore stop */
context.parent!.children[context.childIndex] = context.currentNode = node context.parent!.children[context.childIndex] = context.currentNode = node
}, },
removeNode(node) { removeNode(node) {
/* v8 ignore next 3 */
if (__DEV__ && !context.parent) { if (__DEV__ && !context.parent) {
throw new Error(`Cannot remove root node.`) throw new Error(`Cannot remove root node.`)
} }
@ -243,7 +244,7 @@ export function createTransformContext(
: context.currentNode : context.currentNode
? context.childIndex ? context.childIndex
: -1 : -1
/* istanbul ignore if */ /* v8 ignore next 3 */
if (__DEV__ && removalIndex < 0) { if (__DEV__ && removalIndex < 0) {
throw new Error(`node being removed is not a child of current parent`) throw new Error(`node being removed is not a child of current parent`)
} }
@ -373,7 +374,6 @@ function createRootCodegen(root: RootNode, context: TransformContext) {
} else if (children.length > 1) { } else if (children.length > 1) {
// root has multiple nodes - return a fragment block. // root has multiple nodes - return a fragment block.
let patchFlag = PatchFlags.STABLE_FRAGMENT let patchFlag = PatchFlags.STABLE_FRAGMENT
let patchFlagText = PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
// check if the fragment actually contains a single valid child with // check if the fragment actually contains a single valid child with
// the rest being comments // the rest being comments
if ( if (
@ -381,7 +381,6 @@ function createRootCodegen(root: RootNode, context: TransformContext) {
children.filter(c => c.type !== NodeTypes.COMMENT).length === 1 children.filter(c => c.type !== NodeTypes.COMMENT).length === 1
) { ) {
patchFlag |= PatchFlags.DEV_ROOT_FRAGMENT patchFlag |= PatchFlags.DEV_ROOT_FRAGMENT
patchFlagText += `, ${PatchFlagNames[PatchFlags.DEV_ROOT_FRAGMENT]}`
} }
root.codegenNode = createVNodeCall( root.codegenNode = createVNodeCall(
context, context,

View File

@ -665,7 +665,7 @@ export function buildProps(
} }
// force hydration for v-bind with .prop modifier // force hydration for v-bind with .prop modifier
if (isVBind && modifiers.includes('prop')) { if (isVBind && modifiers.some(mod => mod.content === 'prop')) {
patchFlag |= PatchFlags.NEED_HYDRATION patchFlag |= PatchFlags.NEED_HYDRATION
} }

View File

@ -44,7 +44,7 @@ import { parseExpression } from '@babel/parser'
import { IS_REF, UNREF } from '../runtimeHelpers' import { IS_REF, UNREF } from '../runtimeHelpers'
import { BindingTypes } from '../options' import { BindingTypes } from '../options'
const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this') const isLiteralWhitelisted = /*@__PURE__*/ makeMap('true,false,null,this')
export const transformExpression: NodeTransform = (node, context) => { export const transformExpression: NodeTransform = (node, context) => {
if (node.type === NodeTypes.INTERPOLATION) { if (node.type === NodeTypes.INTERPOLATION) {

View File

@ -69,7 +69,7 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => {
} }
// .sync is replaced by v-model:arg // .sync is replaced by v-model:arg
if (modifiers.includes('camel')) { if (modifiers.some(mod => mod.content === 'camel')) {
if (arg.type === NodeTypes.SIMPLE_EXPRESSION) { if (arg.type === NodeTypes.SIMPLE_EXPRESSION) {
if (arg.isStatic) { if (arg.isStatic) {
arg.content = camelize(arg.content) arg.content = camelize(arg.content)
@ -83,10 +83,10 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => {
} }
if (!context.inSSR) { if (!context.inSSR) {
if (modifiers.includes('prop')) { if (modifiers.some(mod => mod.content === 'prop')) {
injectPrefix(arg, '.') injectPrefix(arg, '.')
} }
if (modifiers.includes('attr')) { if (modifiers.some(mod => mod.content === 'attr')) {
injectPrefix(arg, '^') injectPrefix(arg, '^')
} }
} }

View File

@ -32,7 +32,7 @@ import { processExpression } from './transformExpression'
import { validateBrowserExpression } from '../validateExpression' import { validateBrowserExpression } from '../validateExpression'
import { CREATE_COMMENT, FRAGMENT } from '../runtimeHelpers' import { CREATE_COMMENT, FRAGMENT } from '../runtimeHelpers'
import { findDir, findProp, getMemoedVNodeCall, injectProp } from '../utils' import { findDir, findProp, getMemoedVNodeCall, injectProp } from '../utils'
import { PatchFlagNames, PatchFlags } from '@vue/shared' import { PatchFlags } from '@vue/shared'
export const transformIf: NodeTransform = createStructuralDirectiveTransform( export const transformIf: NodeTransform = createStructuralDirectiveTransform(
/^(if|else|else-if)$/, /^(if|else|else-if)$/,
@ -264,7 +264,6 @@ function createChildrenCodegenNode(
return vnodeCall return vnodeCall
} else { } else {
let patchFlag = PatchFlags.STABLE_FRAGMENT let patchFlag = PatchFlags.STABLE_FRAGMENT
let patchFlagText = PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
// check if the fragment actually contains a single valid child with // check if the fragment actually contains a single valid child with
// the rest being comments // the rest being comments
if ( if (
@ -273,7 +272,6 @@ function createChildrenCodegenNode(
children.filter(c => c.type !== NodeTypes.COMMENT).length === 1 children.filter(c => c.type !== NodeTypes.COMMENT).length === 1
) { ) {
patchFlag |= PatchFlags.DEV_ROOT_FRAGMENT patchFlag |= PatchFlags.DEV_ROOT_FRAGMENT
patchFlagText += `, ${PatchFlagNames[PatchFlags.DEV_ROOT_FRAGMENT]}`
} }
return createVNodeCall( return createVNodeCall(

View File

@ -31,7 +31,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
// we assume v-model directives are always parsed // we assume v-model directives are always parsed
// (not artificially created by a transform) // (not artificially created by a transform)
const rawExp = exp.loc.source const rawExp = exp.loc.source.trim()
const expString = const expString =
exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : rawExp exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : rawExp
@ -55,10 +55,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
bindingType === BindingTypes.SETUP_REF || bindingType === BindingTypes.SETUP_REF ||
bindingType === BindingTypes.SETUP_MAYBE_REF) bindingType === BindingTypes.SETUP_MAYBE_REF)
if ( if (!expString.trim() || (!isMemberExpression(exp, context) && !maybeRef)) {
!expString.trim() ||
(!isMemberExpression(expString, context) && !maybeRef)
) {
context.onError( context.onError(
createCompilerError(ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION, exp.loc), createCompilerError(ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION, exp.loc),
) )
@ -134,6 +131,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
// modelModifiers: { foo: true, "bar-baz": true } // modelModifiers: { foo: true, "bar-baz": true }
if (dir.modifiers.length && node.tagType === ElementTypes.COMPONENT) { if (dir.modifiers.length && node.tagType === ElementTypes.COMPONENT) {
const modifiers = dir.modifiers const modifiers = dir.modifiers
.map(m => m.content)
.map(m => (isSimpleIdentifier(m) ? m : JSON.stringify(m)) + `: true`) .map(m => (isSimpleIdentifier(m) ? m : JSON.stringify(m)) + `: true`)
.join(`, `) .join(`, `)
const modifiersKey = arg const modifiersKey = arg

View File

@ -13,12 +13,9 @@ import { camelize, toHandlerKey } from '@vue/shared'
import { ErrorCodes, createCompilerError } from '../errors' import { ErrorCodes, createCompilerError } from '../errors'
import { processExpression } from './transformExpression' import { processExpression } from './transformExpression'
import { validateBrowserExpression } from '../validateExpression' import { validateBrowserExpression } from '../validateExpression'
import { hasScopeRef, isMemberExpression } from '../utils' import { hasScopeRef, isFnExpression, isMemberExpression } from '../utils'
import { TO_HANDLER_KEY } from '../runtimeHelpers' import { TO_HANDLER_KEY } from '../runtimeHelpers'
const fnExpRE =
/^\s*(async\s*)?(\([^)]*?\)|[\w$_]+)\s*(:[^=]+)?=>|^\s*(async\s+)?function(?:\s+[\w$]+)?\s*\(/
export interface VOnDirectiveNode extends DirectiveNode { export interface VOnDirectiveNode extends DirectiveNode {
// v-on without arg is handled directly in ./transformElements.ts due to it affecting // v-on without arg is handled directly in ./transformElements.ts due to it affecting
// codegen for the entire props object. This transform here is only for v-on // codegen for the entire props object. This transform here is only for v-on
@ -84,8 +81,8 @@ export const transformOn: DirectiveTransform = (
} }
let shouldCache: boolean = context.cacheHandlers && !exp && !context.inVOnce let shouldCache: boolean = context.cacheHandlers && !exp && !context.inVOnce
if (exp) { if (exp) {
const isMemberExp = isMemberExpression(exp.content, context) const isMemberExp = isMemberExpression(exp, context)
const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content)) const isInlineStatement = !(isMemberExp || isFnExpression(exp, context))
const hasMultipleStatements = exp.content.includes(`;`) const hasMultipleStatements = exp.content.includes(`;`)
// process the expression since it's been skipped // process the expression since it's been skipped

View File

@ -40,7 +40,7 @@ import {
import { NOOP, isObject, isString } from '@vue/shared' import { NOOP, isObject, isString } from '@vue/shared'
import type { PropsExpression } from './transforms/transformElement' import type { PropsExpression } from './transforms/transformElement'
import { parseExpression } from '@babel/parser' import { parseExpression } from '@babel/parser'
import type { Expression } from '@babel/types' import type { Expression, Node } from '@babel/types'
import { unwrapTSNode } from './babelUtils' import { unwrapTSNode } from './babelUtils'
export const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode => export const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
@ -78,15 +78,20 @@ const validFirstIdentCharRE = /[A-Za-z_$\xA0-\uFFFF]/
const validIdentCharRE = /[\.\?\w$\xA0-\uFFFF]/ const validIdentCharRE = /[\.\?\w$\xA0-\uFFFF]/
const whitespaceRE = /\s+[.[]\s*|\s*[.[]\s+/g const whitespaceRE = /\s+[.[]\s*|\s*[.[]\s+/g
const getExpSource = (exp: ExpressionNode): string =>
exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : exp.loc.source
/** /**
* Simple lexer to check if an expression is a member expression. This is * Simple lexer to check if an expression is a member expression. This is
* lax and only checks validity at the root level (i.e. does not validate exps * lax and only checks validity at the root level (i.e. does not validate exps
* inside square brackets), but it's ok since these are only used on template * inside square brackets), but it's ok since these are only used on template
* expressions and false positives are invalid expressions in the first place. * expressions and false positives are invalid expressions in the first place.
*/ */
export const isMemberExpressionBrowser = (path: string): boolean => { export const isMemberExpressionBrowser = (exp: ExpressionNode): boolean => {
// remove whitespaces around . or [ first // remove whitespaces around . or [ first
path = path.trim().replace(whitespaceRE, s => s.trim()) const path = getExpSource(exp)
.trim()
.replace(whitespaceRE, s => s.trim())
let state = MemberExpLexState.inMemberExp let state = MemberExpLexState.inMemberExp
let stateStack: MemberExpLexState[] = [] let stateStack: MemberExpLexState[] = []
@ -154,15 +159,19 @@ export const isMemberExpressionBrowser = (path: string): boolean => {
} }
export const isMemberExpressionNode: ( export const isMemberExpressionNode: (
path: string, exp: ExpressionNode,
context: TransformContext, context: TransformContext,
) => boolean = __BROWSER__ ) => boolean = __BROWSER__
? (NOOP as any) ? (NOOP as any)
: (path: string, context: TransformContext): boolean => { : (exp, context) => {
try { try {
let ret: Expression = parseExpression(path, { let ret: Node =
plugins: context.expressionPlugins, exp.ast ||
}) parseExpression(getExpSource(exp), {
plugins: context.expressionPlugins
? [...context.expressionPlugins, 'typescript']
: ['typescript'],
})
ret = unwrapTSNode(ret) as Expression ret = unwrapTSNode(ret) as Expression
return ( return (
ret.type === 'MemberExpression' || ret.type === 'MemberExpression' ||
@ -175,10 +184,52 @@ export const isMemberExpressionNode: (
} }
export const isMemberExpression: ( export const isMemberExpression: (
path: string, exp: ExpressionNode,
context: TransformContext, context: TransformContext,
) => boolean = __BROWSER__ ? isMemberExpressionBrowser : isMemberExpressionNode ) => boolean = __BROWSER__ ? isMemberExpressionBrowser : isMemberExpressionNode
const fnExpRE =
/^\s*(async\s*)?(\([^)]*?\)|[\w$_]+)\s*(:[^=]+)?=>|^\s*(async\s+)?function(?:\s+[\w$]+)?\s*\(/
export const isFnExpressionBrowser: (exp: ExpressionNode) => boolean = exp =>
fnExpRE.test(getExpSource(exp))
export const isFnExpressionNode: (
exp: ExpressionNode,
context: TransformContext,
) => boolean = __BROWSER__
? (NOOP as any)
: (exp, context) => {
try {
let ret: Node =
exp.ast ||
parseExpression(getExpSource(exp), {
plugins: context.expressionPlugins
? [...context.expressionPlugins, 'typescript']
: ['typescript'],
})
// parser may parse the exp as statements when it contains semicolons
if (ret.type === 'Program') {
ret = ret.body[0]
if (ret.type === 'ExpressionStatement') {
ret = ret.expression
}
}
ret = unwrapTSNode(ret) as Expression
return (
ret.type === 'FunctionExpression' ||
ret.type === 'ArrowFunctionExpression'
)
} catch (e) {
return false
}
}
export const isFnExpression: (
exp: ExpressionNode,
context: TransformContext,
) => boolean = __BROWSER__ ? isFnExpressionBrowser : isFnExpressionNode
export function advancePositionWithClone( export function advancePositionWithClone(
pos: Position, pos: Position,
source: string, source: string,
@ -222,7 +273,7 @@ export function advancePositionWithMutation(
} }
export function assert(condition: boolean, msg?: string): void { export function assert(condition: boolean, msg?: string): void {
/* istanbul ignore if */ /* v8 ignore next 3 */
if (!condition) { if (!condition) {
throw new Error(msg || `unexpected compiler condition`) throw new Error(msg || `unexpected compiler condition`)
} }

View File

@ -32,6 +32,22 @@ describe('DOM parser', () => {
}) })
}) })
test('<textarea> should remove leading newline', () => {
const ast = parse('<textarea>\nhello</textarea>', parserOptions)
const element = ast.children[0] as ElementNode
const text = element.children[0] as TextNode
expect(element.children.length).toBe(1)
expect(text).toStrictEqual({
type: NodeTypes.TEXT,
content: 'hello',
loc: {
start: { offset: 10, line: 1, column: 11 },
end: { offset: 16, line: 2, column: 6 },
source: '\nhello',
},
})
})
test('should not treat Uppercase component as special tag', () => { test('should not treat Uppercase component as special tag', () => {
const ast = parse( const ast = parse(
'<TextArea>some<div>text</div>and<!--comment--></TextArea>', '<TextArea>some<div>text</div>and<!--comment--></TextArea>',

View File

@ -1,5 +1,17 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`stringify static html > eligible content (elements > 20) + non-eligible content 1`] = `
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createStaticVNode("<span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>", 20),
_createElementVNode("div", { key: "1" }, "1", -1 /* HOISTED */),
_createStaticVNode("<span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>", 20)
])))
}"
`;
exports[`stringify static html > escape 1`] = ` exports[`stringify static html > escape 1`] = `
"const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue "const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue

View File

@ -389,6 +389,24 @@ describe('stringify static html', () => {
]) ])
}) })
test('should stringify mathML', () => {
const math = `<math xmlns="http://www.w3.org/1998/Math/MathML">`
const repeated = `<ms>1</ms>`
const { ast } = compileWithStringify(
`<div>${math}${repeat(
repeated,
StringifyThresholds.NODE_COUNT,
)}</math></div>`,
)
expect(ast.cached).toMatchObject([
cachedArrayStaticNodeMatcher(
`${math}${repeat(repeated, StringifyThresholds.NODE_COUNT)}</math>`,
1,
),
])
})
// #5439 // #5439
test('stringify v-html', () => { test('stringify v-html', () => {
const { code } = compileWithStringify(` const { code } = compileWithStringify(`
@ -451,4 +469,18 @@ describe('stringify static html', () => {
expect(ast.cached).toMatchObject([cachedArrayBailedMatcher()]) expect(ast.cached).toMatchObject([cachedArrayBailedMatcher()])
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })
test('eligible content (elements > 20) + non-eligible content', () => {
const { code } = compileWithStringify(
`<div>${repeat(
`<span/>`,
StringifyThresholds.NODE_COUNT,
)}<div key="1">1</div>${repeat(
`<span/>`,
StringifyThresholds.NODE_COUNT,
)}</div>`,
)
expect(code).toMatchSnapshot()
})
}) })

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/compiler-dom", "name": "@vue/compiler-dom",
"version": "3.5.0-beta.1", "version": "3.5.11",
"description": "@vue/compiler-dom", "description": "@vue/compiler-dom",
"main": "index.js", "main": "index.js",
"module": "dist/compiler-dom.esm-bundler.js", "module": "dist/compiler-dom.esm-bundler.js",

View File

@ -8,6 +8,7 @@ export const parserOptions: ParserOptions = {
isVoidTag, isVoidTag,
isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag) || isMathMLTag(tag), isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag) || isMathMLTag(tag),
isPreTag: tag => tag === 'pre', isPreTag: tag => tag === 'pre',
isIgnoreNewlineTag: tag => tag === 'pre' || tag === 'textarea',
decodeEntities: __BROWSER__ ? decodeHtmlBrowser : undefined, decodeEntities: __BROWSER__ ? decodeHtmlBrowser : undefined,
isBuiltInComponent: tag => { isBuiltInComponent: tag => {

View File

@ -16,8 +16,6 @@ import {
type TemplateChildNode, type TemplateChildNode,
type TextCallNode, type TextCallNode,
type TransformContext, type TransformContext,
type VNodeCall,
createArrayExpression,
createCallExpression, createCallExpression,
isStaticArgOf, isStaticArgOf,
} from '@vue/compiler-core' } from '@vue/compiler-core'
@ -26,6 +24,7 @@ import {
isArray, isArray,
isBooleanAttr, isBooleanAttr,
isKnownHtmlAttr, isKnownHtmlAttr,
isKnownMathMLAttr,
isKnownSvgAttr, isKnownSvgAttr,
isString, isString,
isSymbol, isSymbol,
@ -106,15 +105,23 @@ export const stringifyStatic: HoistTransform = (children, context, parent) => {
String(currentChunk.length), String(currentChunk.length),
]) ])
const deleteCount = currentChunk.length - 1
if (isParentCached) { if (isParentCached) {
;((parent.codegenNode as VNodeCall).children as CacheExpression).value = // if the parent is cached, then `children` is also the value of the
createArrayExpression([staticCall]) // CacheExpression. Just replace the corresponding range in the cached
// list with staticCall.
children.splice(
currentIndex - currentChunk.length,
currentChunk.length,
// @ts-expect-error
staticCall,
)
} else { } else {
// replace the first node's hoisted expression with the static vnode call // replace the first node's hoisted expression with the static vnode call
;(currentChunk[0].codegenNode as CacheExpression).value = staticCall ;(currentChunk[0].codegenNode as CacheExpression).value = staticCall
if (currentChunk.length > 1) { if (currentChunk.length > 1) {
// remove merged nodes from children // remove merged nodes from children
const deleteCount = currentChunk.length - 1
children.splice(currentIndex - currentChunk.length + 1, deleteCount) children.splice(currentIndex - currentChunk.length + 1, deleteCount)
// also adjust index for the remaining cache items // also adjust index for the remaining cache items
const cacheIndex = context.cached.indexOf( const cacheIndex = context.cached.indexOf(
@ -128,9 +135,9 @@ export const stringifyStatic: HoistTransform = (children, context, parent) => {
} }
context.cached.splice(cacheIndex - deleteCount + 1, deleteCount) context.cached.splice(cacheIndex - deleteCount + 1, deleteCount)
} }
return deleteCount
} }
} }
return deleteCount
} }
return 0 return 0
} }
@ -184,11 +191,13 @@ const isStringifiableAttr = (name: string, ns: Namespaces) => {
? isKnownHtmlAttr(name) ? isKnownHtmlAttr(name)
: ns === Namespaces.SVG : ns === Namespaces.SVG
? isKnownSvgAttr(name) ? isKnownSvgAttr(name)
: false) || dataAriaRE.test(name) : ns === Namespaces.MATH_ML
? isKnownMathMLAttr(name)
: false) || dataAriaRE.test(name)
) )
} }
const isNonStringifiable = /*#__PURE__*/ makeMap( const isNonStringifiable = /*@__PURE__*/ makeMap(
`caption,thead,tr,th,tbody,td,tfoot,colgroup,col`, `caption,thead,tr,th,tbody,td,tfoot,colgroup,col`,
) )

View File

@ -17,8 +17,8 @@ import {
import { V_ON_WITH_KEYS, V_ON_WITH_MODIFIERS } from '../runtimeHelpers' import { V_ON_WITH_KEYS, V_ON_WITH_MODIFIERS } from '../runtimeHelpers'
import { capitalize, makeMap } from '@vue/shared' import { capitalize, makeMap } from '@vue/shared'
const isEventOptionModifier = /*#__PURE__*/ makeMap(`passive,once,capture`) const isEventOptionModifier = /*@__PURE__*/ makeMap(`passive,once,capture`)
const isNonKeyModifier = /*#__PURE__*/ makeMap( const isNonKeyModifier = /*@__PURE__*/ makeMap(
// event propagation management // event propagation management
`stop,prevent,self,` + `stop,prevent,self,` +
// system modifiers + exact // system modifiers + exact
@ -27,15 +27,12 @@ const isNonKeyModifier = /*#__PURE__*/ makeMap(
`middle`, `middle`,
) )
// left & right could be mouse or key modifiers based on event type // left & right could be mouse or key modifiers based on event type
const maybeKeyModifier = /*#__PURE__*/ makeMap('left,right') const maybeKeyModifier = /*@__PURE__*/ makeMap('left,right')
const isKeyboardEvent = /*#__PURE__*/ makeMap( const isKeyboardEvent = /*@__PURE__*/ makeMap(`onkeyup,onkeydown,onkeypress`)
`onkeyup,onkeydown,onkeypress`,
true,
)
const resolveModifiers = ( const resolveModifiers = (
key: ExpressionNode, key: ExpressionNode,
modifiers: string[], modifiers: SimpleExpressionNode[],
context: TransformContext, context: TransformContext,
loc: SourceLocation, loc: SourceLocation,
) => { ) => {
@ -44,7 +41,7 @@ const resolveModifiers = (
const eventOptionModifiers = [] const eventOptionModifiers = []
for (let i = 0; i < modifiers.length; i++) { for (let i = 0; i < modifiers.length; i++) {
const modifier = modifiers[i] const modifier = modifiers[i].content
if ( if (
__COMPAT__ && __COMPAT__ &&
@ -64,7 +61,9 @@ const resolveModifiers = (
// runtimeModifiers: modifiers that needs runtime guards // runtimeModifiers: modifiers that needs runtime guards
if (maybeKeyModifier(modifier)) { if (maybeKeyModifier(modifier)) {
if (isStaticExp(key)) { if (isStaticExp(key)) {
if (isKeyboardEvent((key as SimpleExpressionNode).content)) { if (
isKeyboardEvent((key as SimpleExpressionNode).content.toLowerCase())
) {
keyModifiers.push(modifier) keyModifiers.push(modifier)
} else { } else {
nonKeyModifiers.push(modifier) nonKeyModifiers.push(modifier)
@ -133,7 +132,7 @@ export const transformOn: DirectiveTransform = (dir, node, context) => {
if ( if (
keyModifiers.length && keyModifiers.length &&
// if event name is dynamic, always wrap with keys guard // if event name is dynamic, always wrap with keys guard
(!isStaticExp(key) || isKeyboardEvent(key.content)) (!isStaticExp(key) || isKeyboardEvent(key.content.toLowerCase()))
) { ) {
handlerExp = createCallExpression(context.helper(V_ON_WITH_KEYS), [ handlerExp = createCallExpression(context.helper(V_ON_WITH_KEYS), [
handlerExp, handlerExp,

View File

@ -20,7 +20,7 @@ exports[`SFC analyze <script> bindings > auto name inference > do not overwrite
name: 'Baz' name: 'Baz'
}) })
export default /*#__PURE__*/Object.assign(__default__, { export default /*@__PURE__*/Object.assign(__default__, {
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
const a = 1 const a = 1
@ -36,7 +36,7 @@ exports[`SFC analyze <script> bindings > auto name inference > do not overwrite
name: 'Baz' name: 'Baz'
} }
export default /*#__PURE__*/Object.assign(__default__, { export default /*@__PURE__*/Object.assign(__default__, {
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
const a = 1 const a = 1
@ -53,7 +53,7 @@ exports[`SFC compile <script setup> > <script> and <script setup> co-usage > exp
} }
const __default__ = fn(); const __default__ = fn();
export default /*#__PURE__*/Object.assign(__default__, { export default /*@__PURE__*/Object.assign(__default__, {
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -91,7 +91,7 @@ exports[`SFC compile <script setup> > <script> and <script setup> co-usage > scr
const __default__ = {} const __default__ = {}
export default /*#__PURE__*/Object.assign(__default__, { export default /*@__PURE__*/Object.assign(__default__, {
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -110,7 +110,7 @@ exports[`SFC compile <script setup> > <script> and <script setup> co-usage > scr
const __default__ = {} const __default__ = {}
export default /*#__PURE__*/Object.assign(__default__, { export default /*@__PURE__*/Object.assign(__default__, {
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -131,7 +131,7 @@ import { x } from './x'
} }
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
...__default__, ...__default__,
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -154,7 +154,7 @@ exports[`SFC compile <script setup> > <script> and <script setup> co-usage > scr
const __default__ = def const __default__ = def
export default /*#__PURE__*/Object.assign(__default__, { export default /*@__PURE__*/Object.assign(__default__, {
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -174,7 +174,7 @@ exports[`SFC compile <script setup> > <script> and <script setup> co-usage > spa
some:'option' some:'option'
} }
export default /*#__PURE__*/Object.assign(__default__, { export default /*@__PURE__*/Object.assign(__default__, {
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -194,7 +194,7 @@ exports[`SFC compile <script setup> > <script> and <script setup> co-usage > spa
some:'option' some:'option'
} }
export default /*#__PURE__*/Object.assign(__default__, { export default /*@__PURE__*/Object.assign(__default__, {
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -880,11 +880,13 @@ export default {
const count = ref(0) const count = ref(0)
const style = { color: 'red' } const style = { color: 'red' }
const height = ref(0)
return (_ctx, _push, _parent, _attrs) => { return (_ctx, _push, _parent, _attrs) => {
const _cssVars = { style: { const _cssVars = { style: {
"--xxxxxxxx-count": (count.value), "--xxxxxxxx-count": (count.value),
"--xxxxxxxx-style\\\\.color": (style.color) "--xxxxxxxx-style\\\\.color": (style.color),
"--xxxxxxxx-height\\\\ \\\\+\\\\ \\\\\\"px\\\\\\"": (height.value + "px")
}} }}
_push(\`<!--[--><div\${ _push(\`<!--[--><div\${
_ssrRenderAttrs(_cssVars) _ssrRenderAttrs(_cssVars)
@ -1082,6 +1084,29 @@ return (_ctx, _cache) => {
}" }"
`; `;
exports[`SFC compile <script setup> > inlineTemplate mode > v-model w/ newlines codegen 1`] = `
"import { unref as _unref, isRef as _isRef, vModelText as _vModelText, withDirectives as _withDirectives, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export default {
setup(__props) {
const count = ref(0)
return (_ctx, _cache) => {
return _withDirectives((_openBlock(), _createElementBlock("input", {
"onUpdate:modelValue": _cache[0] || (_cache[0] = $event => (_isRef(count) ? (count).value = $event : null))
}, null, 512 /* NEED_PATCH */)), [
[_vModelText,
_unref(count)
]
])
}
}
}"
`;
exports[`SFC compile <script setup> > inlineTemplate mode > with defineExpose() 1`] = ` exports[`SFC compile <script setup> > inlineTemplate mode > with defineExpose() 1`] = `
" "
export default { export default {
@ -1140,7 +1165,7 @@ exports[`SFC compile <script setup> > with TypeScript > const Enum 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
const enum Foo { A = 123 } const enum Foo { A = 123 }
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -1156,7 +1181,7 @@ exports[`SFC compile <script setup> > with TypeScript > hoist type declarations
export interface Foo {} export interface Foo {}
type Bar = {} type Bar = {}
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -1172,7 +1197,7 @@ exports[`SFC compile <script setup> > with TypeScript > import type 1`] = `
import type { Foo } from './main.ts' import type { Foo } from './main.ts'
import { type Bar, Baz } from './main.ts' import { type Bar, Baz } from './main.ts'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -1187,7 +1212,7 @@ exports[`SFC compile <script setup> > with TypeScript > runtime Enum 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
enum Foo { A = 123 } enum Foo { A = 123 }
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -1205,7 +1230,7 @@ exports[`SFC compile <script setup> > with TypeScript > runtime Enum in normal s
const enum C { C = "C" } const enum C { C = "C" }
enum B { B = "B" } enum B { B = "B" }
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -1221,7 +1246,7 @@ exports[`SFC compile <script setup> > with TypeScript > with generic attribute 1
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
type Bar = {} type Bar = {}
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -1250,7 +1275,7 @@ exports[`SFC genDefaultAs > <script setup> only w/ ts 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
const a = 1 const a = 1
const _sfc_ = /*#__PURE__*/_defineComponent({ const _sfc_ = /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -1265,7 +1290,7 @@ exports[`SFC genDefaultAs > <script> + <script setup> 1`] = `
" "
const __default__ = {} const __default__ = {}
const _sfc_ = /*#__PURE__*/Object.assign(__default__, { const _sfc_ = /*@__PURE__*/Object.assign(__default__, {
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -1281,7 +1306,7 @@ exports[`SFC genDefaultAs > <script> + <script setup> 2`] = `
" "
const __default__ = {} const __default__ = {}
const _sfc_ = /*#__PURE__*/Object.assign(__default__, { const _sfc_ = /*@__PURE__*/Object.assign(__default__, {
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -1298,7 +1323,7 @@ exports[`SFC genDefaultAs > <script> + <script setup> w/ ts 1`] = `
const __default__ = {} const __default__ = {}
const _sfc_ = /*#__PURE__*/_defineComponent({ const _sfc_ = /*@__PURE__*/_defineComponent({
...__default__, ...__default__,
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();

View File

@ -472,6 +472,23 @@ describe('SFC compile <script setup>', () => {
assertCode(content) assertCode(content)
}) })
test('v-model w/ newlines codegen', () => {
const { content } = compile(
`<script setup>
const count = ref(0)
</script>
<template>
<input v-model="
count
">
</template>
`,
{ inlineTemplate: true },
)
expect(content).toMatch(`_isRef(count) ? (count).value = $event : null`)
assertCode(content)
})
test('v-model should not generate ref assignment code for non-setup bindings', () => { test('v-model should not generate ref assignment code for non-setup bindings', () => {
const { content } = compile( const { content } = compile(
`<script setup> `<script setup>
@ -606,6 +623,7 @@ describe('SFC compile <script setup>', () => {
import { ref } from 'vue' import { ref } from 'vue'
const count = ref(0) const count = ref(0)
const style = { color: 'red' } const style = { color: 'red' }
const height = ref(0)
</script> </script>
<template> <template>
<div>{{ count }}</div> <div>{{ count }}</div>
@ -614,6 +632,7 @@ describe('SFC compile <script setup>', () => {
<style> <style>
div { color: v-bind(count) } div { color: v-bind(count) }
span { color: v-bind(style.color) } span { color: v-bind(style.color) }
span { color: v-bind(height + "px") }
</style> </style>
`, `,
{ {
@ -629,6 +648,9 @@ describe('SFC compile <script setup>', () => {
expect(content).not.toMatch(`useCssVars`) expect(content).not.toMatch(`useCssVars`)
expect(content).toMatch(`"--${mockId}-count": (count.value)`) expect(content).toMatch(`"--${mockId}-count": (count.value)`)
expect(content).toMatch(`"--${mockId}-style\\\\.color": (style.color)`) expect(content).toMatch(`"--${mockId}-style\\\\.color": (style.color)`)
expect(content).toMatch(
`"--${mockId}-height\\\\ \\\\+\\\\ \\\\\\"px\\\\\\"": (height.value + "px")`,
)
assertCode(content) assertCode(content)
}) })
@ -1352,7 +1374,7 @@ describe('SFC genDefaultAs', () => {
) )
expect(content).not.toMatch('export default') expect(content).not.toMatch('export default')
expect(content).toMatch( expect(content).toMatch(
`const _sfc_ = /*#__PURE__*/Object.assign(__default__`, `const _sfc_ = /*@__PURE__*/Object.assign(__default__`,
) )
assertCode(content) assertCode(content)
}) })
@ -1371,7 +1393,7 @@ describe('SFC genDefaultAs', () => {
) )
expect(content).not.toMatch('export default') expect(content).not.toMatch('export default')
expect(content).toMatch( expect(content).toMatch(
`const _sfc_ = /*#__PURE__*/Object.assign(__default__`, `const _sfc_ = /*@__PURE__*/Object.assign(__default__`,
) )
assertCode(content) assertCode(content)
}) })
@ -1400,7 +1422,7 @@ describe('SFC genDefaultAs', () => {
}, },
) )
expect(content).not.toMatch('export default') expect(content).not.toMatch('export default')
expect(content).toMatch(`const _sfc_ = /*#__PURE__*/_defineComponent(`) expect(content).toMatch(`const _sfc_ = /*@__PURE__*/_defineComponent(`)
assertCode(content) assertCode(content)
}) })
@ -1418,7 +1440,7 @@ describe('SFC genDefaultAs', () => {
) )
expect(content).not.toMatch('export default') expect(content).not.toMatch('export default')
expect(content).toMatch( expect(content).toMatch(
`const _sfc_ = /*#__PURE__*/_defineComponent({\n ...__default__`, `const _sfc_ = /*@__PURE__*/_defineComponent({\n ...__default__`,
) )
assertCode(content) assertCode(content)
}) })

View File

@ -18,7 +18,7 @@ return { myEmit }
exports[`defineEmits > w/ runtime options 1`] = ` exports[`defineEmits > w/ runtime options 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
emits: ['a', 'b'], emits: ['a', 'b'],
setup(__props, { expose: __expose, emit: __emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
@ -35,7 +35,7 @@ exports[`defineEmits > w/ type (exported interface) 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export interface Emits { (e: 'foo' | 'bar'): void } export interface Emits { (e: 'foo' | 'bar'): void }
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
emits: ["foo", "bar"], emits: ["foo", "bar"],
setup(__props, { expose: __expose, emit: __emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
@ -52,7 +52,7 @@ exports[`defineEmits > w/ type (exported type alias) 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export type Emits = { (e: 'foo' | 'bar'): void } export type Emits = { (e: 'foo' | 'bar'): void }
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
emits: ["foo", "bar"], emits: ["foo", "bar"],
setup(__props, { expose: __expose, emit: __emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
@ -69,7 +69,7 @@ exports[`defineEmits > w/ type (interface ts type) 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
interface Emits { (e: 'foo'): void } interface Emits { (e: 'foo'): void }
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
emits: ['foo'], emits: ['foo'],
setup(__props, { expose: __expose, emit: __emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
@ -87,7 +87,7 @@ exports[`defineEmits > w/ type (interface w/ extends) 1`] = `
interface Base { (e: 'foo'): void } interface Base { (e: 'foo'): void }
interface Emits extends Base { (e: 'bar'): void } interface Emits extends Base { (e: 'bar'): void }
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
emits: ["bar", "foo"], emits: ["bar", "foo"],
setup(__props, { expose: __expose, emit: __emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
@ -104,7 +104,7 @@ exports[`defineEmits > w/ type (interface) 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
interface Emits { (e: 'foo' | 'bar'): void } interface Emits { (e: 'foo' | 'bar'): void }
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
emits: ["foo", "bar"], emits: ["foo", "bar"],
setup(__props, { expose: __expose, emit: __emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
@ -120,7 +120,7 @@ return { emit }
exports[`defineEmits > w/ type (property syntax string literal) 1`] = ` exports[`defineEmits > w/ type (property syntax string literal) 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
emits: ["foo:bar"], emits: ["foo:bar"],
setup(__props, { expose: __expose, emit: __emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
@ -136,7 +136,7 @@ return { emit }
exports[`defineEmits > w/ type (property syntax) 1`] = ` exports[`defineEmits > w/ type (property syntax) 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
emits: ["foo", "bar"], emits: ["foo", "bar"],
setup(__props, { expose: __expose, emit: __emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
@ -153,7 +153,7 @@ exports[`defineEmits > w/ type (referenced exported function type) 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export type Emits = (e: 'foo' | 'bar') => void export type Emits = (e: 'foo' | 'bar') => void
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
emits: ["foo", "bar"], emits: ["foo", "bar"],
setup(__props, { expose: __expose, emit: __emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
@ -170,7 +170,7 @@ exports[`defineEmits > w/ type (referenced function type) 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
type Emits = (e: 'foo' | 'bar') => void type Emits = (e: 'foo' | 'bar') => void
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
emits: ["foo", "bar"], emits: ["foo", "bar"],
setup(__props, { expose: __expose, emit: __emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
@ -187,7 +187,7 @@ exports[`defineEmits > w/ type (type alias) 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
type Emits = { (e: 'foo' | 'bar'): void } type Emits = { (e: 'foo' | 'bar'): void }
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
emits: ["foo", "bar"], emits: ["foo", "bar"],
setup(__props, { expose: __expose, emit: __emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
@ -203,7 +203,7 @@ return { emit }
exports[`defineEmits > w/ type (type literal w/ call signatures) 1`] = ` exports[`defineEmits > w/ type (type literal w/ call signatures) 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
emits: ["foo", "bar", "baz"], emits: ["foo", "bar", "baz"],
setup(__props, { expose: __expose, emit: __emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
@ -221,7 +221,7 @@ exports[`defineEmits > w/ type (type references in union) 1`] = `
type BaseEmit = "change" type BaseEmit = "change"
type Emit = "some" | "emit" | BaseEmit type Emit = "some" | "emit" | BaseEmit
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
emits: ["some", "emit", "change", "another"], emits: ["some", "emit", "change", "another"],
setup(__props, { expose: __expose, emit: __emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
@ -237,7 +237,7 @@ return { emit }
exports[`defineEmits > w/ type (union) 1`] = ` exports[`defineEmits > w/ type (union) 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
emits: ["foo", "bar", "baz"], emits: ["foo", "bar", "baz"],
setup(__props, { expose: __expose, emit: __emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
@ -253,7 +253,7 @@ return { emit }
exports[`defineEmits > w/ type 1`] = ` exports[`defineEmits > w/ type 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
emits: ["foo", "bar"], emits: ["foo", "bar"],
setup(__props, { expose: __expose, emit: __emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();
@ -271,7 +271,7 @@ exports[`defineEmits > w/ type from normal script 1`] = `
export interface Emits { (e: 'foo' | 'bar'): void } export interface Emits { (e: 'foo' | 'bar'): void }
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
emits: ["foo", "bar"], emits: ["foo", "bar"],
setup(__props, { expose: __expose, emit: __emit }) { setup(__props, { expose: __expose, emit: __emit }) {
__expose(); __expose();

View File

@ -29,7 +29,7 @@ return { modelValue, c, toString }
exports[`defineModel() > get / set transformers 1`] = ` exports[`defineModel() > get / set transformers 1`] = `
"import { useModel as _useModel, defineComponent as _defineComponent } from 'vue' "import { useModel as _useModel, defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
"modelValue": { "modelValue": {
required: true required: true
@ -54,7 +54,7 @@ return { modelValue }
exports[`defineModel() > get / set transformers 2`] = ` exports[`defineModel() > get / set transformers 2`] = `
"import { useModel as _useModel, defineComponent as _defineComponent } from 'vue' "import { useModel as _useModel, defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
"modelValue": { "modelValue": {
default: 0, default: 0,
@ -80,8 +80,8 @@ return { modelValue }
exports[`defineModel() > usage w/ props destructure 1`] = ` exports[`defineModel() > usage w/ props destructure 1`] = `
"import { useModel as _useModel, mergeModels as _mergeModels, defineComponent as _defineComponent } from 'vue' "import { useModel as _useModel, mergeModels as _mergeModels, defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: /*#__PURE__*/_mergeModels({ props: /*@__PURE__*/_mergeModels({
x: { type: Number, required: true } x: { type: Number, required: true }
}, { }, {
"modelValue": { "modelValue": {
@ -106,7 +106,7 @@ return { modelValue }
exports[`defineModel() > w/ Boolean And Function types, production mode 1`] = ` exports[`defineModel() > w/ Boolean And Function types, production mode 1`] = `
"import { useModel as _useModel, defineComponent as _defineComponent } from 'vue' "import { useModel as _useModel, defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
"modelValue": { type: [Boolean, String] }, "modelValue": { type: [Boolean, String] },
"modelModifiers": {}, "modelModifiers": {},
@ -127,7 +127,7 @@ exports[`defineModel() > w/ array props 1`] = `
"import { useModel as _useModel, mergeModels as _mergeModels } from 'vue' "import { useModel as _useModel, mergeModels as _mergeModels } from 'vue'
export default { export default {
props: /*#__PURE__*/_mergeModels(['foo', 'bar'], { props: /*@__PURE__*/_mergeModels(['foo', 'bar'], {
"count": {}, "count": {},
"countModifiers": {}, "countModifiers": {},
}), }),
@ -148,11 +148,11 @@ exports[`defineModel() > w/ defineProps and defineEmits 1`] = `
"import { useModel as _useModel, mergeModels as _mergeModels } from 'vue' "import { useModel as _useModel, mergeModels as _mergeModels } from 'vue'
export default { export default {
props: /*#__PURE__*/_mergeModels({ foo: String }, { props: /*@__PURE__*/_mergeModels({ foo: String }, {
"modelValue": { default: 0 }, "modelValue": { default: 0 },
"modelModifiers": {}, "modelModifiers": {},
}), }),
emits: /*#__PURE__*/_mergeModels(['change'], ["update:modelValue"]), emits: /*@__PURE__*/_mergeModels(['change'], ["update:modelValue"]),
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -169,7 +169,7 @@ return { count }
exports[`defineModel() > w/ types, basic usage 1`] = ` exports[`defineModel() > w/ types, basic usage 1`] = `
"import { useModel as _useModel, defineComponent as _defineComponent } from 'vue' "import { useModel as _useModel, defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
"modelValue": { type: [Boolean, String] }, "modelValue": { type: [Boolean, String] },
"modelModifiers": {}, "modelModifiers": {},
@ -198,7 +198,7 @@ return { modelValue, count, disabled, any }
exports[`defineModel() > w/ types, production mode 1`] = ` exports[`defineModel() > w/ types, production mode 1`] = `
"import { useModel as _useModel, defineComponent as _defineComponent } from 'vue' "import { useModel as _useModel, defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
"modelValue": { type: Boolean }, "modelValue": { type: Boolean },
"modelModifiers": {}, "modelModifiers": {},
@ -230,7 +230,7 @@ return { modelValue, fn, fnWithDefault, str, optional }
exports[`defineModel() > w/ types, production mode, boolean + multiple types 1`] = ` exports[`defineModel() > w/ types, production mode, boolean + multiple types 1`] = `
"import { useModel as _useModel, defineComponent as _defineComponent } from 'vue' "import { useModel as _useModel, defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
"modelValue": { type: [Boolean, String, Object] }, "modelValue": { type: [Boolean, String, Object] },
"modelModifiers": {}, "modelModifiers": {},
@ -250,7 +250,7 @@ return { modelValue }
exports[`defineModel() > w/ types, production mode, function + runtime opts + multiple types 1`] = ` exports[`defineModel() > w/ types, production mode, function + runtime opts + multiple types 1`] = `
"import { useModel as _useModel, defineComponent as _defineComponent } from 'vue' "import { useModel as _useModel, defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
"modelValue": { type: [Number, Function], ...{ default: () => 1 } }, "modelValue": { type: [Number, Function], ...{ default: () => 1 } },
"modelModifiers": {}, "modelModifiers": {},

View File

@ -2,7 +2,7 @@
exports[`defineOptions() > basic usage 1`] = ` exports[`defineOptions() > basic usage 1`] = `
" "
export default /*#__PURE__*/Object.assign({ name: 'FooApp' }, { export default /*@__PURE__*/Object.assign({ name: 'FooApp' }, {
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();

View File

@ -24,7 +24,7 @@ interface Props {
foo?: number; foo?: number;
} }
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
__name: 'app.ce', __name: 'app.ce',
props: { props: {
foo: { default: 5.5, type: Number } foo: { default: 5.5, type: Number }
@ -43,7 +43,7 @@ return { props }
exports[`defineProps > custom element retains the props type & production mode 1`] = ` exports[`defineProps > custom element retains the props type & production mode 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
__name: 'app.ce', __name: 'app.ce',
props: { props: {
foo: {type: Number} foo: {type: Number}
@ -62,7 +62,7 @@ return { props }
exports[`defineProps > defineProps w/ runtime options 1`] = ` exports[`defineProps > defineProps w/ runtime options 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { foo: String }, props: { foo: String },
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -78,7 +78,7 @@ return { props }
exports[`defineProps > destructure without enabling reactive destructure 1`] = ` exports[`defineProps > destructure without enabling reactive destructure 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
foo: { type: null, required: true } foo: { type: null, required: true }
}, },
@ -96,7 +96,7 @@ return { foo }
exports[`defineProps > should escape names w/ special symbols 1`] = ` exports[`defineProps > should escape names w/ special symbols 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
"spa ce": { type: null, required: true }, "spa ce": { type: null, required: true },
"exclamation!mark": { type: null, required: true }, "exclamation!mark": { type: null, required: true },
@ -141,7 +141,7 @@ return { }
exports[`defineProps > w/ TS assertion 1`] = ` exports[`defineProps > w/ TS assertion 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: ['foo'], props: ['foo'],
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -158,7 +158,7 @@ exports[`defineProps > w/ exported interface 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export interface Props { x?: number } export interface Props { x?: number }
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
x: { type: Number, required: false } x: { type: Number, required: false }
}, },
@ -178,7 +178,7 @@ exports[`defineProps > w/ exported interface in normal script 1`] = `
export interface Props { x?: number } export interface Props { x?: number }
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
x: { type: Number, required: false } x: { type: Number, required: false }
}, },
@ -197,7 +197,7 @@ exports[`defineProps > w/ exported type alias 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export type Props = { x?: number } export type Props = { x?: number }
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
x: { type: Number, required: false } x: { type: Number, required: false }
}, },
@ -222,7 +222,7 @@ interface Bar extends Foo { y?: number }
interface Foo { x?: number } interface Foo { x?: number }
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
z: { type: Number, required: true }, z: { type: Number, required: true },
y: { type: String, required: true }, y: { type: String, required: true },
@ -259,7 +259,7 @@ exports[`defineProps > w/ interface 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
interface Props { x?: number } interface Props { x?: number }
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
x: { type: Number, required: false } x: { type: Number, required: false }
}, },
@ -296,7 +296,7 @@ interface Test {}
type Alias = number[] type Alias = number[]
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
string: { type: String, required: true }, string: { type: String, required: true },
number: { type: Number, required: true }, number: { type: Number, required: true },
@ -353,7 +353,7 @@ exports[`defineProps > w/ type alias 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
type Props = { x?: number } type Props = { x?: number }
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
x: { type: Number, required: false } x: { type: Number, required: false }
}, },
@ -372,8 +372,8 @@ exports[`defineProps > withDefaults (dynamic) 1`] = `
"import { mergeDefaults as _mergeDefaults, defineComponent as _defineComponent } from 'vue' "import { mergeDefaults as _mergeDefaults, defineComponent as _defineComponent } from 'vue'
import { defaults } from './foo' import { defaults } from './foo'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: /*#__PURE__*/_mergeDefaults({ props: /*@__PURE__*/_mergeDefaults({
foo: { type: String, required: false }, foo: { type: String, required: false },
bar: { type: Number, required: false }, bar: { type: Number, required: false },
baz: { type: Boolean, required: true } baz: { type: Boolean, required: true }
@ -393,8 +393,8 @@ exports[`defineProps > withDefaults (dynamic) w/ production mode 1`] = `
"import { mergeDefaults as _mergeDefaults, defineComponent as _defineComponent } from 'vue' "import { mergeDefaults as _mergeDefaults, defineComponent as _defineComponent } from 'vue'
import { defaults } from './foo' import { defaults } from './foo'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: /*#__PURE__*/_mergeDefaults({ props: /*@__PURE__*/_mergeDefaults({
foo: { type: Function }, foo: { type: Function },
bar: { type: Boolean }, bar: { type: Boolean },
baz: { type: [Boolean, Function] }, baz: { type: [Boolean, Function] },
@ -415,8 +415,8 @@ exports[`defineProps > withDefaults (reference) 1`] = `
"import { mergeDefaults as _mergeDefaults, defineComponent as _defineComponent } from 'vue' "import { mergeDefaults as _mergeDefaults, defineComponent as _defineComponent } from 'vue'
import { defaults } from './foo' import { defaults } from './foo'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: /*#__PURE__*/_mergeDefaults({ props: /*@__PURE__*/_mergeDefaults({
foo: { type: String, required: false }, foo: { type: String, required: false },
bar: { type: Number, required: false }, bar: { type: Number, required: false },
baz: { type: Boolean, required: true } baz: { type: Boolean, required: true }
@ -439,7 +439,7 @@ exports[`defineProps > withDefaults (static) + normal script 1`] = `
a?: string; a?: string;
} }
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
a: { type: String, required: false, default: "a" } a: { type: String, required: false, default: "a" }
}, },
@ -457,7 +457,7 @@ return { props }
exports[`defineProps > withDefaults (static) 1`] = ` exports[`defineProps > withDefaults (static) 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
foo: { type: String, required: false, default: 'hi' }, foo: { type: String, required: false, default: 'hi' },
bar: { type: Number, required: false }, bar: { type: Number, required: false },
@ -481,7 +481,7 @@ return { props }
exports[`defineProps > withDefaults (static) w/ production mode 1`] = ` exports[`defineProps > withDefaults (static) w/ production mode 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
foo: {}, foo: {},
bar: { type: Boolean }, bar: { type: Boolean },
@ -502,8 +502,8 @@ return { props }
exports[`defineProps > withDefaults w/ dynamic object method 1`] = ` exports[`defineProps > withDefaults w/ dynamic object method 1`] = `
"import { mergeDefaults as _mergeDefaults, defineComponent as _defineComponent } from 'vue' "import { mergeDefaults as _mergeDefaults, defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: /*#__PURE__*/_mergeDefaults({ props: /*@__PURE__*/_mergeDefaults({
foo: { type: Function, required: false } foo: { type: Function, required: false }
}, { }, {
['fo' + 'o']() { return 'foo' } ['fo' + 'o']() { return 'foo' }

View File

@ -62,7 +62,7 @@ exports[`sfc reactive props destructure > default values w/ array runtime declar
"import { mergeDefaults as _mergeDefaults } from 'vue' "import { mergeDefaults as _mergeDefaults } from 'vue'
export default { export default {
props: /*#__PURE__*/_mergeDefaults(['foo', 'bar', 'baz'], { props: /*@__PURE__*/_mergeDefaults(['foo', 'bar', 'baz'], {
foo: 1, foo: 1,
bar: () => ({}), bar: () => ({}),
func: () => {}, __skip_func: true func: () => {}, __skip_func: true
@ -81,7 +81,7 @@ exports[`sfc reactive props destructure > default values w/ object runtime decla
"import { mergeDefaults as _mergeDefaults } from 'vue' "import { mergeDefaults as _mergeDefaults } from 'vue'
export default { export default {
props: /*#__PURE__*/_mergeDefaults({ foo: Number, bar: Object, func: Function, ext: null }, { props: /*@__PURE__*/_mergeDefaults({ foo: Number, bar: Object, func: Function, ext: null }, {
foo: 1, foo: 1,
bar: () => ({}), bar: () => ({}),
func: () => {}, __skip_func: true, func: () => {}, __skip_func: true,
@ -101,7 +101,7 @@ exports[`sfc reactive props destructure > default values w/ runtime declaration
"import { mergeDefaults as _mergeDefaults } from 'vue' "import { mergeDefaults as _mergeDefaults } from 'vue'
export default { export default {
props: /*#__PURE__*/_mergeDefaults(['foo', 'foo:bar'], { props: /*@__PURE__*/_mergeDefaults(['foo', 'foo:bar'], {
foo: 1, foo: 1,
"foo:bar": 'foo-bar' "foo:bar": 'foo-bar'
}), }),
@ -118,7 +118,7 @@ return () => {}
exports[`sfc reactive props destructure > default values w/ type declaration & key is string 1`] = ` exports[`sfc reactive props destructure > default values w/ type declaration & key is string 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
foo: { type: Number, required: true, default: 1 }, foo: { type: Number, required: true, default: 1 },
bar: { type: Number, required: true, default: 2 }, bar: { type: Number, required: true, default: 2 },
@ -138,7 +138,7 @@ return () => {}
exports[`sfc reactive props destructure > default values w/ type declaration 1`] = ` exports[`sfc reactive props destructure > default values w/ type declaration 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
foo: { type: Number, required: false, default: 1 }, foo: { type: Number, required: false, default: 1 },
bar: { type: Object, required: false, default: () => ({}) }, bar: { type: Object, required: false, default: () => ({}) },
@ -157,7 +157,7 @@ return () => {}
exports[`sfc reactive props destructure > default values w/ type declaration, prod mode 1`] = ` exports[`sfc reactive props destructure > default values w/ type declaration, prod mode 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
foo: { default: 1 }, foo: { default: 1 },
bar: { default: () => ({}) }, bar: { default: () => ({}) },

View File

@ -3,7 +3,7 @@
exports[`defineSlots() > basic usage 1`] = ` exports[`defineSlots() > basic usage 1`] = `
"import { useSlots as _useSlots, defineComponent as _defineComponent } from 'vue' "import { useSlots as _useSlots, defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -33,7 +33,7 @@ return { slots }
exports[`defineSlots() > w/o return value 1`] = ` exports[`defineSlots() > w/o return value 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();

View File

@ -71,7 +71,7 @@ return () => {}
exports[`sfc hoist static > should not hoist a constant initialized to a reference value 1`] = ` exports[`sfc hoist static > should not hoist a constant initialized to a reference value 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
setup(__props) { setup(__props) {
const KEY1 = Boolean const KEY1 = Boolean

View File

@ -5,7 +5,7 @@ exports[`TS annotations 1`] = `
import { Foo, Bar, Baz, Qux, Fred } from './x' import { Foo, Bar, Baz, Qux, Fred } from './x'
const a = 1 const a = 1
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -22,7 +22,7 @@ exports[`attribute expressions 1`] = `
import { bar, baz } from './x' import { bar, baz } from './x'
const cond = true const cond = true
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -38,7 +38,7 @@ exports[`components 1`] = `
import { FooBar, FooBaz, FooQux, foo } from './x' import { FooBar, FooBaz, FooQux, foo } from './x'
const fooBar: FooBar = 1 const fooBar: FooBar = 1
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -53,7 +53,7 @@ exports[`directive 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
import { vMyDir } from './x' import { vMyDir } from './x'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -68,7 +68,7 @@ exports[`dynamic arguments 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
import { FooBar, foo, bar, unused, baz, msg } from './x' import { FooBar, foo, bar, unused, baz, msg } from './x'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -83,7 +83,7 @@ exports[`js template string interpolations 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
import { VAR, VAR2, VAR3 } from './x' import { VAR, VAR2, VAR3 } from './x'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -98,7 +98,7 @@ exports[`last tag 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
import { FooBaz, Last } from './x' import { FooBaz, Last } from './x'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -113,7 +113,7 @@ exports[`namespace / dot component usage 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
import * as Foo from './foo' import * as Foo from './foo'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -128,7 +128,7 @@ exports[`property access (whitespace) 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
import { Foo, Bar, Baz } from './foo' import { Foo, Bar, Baz } from './foo'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -143,7 +143,7 @@ exports[`property access 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
import { Foo, Bar, Baz } from './foo' import { Foo, Bar, Baz } from './foo'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -158,7 +158,7 @@ exports[`spread operator 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
import { Foo, Bar, Baz } from './foo' import { Foo, Bar, Baz } from './foo'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -173,7 +173,7 @@ exports[`template ref 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
import { foo, bar, Baz } from './foo' import { foo, bar, Baz } from './foo'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -188,7 +188,7 @@ exports[`vue interpolations 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
import { x, y, z, x$y } from './x' import { x, y, z, x$y } from './x'
export default /*#__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();

View File

@ -31,7 +31,7 @@ const emit = defineEmits(['a', 'b'])
</script> </script>
`) `)
assertCode(content) assertCode(content)
expect(content).toMatch(`export default /*#__PURE__*/_defineComponent({ expect(content).toMatch(`export default /*@__PURE__*/_defineComponent({
emits: ['a', 'b'], emits: ['a', 'b'],
setup(__props, { expose: __expose, emit: __emit }) {`) setup(__props, { expose: __expose, emit: __emit }) {`)
expect(content).toMatch('const emit = __emit') expect(content).toMatch('const emit = __emit')

View File

@ -47,7 +47,7 @@ describe('defineModel()', () => {
`, `,
) )
assertCode(content) assertCode(content)
expect(content).toMatch(`props: /*#__PURE__*/_mergeModels({ foo: String }`) expect(content).toMatch(`props: /*@__PURE__*/_mergeModels({ foo: String }`)
expect(content).toMatch(`"modelValue": { default: 0 }`) expect(content).toMatch(`"modelValue": { default: 0 }`)
expect(content).toMatch(`const count = _useModel(__props, "modelValue")`) expect(content).toMatch(`const count = _useModel(__props, "modelValue")`)
expect(content).not.toMatch('defineModel') expect(content).not.toMatch('defineModel')
@ -68,7 +68,7 @@ describe('defineModel()', () => {
`, `,
) )
assertCode(content) assertCode(content)
expect(content).toMatch(`props: /*#__PURE__*/_mergeModels(['foo', 'bar'], { expect(content).toMatch(`props: /*@__PURE__*/_mergeModels(['foo', 'bar'], {
"count": {}, "count": {},
"countModifiers": {}, "countModifiers": {},
})`) })`)

View File

@ -12,7 +12,7 @@ describe('defineOptions()', () => {
expect(content).not.toMatch('defineOptions') expect(content).not.toMatch('defineOptions')
// should include context options in default export // should include context options in default export
expect(content).toMatch( expect(content).toMatch(
`export default /*#__PURE__*/Object.assign({ name: 'FooApp' }, `, `export default /*@__PURE__*/Object.assign({ name: 'FooApp' }, `,
) )
}) })

View File

@ -64,7 +64,7 @@ const props = defineProps({ foo: String })
</script> </script>
`) `)
assertCode(content) assertCode(content)
expect(content).toMatch(`export default /*#__PURE__*/_defineComponent({ expect(content).toMatch(`export default /*@__PURE__*/_defineComponent({
props: { foo: String }, props: { foo: String },
setup(__props, { expose: __expose }) {`) setup(__props, { expose: __expose }) {`)
}) })

View File

@ -78,7 +78,7 @@ describe('sfc reactive props destructure', () => {
// function // function
// functions need to be marked with a skip marker // functions need to be marked with a skip marker
expect(content) expect(content)
.toMatch(`props: /*#__PURE__*/_mergeDefaults(['foo', 'bar', 'baz'], { .toMatch(`props: /*@__PURE__*/_mergeDefaults(['foo', 'bar', 'baz'], {
foo: 1, foo: 1,
bar: () => ({}), bar: () => ({}),
func: () => {}, __skip_func: true func: () => {}, __skip_func: true
@ -98,7 +98,7 @@ describe('sfc reactive props destructure', () => {
// safely infer whether runtime type is Function (e.g. if the runtime decl // safely infer whether runtime type is Function (e.g. if the runtime decl
// is imported, or spreads another object) // is imported, or spreads another object)
expect(content) expect(content)
.toMatch(`props: /*#__PURE__*/_mergeDefaults({ foo: Number, bar: Object, func: Function, ext: null }, { .toMatch(`props: /*@__PURE__*/_mergeDefaults({ foo: Number, bar: Object, func: Function, ext: null }, {
foo: 1, foo: 1,
bar: () => ({}), bar: () => ({}),
func: () => {}, __skip_func: true, func: () => {}, __skip_func: true,
@ -122,7 +122,7 @@ describe('sfc reactive props destructure', () => {
}) })
expect(content).toMatch(` expect(content).toMatch(`
props: /*#__PURE__*/_mergeDefaults(['foo', 'foo:bar'], { props: /*@__PURE__*/_mergeDefaults(['foo', 'foo:bar'], {
foo: 1, foo: 1,
"foo:bar": 'foo-bar' "foo:bar": 'foo-bar'
}),`) }),`)
@ -378,14 +378,15 @@ describe('sfc reactive props destructure', () => {
).toThrow(`destructure cannot use computed key`) ).toThrow(`destructure cannot use computed key`)
}) })
test('should error when used with withDefaults', () => { test('should warn when used with withDefaults', () => {
expect(() => compile(
compile( `<script setup lang="ts">
`<script setup lang="ts"> const { foo } = withDefaults(defineProps<{ foo: string }>(), { foo: 'foo' })
const { foo } = withDefaults(defineProps<{ foo: string }>(), { foo: 'foo' }) </script>`,
</script>`, )
), expect(
).toThrow(`withDefaults() is unnecessary when using destructure`) `withDefaults() is unnecessary when using destructure`,
).toHaveBeenWarned()
}) })
test('should error if destructure reference local vars', () => { test('should error if destructure reference local vars', () => {

View File

@ -234,3 +234,34 @@ test('namespace / dot component usage', () => {
expect(content).toMatch('return { get Foo() { return Foo } }') expect(content).toMatch('return { get Foo() { return Foo } }')
assertCode(content) assertCode(content)
}) })
test('check when has explicit parse options', () => {
const { content } = compile(
`
<script setup lang="ts">
import { x } from './x'
</script>
<template>
{{ x }}
</template>
`,
undefined,
{ templateParseOptions: {} },
)
expect(content).toMatch('return { get x() { return x } }')
})
// #11745
test('shorthand binding w/ kebab-case', () => {
const { content } = compile(
`
<script setup lang="ts">
import { fooBar } from "./foo.ts"
</script>
<template>
<div :foo-bar></div>
</template>
`,
)
expect(content).toMatch('return { get fooBar() { return fooBar }')
})

View File

@ -596,6 +596,65 @@ describe('resolveType', () => {
}) })
}) })
test('keyof: nested object with number', () => {
const { props } = resolve(
`
interface Type {
deep: {
1: any
}
}
defineProps<{
route: keyof Type['deep']
}>()`,
)
expect(props).toStrictEqual({
route: ['Number'],
})
})
test('keyof: nested object with string', () => {
const { props } = resolve(
`
interface Type {
deep: {
foo: any
}
}
defineProps<{
route: keyof Type['deep']
}>()`,
)
expect(props).toStrictEqual({
route: ['String'],
})
})
test('keyof: nested object with intermediate', () => {
const { props } = resolve(
`
interface Type {
deep: {
foo: any
}
}
type Foo = Type['deep']
defineProps<{
route: keyof Foo
}>()`,
)
expect(props).toStrictEqual({
route: ['String'],
})
})
test('ExtractPropTypes (element-plus)', () => { test('ExtractPropTypes (element-plus)', () => {
const { props, raw } = resolve( const { props, raw } = resolve(
` `
@ -1126,6 +1185,49 @@ describe('resolveType', () => {
expect(deps && [...deps]).toStrictEqual(['/user.ts']) expect(deps && [...deps]).toStrictEqual(['/user.ts'])
}) })
// #11382
test('ts module resolve circular project reference', () => {
const files = {
'/tsconfig.json': JSON.stringify({
exclude: ['**/*.ts', '**/*.vue'],
references: [
{
path: './tsconfig.web.json',
},
],
}),
'/tsconfig.web.json': JSON.stringify({
include: ['**/*.ts', '**/*.vue'],
compilerOptions: {
composite: true,
paths: {
user: ['./user.ts'],
},
},
references: [
{
// circular reference
path: './tsconfig.json',
},
],
}),
'/user.ts': 'export type User = { bar: string }',
}
const { props, deps } = resolve(
`
import { User } from 'user'
defineProps<User>()
`,
files,
)
expect(props).toStrictEqual({
bar: ['String'],
})
expect(deps && [...deps]).toStrictEqual(['/user.ts'])
})
test('ts module resolve w/ path aliased vue file', () => { test('ts module resolve w/ path aliased vue file', () => {
const files = { const files = {
'/tsconfig.json': JSON.stringify({ '/tsconfig.json': JSON.stringify({

View File

@ -41,6 +41,44 @@ describe('SFC scoped CSS', () => {
) )
}) })
test('nesting selector', () => {
expect(compileScoped(`h1 { color: red; .foo { color: red; } }`)).toMatch(
`h1 {\n&[data-v-test] { color: red;\n}\n.foo[data-v-test] { color: red;`,
)
})
test('nesting selector with atrule and comment', () => {
expect(
compileScoped(
`h1 {
color: red;
/*background-color: pink;*/
@media only screen and (max-width: 800px) {
background-color: green;
.bar { color: white }
}
.foo { color: red; }
}`,
),
).toMatch(
`h1 {
&[data-v-test] {
color: red
/*background-color: pink;*/
}
@media only screen and (max-width: 800px) {
&[data-v-test] {
background-color: green
}
.bar[data-v-test] { color: white
}
}
.foo[data-v-test] { color: red;
}
}`,
)
})
test('multiple selectors', () => { test('multiple selectors', () => {
expect(compileScoped(`h1 .foo, .bar, .baz { color: red; }`)).toMatch( expect(compileScoped(`h1 .foo, .bar, .baz { color: red; }`)).toMatch(
`h1 .foo[data-v-test], .bar[data-v-test], .baz[data-v-test] { color: red;`, `h1 .foo[data-v-test], .bar[data-v-test], .baz[data-v-test] { color: red;`,
@ -95,6 +133,13 @@ describe('SFC scoped CSS', () => {
":where(.foo[data-v-test] .bar) { color: red; ":where(.foo[data-v-test] .bar) { color: red;
}" }"
`) `)
expect(compileScoped(`:deep(.foo) { color: red; .bar { color: red; } }`))
.toMatchInlineSnapshot(`
"[data-v-test] .foo { color: red;
.bar { color: red;
}
}"
`)
}) })
test('::v-slotted', () => { test('::v-slotted', () => {

View File

@ -1,5 +1,6 @@
import { import {
type SFCParseOptions, type SFCParseOptions,
type SFCScriptBlock,
type SFCScriptCompileOptions, type SFCScriptCompileOptions,
compileScript, compileScript,
parse, parse,
@ -12,7 +13,7 @@ export function compileSFCScript(
src: string, src: string,
options?: Partial<SFCScriptCompileOptions>, options?: Partial<SFCScriptCompileOptions>,
parseOptions?: SFCParseOptions, parseOptions?: SFCParseOptions,
) { ): SFCScriptBlock {
const { descriptor, errors } = parse(src, parseOptions) const { descriptor, errors } = parse(src, parseOptions)
if (errors.length) { if (errors.length) {
console.warn(errors[0]) console.warn(errors[0])
@ -23,7 +24,7 @@ export function compileSFCScript(
}) })
} }
export function assertCode(code: string) { export function assertCode(code: string): void {
// parse the generated code to make sure it is valid // parse the generated code to make sure it is valid
try { try {
babelParse(code, { babelParse(code, {

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/compiler-sfc", "name": "@vue/compiler-sfc",
"version": "3.5.0-beta.1", "version": "3.5.11",
"description": "@vue/compiler-sfc", "description": "@vue/compiler-sfc",
"main": "dist/compiler-sfc.cjs.js", "main": "dist/compiler-sfc.cjs.js",
"module": "dist/compiler-sfc.esm-browser.js", "module": "dist/compiler-sfc.esm-browser.js",
@ -49,7 +49,7 @@
"@vue/shared": "workspace:*", "@vue/shared": "workspace:*",
"estree-walker": "catalog:", "estree-walker": "catalog:",
"magic-string": "catalog:", "magic-string": "catalog:",
"postcss": "^8.4.41", "postcss": "^8.4.47",
"source-map-js": "catalog:" "source-map-js": "catalog:"
}, },
"devDependencies": { "devDependencies": {
@ -60,8 +60,8 @@
"merge-source-map": "^1.1.0", "merge-source-map": "^1.1.0",
"minimatch": "~9.0.5", "minimatch": "~9.0.5",
"postcss-modules": "^6.0.0", "postcss-modules": "^6.0.0",
"postcss-selector-parser": "^6.1.1", "postcss-selector-parser": "^6.1.2",
"pug": "^3.0.3", "pug": "^3.0.3",
"sass": "^1.77.8" "sass": "^1.79.4"
} }
} }

View File

@ -3,6 +3,7 @@ import { LRUCache } from 'lru-cache'
export function createCache<T extends {}>( export function createCache<T extends {}>(
max = 500, max = 500,
): Map<string, T> | LRUCache<string, T> { ): Map<string, T> | LRUCache<string, T> {
/* v8 ignore next 3 */
if (__GLOBAL__ || __ESM_BROWSER__) { if (__GLOBAL__ || __ESM_BROWSER__) {
return new Map<string, T>() return new Map<string, T>()
} }

View File

@ -979,7 +979,7 @@ export function compileScript(
(definedOptions ? `\n ...${definedOptions},` : '') (definedOptions ? `\n ...${definedOptions},` : '')
ctx.s.prependLeft( ctx.s.prependLeft(
startOffset, startOffset,
`\n${genDefaultAs} /*#__PURE__*/${ctx.helper( `\n${genDefaultAs} /*@__PURE__*/${ctx.helper(
`defineComponent`, `defineComponent`,
)}({${def}${runtimeOptions}\n ${ )}({${def}${runtimeOptions}\n ${
hasAwait ? `async ` : `` hasAwait ? `async ` : ``
@ -992,7 +992,7 @@ export function compileScript(
// export default Object.assign(__default__, { ... }) // export default Object.assign(__default__, { ... })
ctx.s.prependLeft( ctx.s.prependLeft(
startOffset, startOffset,
`\n${genDefaultAs} /*#__PURE__*/Object.assign(${ `\n${genDefaultAs} /*@__PURE__*/Object.assign(${
defaultExport ? `${normalScriptDefaultVar}, ` : '' defaultExport ? `${normalScriptDefaultVar}, ` : ''
}${definedOptions ? `${definedOptions}, ` : ''}{${runtimeOptions}\n ` + }${definedOptions ? `${definedOptions}, ` : ''}{${runtimeOptions}\n ` +
`${hasAwait ? `async ` : ``}setup(${args}) {\n${exposeCall}`, `${hasAwait ? `async ` : ``}setup(${args}) {\n${exposeCall}`,

View File

@ -18,6 +18,7 @@ import { createCache } from './cache'
import type { ImportBinding } from './compileScript' import type { ImportBinding } from './compileScript'
import { isImportUsed } from './script/importUsageCheck' import { isImportUsed } from './script/importUsageCheck'
import type { LRUCache } from 'lru-cache' import type { LRUCache } from 'lru-cache'
import { genCacheKey } from '@vue/shared'
export const DEFAULT_FILENAME = 'anonymous.vue' export const DEFAULT_FILENAME = 'anonymous.vue'
@ -103,24 +104,14 @@ export const parseCache:
| Map<string, SFCParseResult> | Map<string, SFCParseResult>
| LRUCache<string, SFCParseResult> = createCache<SFCParseResult>() | LRUCache<string, SFCParseResult> = createCache<SFCParseResult>()
function genCacheKey(source: string, options: SFCParseOptions): string {
return (
source +
JSON.stringify(
{
...options,
compiler: { parse: options.compiler?.parse },
},
(_, val) => (typeof val === 'function' ? val.toString() : val),
)
)
}
export function parse( export function parse(
source: string, source: string,
options: SFCParseOptions = {}, options: SFCParseOptions = {},
): SFCParseResult { ): SFCParseResult {
const sourceKey = genCacheKey(source, options) const sourceKey = genCacheKey(source, {
...options,
compiler: { parse: options.compiler?.parse },
})
const cache = parseCache.get(sourceKey) const cache = parseCache.get(sourceKey)
if (cache) { if (cache) {
return cache return cache
@ -133,7 +124,7 @@ export function parse(
pad = false, pad = false,
ignoreEmpty = true, ignoreEmpty = true,
compiler = CompilerDOM, compiler = CompilerDOM,
templateParseOptions = { prefixIdentifiers: true }, templateParseOptions = {},
} = options } = options
const descriptor: SFCDescriptor = { const descriptor: SFCDescriptor = {
@ -152,6 +143,7 @@ export function parse(
const errors: (CompilerError | SyntaxError)[] = [] const errors: (CompilerError | SyntaxError)[] = []
const ast = compiler.parse(source, { const ast = compiler.parse(source, {
parseMode: 'sfc', parseMode: 'sfc',
prefixIdentifiers: true,
...templateParseOptions, ...templateParseOptions,
onError: e => { onError: e => {
errors.push(e) errors.push(e)
@ -234,7 +226,7 @@ export function parse(
if (!descriptor.template && !descriptor.script && !descriptor.scriptSetup) { if (!descriptor.template && !descriptor.script && !descriptor.scriptSetup) {
errors.push( errors.push(
new SyntaxError( new SyntaxError(
`At least one <template> or <script> is required in a single file component.`, `At least one <template> or <script> is required in a single file component. ${descriptor.filename}`,
), ),
) )
} }

View File

@ -8,6 +8,7 @@ import type { ModelDecl } from './defineModel'
import type { BindingMetadata } from '../../../compiler-core/src' import type { BindingMetadata } from '../../../compiler-core/src'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import type { TypeScope } from './resolveType' import type { TypeScope } from './resolveType'
import { warn } from '../warn'
export class ScriptCompileContext { export class ScriptCompileContext {
isJS: boolean isJS: boolean
@ -145,20 +146,31 @@ export class ScriptCompileContext {
return block.content.slice(node.start!, node.end!) return block.content.slice(node.start!, node.end!)
} }
warn(msg: string, node: Node, scope?: TypeScope): void {
warn(generateError(msg, node, this, scope))
}
error(msg: string, node: Node, scope?: TypeScope): never { error(msg: string, node: Node, scope?: TypeScope): never {
const offset = scope ? scope.offset : this.startOffset!
throw new Error( throw new Error(
`[@vue/compiler-sfc] ${msg}\n\n${ `[@vue/compiler-sfc] ${generateError(msg, node, this, scope)}`,
(scope || this.descriptor).filename
}\n${generateCodeFrame(
(scope || this.descriptor).source,
node.start! + offset,
node.end! + offset,
)}`,
) )
} }
} }
function generateError(
msg: string,
node: Node,
ctx: ScriptCompileContext,
scope?: TypeScope,
) {
const offset = scope ? scope.offset : ctx.startOffset!
return `${msg}\n\n${(scope || ctx.descriptor).filename}\n${generateCodeFrame(
(scope || ctx.descriptor).source,
node.start! + offset,
node.end! + offset,
)}`
}
export function resolveParserPlugins( export function resolveParserPlugins(
lang: string, lang: string,
userPlugins?: ParserPlugin[], userPlugins?: ParserPlugin[],

View File

@ -62,7 +62,7 @@ export function genRuntimeEmits(ctx: ScriptCompileContext): string | undefined {
.map(n => JSON.stringify(`update:${n}`)) .map(n => JSON.stringify(`update:${n}`))
.join(', ')}]` .join(', ')}]`
emitsDecl = emitsDecl emitsDecl = emitsDecl
? `/*#__PURE__*/${ctx.helper( ? `/*@__PURE__*/${ctx.helper(
'mergeModels', 'mergeModels',
)}(${emitsDecl}, ${modelEmitsDecl})` )}(${emitsDecl}, ${modelEmitsDecl})`
: modelEmitsDecl : modelEmitsDecl

View File

@ -48,6 +48,7 @@ export function processDefineProps(
ctx: ScriptCompileContext, ctx: ScriptCompileContext,
node: Node, node: Node,
declId?: LVal, declId?: LVal,
isWithDefaults = false,
): boolean { ): boolean {
if (!isCallOf(node, DEFINE_PROPS)) { if (!isCallOf(node, DEFINE_PROPS)) {
return processWithDefaults(ctx, node, declId) return processWithDefaults(ctx, node, declId)
@ -81,7 +82,7 @@ export function processDefineProps(
} }
// handle props destructure // handle props destructure
if (declId && declId.type === 'ObjectPattern') { if (!isWithDefaults && declId && declId.type === 'ObjectPattern') {
processPropsDestructure(ctx, declId) processPropsDestructure(ctx, declId)
} }
@ -99,7 +100,14 @@ function processWithDefaults(
if (!isCallOf(node, WITH_DEFAULTS)) { if (!isCallOf(node, WITH_DEFAULTS)) {
return false return false
} }
if (!processDefineProps(ctx, node.arguments[0], declId)) { if (
!processDefineProps(
ctx,
node.arguments[0],
declId,
true /* isWithDefaults */,
)
) {
ctx.error( ctx.error(
`${WITH_DEFAULTS}' first argument must be a ${DEFINE_PROPS} call.`, `${WITH_DEFAULTS}' first argument must be a ${DEFINE_PROPS} call.`,
node.arguments[0] || node, node.arguments[0] || node,
@ -113,10 +121,11 @@ function processWithDefaults(
node, node,
) )
} }
if (ctx.propsDestructureDecl) { if (declId && declId.type === 'ObjectPattern') {
ctx.error( ctx.warn(
`${WITH_DEFAULTS}() is unnecessary when using destructure with ${DEFINE_PROPS}().\n` + `${WITH_DEFAULTS}() is unnecessary when using destructure with ${DEFINE_PROPS}().\n` +
`Prefer using destructure default values, e.g. const { foo = 1 } = defineProps(...).`, `Reactive destructure will be disabled when using withDefaults().\n` +
`Prefer using destructure default values, e.g. const { foo = 1 } = defineProps(...). `,
node.callee, node.callee,
) )
} }
@ -147,7 +156,7 @@ export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined {
) )
} }
if (defaults.length) { if (defaults.length) {
propsDecls = `/*#__PURE__*/${ctx.helper( propsDecls = `/*@__PURE__*/${ctx.helper(
`mergeDefaults`, `mergeDefaults`,
)}(${propsDecls}, {\n ${defaults.join(',\n ')}\n})` )}(${propsDecls}, {\n ${defaults.join(',\n ')}\n})`
} }
@ -159,7 +168,7 @@ export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined {
const modelsDecls = genModelProps(ctx) const modelsDecls = genModelProps(ctx)
if (propsDecls && modelsDecls) { if (propsDecls && modelsDecls) {
return `/*#__PURE__*/${ctx.helper( return `/*@__PURE__*/${ctx.helper(
'mergeModels', 'mergeModels',
)}(${propsDecls}, ${modelsDecls})` )}(${propsDecls}, ${modelsDecls})`
} else { } else {
@ -191,7 +200,7 @@ export function extractRuntimeProps(
${propStrings.join(',\n ')}\n }` ${propStrings.join(',\n ')}\n }`
if (ctx.propsRuntimeDefaults && !hasStaticDefaults) { if (ctx.propsRuntimeDefaults && !hasStaticDefaults) {
propsDecls = `/*#__PURE__*/${ctx.helper( propsDecls = `/*@__PURE__*/${ctx.helper(
'mergeDefaults', 'mergeDefaults',
)}(${propsDecls}, ${ctx.getString(ctx.propsRuntimeDefaults)})` )}(${propsDecls}, ${ctx.getString(ctx.propsRuntimeDefaults)})`
} }

View File

@ -102,7 +102,7 @@ export function transformDestructuredProps(
return return
} }
const rootScope: Scope = {} const rootScope: Scope = Object.create(null)
const scopeStack: Scope[] = [rootScope] const scopeStack: Scope[] = [rootScope]
let currentScope: Scope = rootScope let currentScope: Scope = rootScope
const excludedIds = new WeakSet<Identifier>() const excludedIds = new WeakSet<Identifier>()
@ -242,6 +242,7 @@ export function transformDestructuredProps(
parent.type.startsWith('TS') && parent.type.startsWith('TS') &&
parent.type !== 'TSAsExpression' && parent.type !== 'TSAsExpression' &&
parent.type !== 'TSNonNullExpression' && parent.type !== 'TSNonNullExpression' &&
parent.type !== 'TSSatisfiesExpression' &&
parent.type !== 'TSTypeAssertion' parent.type !== 'TSTypeAssertion'
) { ) {
return this.skip() return this.skip()

View File

@ -62,7 +62,7 @@ function resolveTemplateUsedIdentifiers(sfc: SFCDescriptor): Set<string> {
extractIdentifiers(ids, prop.exp) extractIdentifiers(ids, prop.exp)
} else if (prop.name === 'bind' && !prop.exp) { } else if (prop.name === 'bind' && !prop.exp) {
// v-bind shorthand name as identifier // v-bind shorthand name as identifier
ids.add((prop.arg as SimpleExpressionNode).content) ids.add(camelize((prop.arg as SimpleExpressionNode).content))
} }
} }
if ( if (

View File

@ -1070,6 +1070,7 @@ function loadTSConfig(
configPath: string, configPath: string,
ts: typeof TS, ts: typeof TS,
fs: FS, fs: FS,
visited = new Set<string>(),
): TS.ParsedCommandLine[] { ): TS.ParsedCommandLine[] {
// The only case where `fs` is NOT `ts.sys` is during tests. // The only case where `fs` is NOT `ts.sys` is during tests.
// parse config host requires an extra `readDirectory` method // parse config host requires an extra `readDirectory` method
@ -1089,14 +1090,15 @@ function loadTSConfig(
configPath, configPath,
) )
const res = [config] const res = [config]
visited.add(configPath)
if (config.projectReferences) { if (config.projectReferences) {
for (const ref of config.projectReferences) { for (const ref of config.projectReferences) {
const refPath = ts.resolveProjectReferencePath(ref) const refPath = ts.resolveProjectReferencePath(ref)
if (!fs.fileExists(refPath)) { if (visited.has(refPath) || !fs.fileExists(refPath)) {
continue continue
} }
tsConfigRefMap.set(refPath, configPath) tsConfigRefMap.set(refPath, configPath)
res.unshift(...loadTSConfig(refPath, ts, fs)) res.unshift(...loadTSConfig(refPath, ts, fs, visited))
} }
} }
return res return res
@ -1703,7 +1705,7 @@ export function inferRuntimeType(
case 'TSIndexedAccessType': { case 'TSIndexedAccessType': {
const types = resolveIndexType(ctx, node, scope) const types = resolveIndexType(ctx, node, scope)
return flattenTypes(ctx, types, scope) return flattenTypes(ctx, types, scope, isKeyOf)
} }
case 'ClassDeclaration': case 'ClassDeclaration':

View File

@ -121,15 +121,3 @@ export const propNameEscapeSymbolsRE: RegExp =
export function getEscapedPropName(key: string): string { export function getEscapedPropName(key: string): string {
return propNameEscapeSymbolsRE.test(key) ? JSON.stringify(key) : key return propNameEscapeSymbolsRE.test(key) ? JSON.stringify(key) : key
} }
export const cssVarNameEscapeSymbolsRE: RegExp =
/[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g
export function getEscapedCssVarName(
key: string,
doubleEscape: boolean,
): string {
return key.replace(cssVarNameEscapeSymbolsRE, s =>
doubleEscape ? `\\\\${s}` : `\\${s}`,
)
}

View File

@ -8,9 +8,9 @@ import {
processExpression, processExpression,
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import type { SFCDescriptor } from '../parse' import type { SFCDescriptor } from '../parse'
import { getEscapedCssVarName } from '../script/utils'
import type { PluginCreator } from 'postcss' import type { PluginCreator } from 'postcss'
import hash from 'hash-sum' import hash from 'hash-sum'
import { getEscapedCssVarName } from '@vue/shared'
export const CSS_VARS_HELPER = `useCssVars` export const CSS_VARS_HELPER = `useCssVars`

View File

@ -1,4 +1,10 @@
import type { AtRule, PluginCreator, Rule } from 'postcss' import {
type AtRule,
type Container,
type Document,
type PluginCreator,
Rule,
} from 'postcss'
import selectorParser from 'postcss-selector-parser' import selectorParser from 'postcss-selector-parser'
import { warn } from '../warn' import { warn } from '../warn'
@ -71,21 +77,32 @@ function processRule(id: string, rule: Rule) {
return return
} }
processedRules.add(rule) processedRules.add(rule)
let deep = false
let parent: Document | Container | undefined = rule.parent
while (parent && parent.type !== 'root') {
if ((parent as any).__deep) {
deep = true
break
}
parent = parent.parent
}
rule.selector = selectorParser(selectorRoot => { rule.selector = selectorParser(selectorRoot => {
selectorRoot.each(selector => { selectorRoot.each(selector => {
rewriteSelector(id, selector, selectorRoot) rewriteSelector(id, rule, selector, selectorRoot, deep)
}) })
}).processSync(rule.selector) }).processSync(rule.selector)
} }
function rewriteSelector( function rewriteSelector(
id: string, id: string,
rule: Rule,
selector: selectorParser.Selector, selector: selectorParser.Selector,
selectorRoot: selectorParser.Root, selectorRoot: selectorParser.Root,
deep: boolean,
slotted = false, slotted = false,
) { ) {
let node: selectorParser.Node | null = null let node: selectorParser.Node | null = null
let shouldInject = true let shouldInject = !deep
// find the last child node to insert attribute selector // find the last child node to insert attribute selector
selector.each(n => { selector.each(n => {
// DEPRECATED ">>>" and "/deep/" combinator // DEPRECATED ">>>" and "/deep/" combinator
@ -107,6 +124,7 @@ function rewriteSelector(
// deep: inject [id] attribute at the node before the ::v-deep // deep: inject [id] attribute at the node before the ::v-deep
// combinator. // combinator.
if (value === ':deep' || value === '::v-deep') { if (value === ':deep' || value === '::v-deep') {
;(rule as any).__deep = true
if (n.nodes.length) { if (n.nodes.length) {
// .foo ::v-deep(.bar) -> .foo[xxxxxxx] .bar // .foo ::v-deep(.bar) -> .foo[xxxxxxx] .bar
// replace the current node with ::v-deep's inner selector // replace the current node with ::v-deep's inner selector
@ -147,7 +165,14 @@ function rewriteSelector(
// instead. // instead.
// ::v-slotted(.foo) -> .foo[xxxxxxx-s] // ::v-slotted(.foo) -> .foo[xxxxxxx-s]
if (value === ':slotted' || value === '::v-slotted') { if (value === ':slotted' || value === '::v-slotted') {
rewriteSelector(id, n.nodes[0], selectorRoot, true /* slotted */) rewriteSelector(
id,
rule,
n.nodes[0],
selectorRoot,
deep,
true /* slotted */,
)
let last: selectorParser.Selector['nodes'][0] = n let last: selectorParser.Selector['nodes'][0] = n
n.nodes[0].each(ss => { n.nodes[0].each(ss => {
selector.insertAfter(last, ss) selector.insertAfter(last, ss)
@ -206,11 +231,23 @@ function rewriteSelector(
} }
}) })
if (rule.nodes.some(node => node.type === 'rule')) {
const deep = (rule as any).__deep
if (!deep) {
extractAndWrapNodes(rule)
const atruleNodes = rule.nodes.filter(node => node.type === 'atrule')
for (const atnode of atruleNodes) {
extractAndWrapNodes(atnode)
}
}
shouldInject = deep
}
if (node) { if (node) {
const { type, value } = node as selectorParser.Node const { type, value } = node as selectorParser.Node
if (type === 'pseudo' && (value === ':is' || value === ':where')) { if (type === 'pseudo' && (value === ':is' || value === ':where')) {
;(node as selectorParser.Pseudo).nodes.forEach(value => ;(node as selectorParser.Pseudo).nodes.forEach(value =>
rewriteSelector(id, value, selectorRoot, slotted), rewriteSelector(id, rule, value, selectorRoot, deep, slotted),
) )
shouldInject = false shouldInject = false
} }
@ -245,5 +282,22 @@ function isSpaceCombinator(node: selectorParser.Node) {
return node.type === 'combinator' && /^\s+$/.test(node.value) return node.type === 'combinator' && /^\s+$/.test(node.value)
} }
function extractAndWrapNodes(parentNode: Rule | AtRule) {
if (!parentNode.nodes) return
const nodes = parentNode.nodes.filter(
node => node.type === 'decl' || node.type === 'comment',
)
if (nodes.length) {
for (const node of nodes) {
parentNode.removeChild(node)
}
const wrappedRule = new Rule({
nodes: nodes,
selector: '&',
})
parentNode.prepend(wrappedRule)
}
}
scopedPlugin.postcss = true scopedPlugin.postcss = true
export default scopedPlugin export default scopedPlugin

View File

@ -39,7 +39,7 @@ describe('transition-group', () => {
}) })
// #11514 // #11514
test('with static tag + comment', () => { test('with static tag + v-if comment', () => {
expect( expect(
compile( compile(
`<transition-group tag="ul"><div v-for="i in list"/><div v-if="false"></div></transition-group>`, `<transition-group tag="ul"><div v-for="i in list"/><div v-if="false"></div></transition-group>`,
@ -60,6 +60,25 @@ describe('transition-group', () => {
`) `)
}) })
// #11958
test('with static tag + comment', () => {
expect(
compile(
`<transition-group tag="ul"><div v-for="i in list"/><!--test--></transition-group>`,
).code,
).toMatchInlineSnapshot(`
"const { ssrRenderAttrs: _ssrRenderAttrs, ssrRenderList: _ssrRenderList } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<ul\${_ssrRenderAttrs(_attrs)}>\`)
_ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`)
})
_push(\`</ul>\`)
}"
`)
})
test('with dynamic tag', () => { test('with dynamic tag', () => {
expect( expect(
compile( compile(

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/compiler-ssr", "name": "@vue/compiler-ssr",
"version": "3.5.0-beta.1", "version": "3.5.11",
"description": "@vue/compiler-ssr", "description": "@vue/compiler-ssr",
"main": "dist/compiler-ssr.cjs.js", "main": "dist/compiler-ssr.cjs.js",
"types": "dist/compiler-ssr.d.ts", "types": "dist/compiler-ssr.d.ts",

View File

@ -156,7 +156,7 @@ export function processChildren(
context: SSRTransformContext, context: SSRTransformContext,
asFragment = false, asFragment = false,
disableNestedFragments = false, disableNestedFragments = false,
disableCommentAsIfAlternate = false, disableComment = false,
): void { ): void {
if (asFragment) { if (asFragment) {
context.pushStringPart(`<!--[-->`) context.pushStringPart(`<!--[-->`)
@ -197,7 +197,9 @@ export function processChildren(
case NodeTypes.COMMENT: case NodeTypes.COMMENT:
// no need to escape comment here because the AST can only // no need to escape comment here because the AST can only
// contain valid comments. // contain valid comments.
context.pushStringPart(`<!--${child.content}-->`) if (!disableComment) {
context.pushStringPart(`<!--${child.content}-->`)
}
break break
case NodeTypes.INTERPOLATION: case NodeTypes.INTERPOLATION:
context.pushStringPart( context.pushStringPart(
@ -207,12 +209,7 @@ export function processChildren(
) )
break break
case NodeTypes.IF: case NodeTypes.IF:
ssrProcessIf( ssrProcessIf(child, context, disableNestedFragments, disableComment)
child,
context,
disableNestedFragments,
disableCommentAsIfAlternate,
)
break break
case NodeTypes.FOR: case NodeTypes.FOR:
ssrProcessFor(child, context, disableNestedFragments) ssrProcessFor(child, context, disableNestedFragments)

View File

@ -27,7 +27,7 @@ export function ssrProcessIf(
node: IfNode, node: IfNode,
context: SSRTransformContext, context: SSRTransformContext,
disableNestedFragments = false, disableNestedFragments = false,
disableCommentAsIfAlternate = false, disableComment = false,
): void { ): void {
const [rootBranch] = node.branches const [rootBranch] = node.branches
const ifStatement = createIfStatement( const ifStatement = createIfStatement(
@ -56,7 +56,7 @@ export function ssrProcessIf(
} }
} }
if (!currentIf.alternate && !disableCommentAsIfAlternate) { if (!currentIf.alternate && !disableComment) {
currentIf.alternate = createBlockStatement([ currentIf.alternate = createBlockStatement([
createCallExpression(`_push`, ['`<!---->`']), createCallExpression(`_push`, ['`<!---->`']),
]) ])

View File

@ -1,5 +1,3 @@
/// <reference types="vite/client" />
// Global compile-time constants // Global compile-time constants
declare var __DEV__: boolean declare var __DEV__: boolean
declare var __TEST__: boolean declare var __TEST__: boolean
@ -9,7 +7,6 @@ declare var __ESM_BUNDLER__: boolean
declare var __ESM_BROWSER__: boolean declare var __ESM_BROWSER__: boolean
declare var __CJS__: boolean declare var __CJS__: boolean
declare var __SSR__: boolean declare var __SSR__: boolean
declare var __COMMIT__: string
declare var __VERSION__: string declare var __VERSION__: string
declare var __COMPAT__: boolean declare var __COMPAT__: boolean
@ -21,10 +18,6 @@ declare var __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__: boolean
declare module '*.vue' {} declare module '*.vue' {}
declare module 'file-saver' {
export function saveAs(blob: any, name: any): void
}
declare module 'estree-walker' { declare module 'estree-walker' {
export function walk<T>( export function walk<T>(
root: T, root: T,

View File

@ -1,4 +1,6 @@
import { import {
type TestElement,
defineComponent,
h, h,
nextTick, nextTick,
nodeOps, nodeOps,
@ -6,6 +8,7 @@ import {
onUnmounted, onUnmounted,
render, render,
serializeInner, serializeInner,
triggerEvent,
} from '@vue/runtime-test' } from '@vue/runtime-test'
import { import {
type DebuggerEvent, type DebuggerEvent,
@ -20,6 +23,7 @@ import {
ref, ref,
shallowRef, shallowRef,
toRaw, toRaw,
triggerRef,
} from '../src' } from '../src'
import { EffectFlags, pauseTracking, resetTracking } from '../src/effect' import { EffectFlags, pauseTracking, resetTracking } from '../src/effect'
import type { ComputedRef, ComputedRefImpl } from '../src/computed' import type { ComputedRef, ComputedRefImpl } from '../src/computed'
@ -33,6 +37,20 @@ describe('reactivity/computed', () => {
expect(cValue.value).toBe(1) expect(cValue.value).toBe(1)
}) })
it('pass oldValue to computed getter', () => {
const count = ref(0)
const oldValue = ref()
const curValue = computed(pre => {
oldValue.value = pre
return count.value
})
expect(curValue.value).toBe(0)
expect(oldValue.value).toBe(undefined)
count.value++
expect(curValue.value).toBe(1)
expect(oldValue.value).toBe(0)
})
it('should compute lazily', () => { it('should compute lazily', () => {
const value = reactive<{ foo?: number }>({}) const value = reactive<{ foo?: number }>({})
const getter = vi.fn(() => value.foo) const getter = vi.fn(() => value.foo)
@ -577,7 +595,7 @@ describe('reactivity/computed', () => {
v.value += ' World' v.value += ' World'
await nextTick() await nextTick()
expect(serializeInner(root)).toBe('Hello World World World') expect(serializeInner(root)).toBe('Hello World World World World')
// expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned() // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
}) })
@ -875,7 +893,7 @@ describe('reactivity/computed', () => {
v.value += ' World' v.value += ' World'
await nextTick() await nextTick()
expect(serializeInner(root)).toBe( expect(serializeInner(root)).toBe(
'Hello World World World | Hello World World World', 'Hello World World World World | Hello World World World World',
) )
}) })
@ -944,4 +962,149 @@ describe('reactivity/computed', () => {
newValue: 2, newValue: 2,
}) })
}) })
// #11797
test('should prevent endless recursion in self-referencing computed getters', async () => {
const Comp = defineComponent({
data() {
return {
counter: 0,
}
},
computed: {
message(): string {
if (this.counter === 0) {
this.counter++
return this.message
} else {
return `Step ${this.counter}`
}
},
},
render() {
return [
h(
'button',
{
onClick: () => {
this.counter++
},
},
'Step',
),
h('p', this.message),
]
},
})
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(serializeInner(root)).toBe(`<button>Step</button><p>Step 1</p>`)
triggerEvent(root.children[1] as TestElement, 'click')
await nextTick()
expect(serializeInner(root)).toBe(`<button>Step</button><p>Step 2</p>`)
})
test('manual trigger computed', () => {
const cValue = computed(() => 1)
triggerRef(cValue)
expect(cValue.value).toBe(1)
})
test('computed should remain live after losing all subscribers', () => {
const state = reactive({ a: 1 })
const p = computed(() => state.a + 1)
const { effect: e } = effect(() => p.value)
e.stop()
expect(p.value).toBe(2)
state.a++
expect(p.value).toBe(3)
})
// #11995
test('computed dep cleanup should not cause property dep to be deleted', () => {
const toggle = ref(true)
const state = reactive({ a: 1 })
const p = computed(() => {
return toggle.value ? state.a : 111
})
const pp = computed(() => state.a)
effect(() => p.value)
expect(pp.value).toBe(1)
toggle.value = false
state.a++
expect(pp.value).toBe(2)
})
// #12020
test('computed value updates correctly after dep cleanup', () => {
const obj = reactive({ foo: 1, flag: 1 })
const c1 = computed(() => obj.foo)
let foo
effect(() => {
foo = obj.flag ? (obj.foo, c1.value) : 0
})
expect(foo).toBe(1)
obj.flag = 0
expect(foo).toBe(0)
obj.foo = 2
obj.flag = 1
expect(foo).toBe(2)
})
// #11928
test('should not lead to exponential perf cost with deeply chained computed', () => {
const start = {
prop1: shallowRef(1),
prop2: shallowRef(2),
prop3: shallowRef(3),
prop4: shallowRef(4),
}
let layer = start
const LAYERS = 1000
for (let i = LAYERS; i > 0; i--) {
const m = layer
const s = {
prop1: computed(() => m.prop2.value),
prop2: computed(() => m.prop1.value - m.prop3.value),
prop3: computed(() => m.prop2.value + m.prop4.value),
prop4: computed(() => m.prop3.value),
}
effect(() => s.prop1.value)
effect(() => s.prop2.value)
effect(() => s.prop3.value)
effect(() => s.prop4.value)
s.prop1.value
s.prop2.value
s.prop3.value
s.prop4.value
layer = s
}
const t = performance.now()
start.prop1.value = 4
start.prop2.value = 3
start.prop3.value = 2
start.prop4.value = 1
expect(performance.now() - t).toBeLessThan(process.env.CI ? 100 : 30)
const end = layer
expect([
end.prop1.value,
end.prop2.value,
end.prop3.value,
end.prop4.value,
]).toMatchObject([-2, -4, 2, 3])
})
}) })

View File

@ -13,6 +13,7 @@ import {
} from '../src/reactive' } from '../src/reactive'
import { computed } from '../src/computed' import { computed } from '../src/computed'
import { effect } from '../src/effect' import { effect } from '../src/effect'
import { targetMap } from '../src/dep'
describe('reactivity/reactive', () => { describe('reactivity/reactive', () => {
test('Object', () => { test('Object', () => {
@ -293,6 +294,13 @@ describe('reactivity/reactive', () => {
expect(() => markRaw(obj)).not.toThrowError() expect(() => markRaw(obj)).not.toThrowError()
}) })
test('markRaw should not redefine on an marked object', () => {
const obj = markRaw({ foo: 1 })
const raw = markRaw(obj)
expect(raw).toBe(obj)
expect(() => markRaw(obj)).not.toThrowError()
})
test('should not observe non-extensible objects', () => { test('should not observe non-extensible objects', () => {
const obj = reactive({ const obj = reactive({
foo: Object.preventExtensions({ a: 1 }), foo: Object.preventExtensions({ a: 1 }),
@ -382,4 +390,23 @@ describe('reactivity/reactive', () => {
count++ count++
} }
}) })
// #11696
test('should use correct receiver on set handler for refs', () => {
const a = reactive(ref(1))
effect(() => a.value)
expect(() => {
a.value++
}).not.toThrow()
})
// #11979
test('should release property Dep instance if it no longer has subscribers', () => {
let obj = { x: 1 }
let a = reactive(obj)
const e = effect(() => a.x)
expect(targetMap.get(obj)?.get('x')).toBeTruthy()
e.effect.stop()
expect(targetMap.get(obj)?.get('x')).toBeFalsy()
})
}) })

View File

@ -303,19 +303,35 @@ describe('reactivity/reactive/Array', () => {
const a2 = reactive([{ val: 3 }]) const a2 = reactive([{ val: 3 }])
const a3 = [4, 5] const a3 = [4, 5]
let result = computed(() => a1.concat(a2, a3)) let result = computed(() => a1.concat(a2, a3, 6, { val: 7 }))
expect(result.value).toStrictEqual([1, { val: 2 }, { val: 3 }, 4, 5]) expect(result.value).toStrictEqual([
1,
{ val: 2 },
{ val: 3 },
4,
5,
6,
{ val: 7 },
])
expect(isReactive(result.value[1])).toBe(false) expect(isReactive(result.value[1])).toBe(false)
expect(isReactive(result.value[2])).toBe(true) expect(isReactive(result.value[2])).toBe(true)
expect(isReactive(result.value[6])).toBe(false)
a1.shift() a1.shift()
expect(result.value).toStrictEqual([{ val: 2 }, { val: 3 }, 4, 5]) expect(result.value).toStrictEqual([
{ val: 2 },
{ val: 3 },
4,
5,
6,
{ val: 7 },
])
a2.pop() a2.pop()
expect(result.value).toStrictEqual([{ val: 2 }, 4, 5]) expect(result.value).toStrictEqual([{ val: 2 }, 4, 5, 6, { val: 7 }])
a3.pop() a3.pop()
expect(result.value).toStrictEqual([{ val: 2 }, 4, 5]) expect(result.value).toStrictEqual([{ val: 2 }, 4, 5, 6, { val: 7 }])
}) })
test('entries', () => { test('entries', () => {
@ -709,13 +725,42 @@ describe('reactivity/reactive/Array', () => {
expect(state.things.every('foo', 'bar', 'baz')).toBe(false) expect(state.things.every('foo', 'bar', 'baz')).toBe(false)
expect(state.things.filter('foo', 'bar', 'baz')).toEqual([foo]) expect(state.things.filter('foo', 'bar', 'baz')).toEqual([foo])
expect(state.things.find('foo', 'bar', 'baz')).toBe(foo)
const _foo = state.things.find('foo', 'bar', 'baz')
expect(isReactive(_foo)).toBe(true)
expect(foo).toStrictEqual(_foo)
expect(state.things.findIndex('foo', 'bar', 'baz')).toBe(1) expect(state.things.findIndex('foo', 'bar', 'baz')).toBe(1)
expect(state.things.findLast('foo', 'bar', 'baz')).toBe(bar)
const _bar = state.things.findLast('foo', 'bar', 'baz')
expect(isReactive(_bar)).toBe(true)
expect(bar).toStrictEqual(_bar)
expect(state.things.findLastIndex('foo', 'bar', 'baz')).toBe(1) expect(state.things.findLastIndex('foo', 'bar', 'baz')).toBe(1)
expect(state.things.forEach('foo', 'bar', 'baz')).toBeUndefined() expect(state.things.forEach('foo', 'bar', 'baz')).toBeUndefined()
expect(state.things.map('foo', 'bar', 'baz')).toEqual(['1', '2', '3']) expect(state.things.map('foo', 'bar', 'baz')).toEqual(['1', '2', '3'])
expect(state.things.some('foo', 'bar', 'baz')).toBe(true) expect(state.things.some('foo', 'bar', 'baz')).toBe(true)
{
class Collection extends Array {
find(matcher: any) {
return super.find(matcher)
}
}
const state = reactive({
// @ts-expect-error
things: new Collection({ foo: '' }),
})
const bar = computed(() => {
return state.things.find((obj: any) => obj.foo === 'bar')
})
bar.value
state.things[0].foo = 'bar'
expect(bar.value).toEqual({ foo: 'bar' })
}
}) })
}) })
}) })

View File

@ -43,6 +43,28 @@ describe('reactivity/ref', () => {
expect(fn).toHaveBeenCalledTimes(2) expect(fn).toHaveBeenCalledTimes(2)
}) })
it('ref wrapped in reactive should not track internal _value access', () => {
const a = ref(1)
const b = reactive(a)
let dummy
const fn = vi.fn(() => {
dummy = b.value // this will observe both b.value and a.value access
})
effect(fn)
expect(fn).toHaveBeenCalledTimes(1)
expect(dummy).toBe(1)
// mutating a.value should only trigger effect once
a.value = 3
expect(fn).toHaveBeenCalledTimes(2)
expect(dummy).toBe(3)
// mutating b.value should trigger the effect twice. (once for a.value change and once for b.value change)
b.value = 5
expect(fn).toHaveBeenCalledTimes(4)
expect(dummy).toBe(5)
})
it('should make nested properties reactive', () => { it('should make nested properties reactive', () => {
const a = ref({ const a = ref({
count: 1, count: 1,

View File

@ -0,0 +1,280 @@
import {
EffectScope,
type Ref,
WatchErrorCodes,
type WatchOptions,
type WatchScheduler,
computed,
onWatcherCleanup,
ref,
watch,
} from '../src'
const queue: (() => void)[] = []
// a simple scheduler for testing purposes
let isFlushPending = false
const resolvedPromise = /*@__PURE__*/ Promise.resolve() as Promise<any>
const nextTick = (fn?: () => any) =>
fn ? resolvedPromise.then(fn) : resolvedPromise
const scheduler: WatchScheduler = (job, isFirstRun) => {
if (isFirstRun) {
job()
} else {
queue.push(job)
flushJobs()
}
}
const flushJobs = () => {
if (isFlushPending) return
isFlushPending = true
resolvedPromise.then(() => {
queue.forEach(job => job())
queue.length = 0
isFlushPending = false
})
}
describe('watch', () => {
test('effect', () => {
let dummy: any
const source = ref(0)
watch(() => {
dummy = source.value
})
expect(dummy).toBe(0)
source.value++
expect(dummy).toBe(1)
})
test('with callback', () => {
let dummy: any
const source = ref(0)
watch(source, () => {
dummy = source.value
})
expect(dummy).toBe(undefined)
source.value++
expect(dummy).toBe(1)
})
test('call option with error handling', () => {
const onError = vi.fn()
const call: WatchOptions['call'] = function call(fn, type, args) {
if (Array.isArray(fn)) {
fn.forEach(f => call(f, type, args))
return
}
try {
fn.apply(null, args)
} catch (e) {
onError(e, type)
}
}
watch(
() => {
throw 'oops in effect'
},
null,
{ call },
)
const source = ref(0)
const effect = watch(
source,
() => {
onWatcherCleanup(() => {
throw 'oops in cleanup'
})
throw 'oops in watch'
},
{ call },
)
expect(onError.mock.calls.length).toBe(1)
expect(onError.mock.calls[0]).toMatchObject([
'oops in effect',
WatchErrorCodes.WATCH_CALLBACK,
])
source.value++
expect(onError.mock.calls.length).toBe(2)
expect(onError.mock.calls[1]).toMatchObject([
'oops in watch',
WatchErrorCodes.WATCH_CALLBACK,
])
effect!.stop()
source.value++
expect(onError.mock.calls.length).toBe(3)
expect(onError.mock.calls[2]).toMatchObject([
'oops in cleanup',
WatchErrorCodes.WATCH_CLEANUP,
])
})
test('watch with onWatcherCleanup', async () => {
let dummy = 0
let source: Ref<number>
const scope = new EffectScope()
scope.run(() => {
source = ref(0)
watch(onCleanup => {
source.value
onCleanup(() => (dummy += 2))
onWatcherCleanup(() => (dummy += 3))
onWatcherCleanup(() => (dummy += 5))
})
})
expect(dummy).toBe(0)
scope.run(() => {
source.value++
})
expect(dummy).toBe(10)
scope.run(() => {
source.value++
})
expect(dummy).toBe(20)
scope.stop()
expect(dummy).toBe(30)
})
test('nested calls to baseWatch and onWatcherCleanup', async () => {
let calls: string[] = []
let source: Ref<number>
let copyist: Ref<number>
const scope = new EffectScope()
scope.run(() => {
source = ref(0)
copyist = ref(0)
// sync by default
watch(
() => {
const current = (copyist.value = source.value)
onWatcherCleanup(() => calls.push(`sync ${current}`))
},
null,
{},
)
// with scheduler
watch(
() => {
const current = copyist.value
onWatcherCleanup(() => calls.push(`post ${current}`))
},
null,
{ scheduler },
)
})
await nextTick()
expect(calls).toEqual([])
scope.run(() => source.value++)
expect(calls).toEqual(['sync 0'])
await nextTick()
expect(calls).toEqual(['sync 0', 'post 0'])
calls.length = 0
scope.run(() => source.value++)
expect(calls).toEqual(['sync 1'])
await nextTick()
expect(calls).toEqual(['sync 1', 'post 1'])
calls.length = 0
scope.stop()
expect(calls).toEqual(['sync 2', 'post 2'])
})
test('once option should be ignored by simple watch', async () => {
let dummy: any
const source = ref(0)
watch(
() => {
dummy = source.value
},
null,
{ once: true },
)
expect(dummy).toBe(0)
source.value++
expect(dummy).toBe(1)
})
// #12033
test('recursive sync watcher on computed', () => {
const r = ref(0)
const c = computed(() => r.value)
watch(c, v => {
if (v > 1) {
r.value--
}
})
expect(r.value).toBe(0)
expect(c.value).toBe(0)
r.value = 10
expect(r.value).toBe(1)
expect(c.value).toBe(1)
})
// edge case where a nested endBatch() causes an effect to be batched in a
// nested batch loop with its .next mutated, causing the outer loop to end
// early
test('nested batch edge case', () => {
// useClamp from VueUse
const clamp = (n: number, min: number, max: number) =>
Math.min(max, Math.max(min, n))
function useClamp(src: Ref<number>, min: number, max: number) {
return computed({
get() {
return (src.value = clamp(src.value, min, max))
},
set(val) {
src.value = clamp(val, min, max)
},
})
}
const src = ref(1)
const clamped = useClamp(src, 1, 5)
watch(src, val => (clamped.value = val))
const spy = vi.fn()
watch(clamped, spy)
src.value = 2
expect(spy).toHaveBeenCalledTimes(1)
src.value = 10
expect(spy).toHaveBeenCalledTimes(2)
})
test('should ensure correct execution order in batch processing', () => {
const dummy: number[] = []
const n1 = ref(0)
const n2 = ref(0)
const sum = computed(() => n1.value + n2.value)
watch(n1, () => {
dummy.push(1)
n2.value++
})
watch(sum, () => dummy.push(2))
watch(n1, () => dummy.push(3))
n1.value++
expect(dummy).toEqual([1, 2, 3])
})
})

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/reactivity", "name": "@vue/reactivity",
"version": "3.5.0-beta.1", "version": "3.5.11",
"description": "@vue/reactivity", "description": "@vue/reactivity",
"main": "index.js", "main": "index.js",
"module": "dist/reactivity.esm-bundler.js", "module": "dist/reactivity.esm-bundler.js",

View File

@ -2,6 +2,7 @@ import { TrackOpTypes } from './constants'
import { endBatch, pauseTracking, resetTracking, startBatch } from './effect' import { endBatch, pauseTracking, resetTracking, startBatch } from './effect'
import { isProxy, isShallow, toRaw, toReactive } from './reactive' import { isProxy, isShallow, toRaw, toReactive } from './reactive'
import { ARRAY_ITERATE_KEY, track } from './dep' import { ARRAY_ITERATE_KEY, track } from './dep'
import { isArray } from '@vue/shared'
/** /**
* Track array iteration and return: * Track array iteration and return:
@ -30,9 +31,9 @@ export const arrayInstrumentations: Record<string | symbol, Function> = <any>{
return iterator(this, Symbol.iterator, toReactive) return iterator(this, Symbol.iterator, toReactive)
}, },
concat(...args: unknown[][]) { concat(...args: unknown[]) {
return reactiveReadArray(this).concat( return reactiveReadArray(this).concat(
...args.map(x => reactiveReadArray(x)), ...args.map(x => (isArray(x) ? reactiveReadArray(x) : x)),
) )
}, },
@ -239,16 +240,21 @@ function apply(
args?: IArguments, args?: IArguments,
) { ) {
const arr = shallowReadArray(self) const arr = shallowReadArray(self)
let methodFn const needsWrap = arr !== self && !isShallow(self)
// @ts-expect-error our code is limited to es2016 but user code is not // @ts-expect-error our code is limited to es2016 but user code is not
if ((methodFn = arr[method]) !== arrayProto[method]) { const methodFn = arr[method]
return methodFn.apply(arr, args)
// #11759
// If the method being called is from a user-extended Array, the arguments will be unknown
// (unknown order and unknown parameter types). In this case, we skip the shallowReadArray
// handling and directly call apply with self.
if (methodFn !== arrayProto[method as any]) {
const result = methodFn.apply(self, args)
return needsWrap ? toReactive(result) : result
} }
let needsWrap = false
let wrappedFn = fn let wrappedFn = fn
if (arr !== self) { if (arr !== self) {
needsWrap = !isShallow(self)
if (needsWrap) { if (needsWrap) {
wrappedFn = function (this: unknown, item, index) { wrappedFn = function (this: unknown, item, index) {
return fn.call(this, toReactive(item), index, self) return fn.call(this, toReactive(item), index, self)

View File

@ -25,10 +25,10 @@ import {
import { isRef } from './ref' import { isRef } from './ref'
import { warn } from './warning' import { warn } from './warning'
const isNonTrackableKeys = /*#__PURE__*/ makeMap(`__proto__,__v_isRef,__isVue`) const isNonTrackableKeys = /*@__PURE__*/ makeMap(`__proto__,__v_isRef,__isVue`)
const builtInSymbols = new Set( const builtInSymbols = new Set(
/*#__PURE__*/ /*@__PURE__*/
Object.getOwnPropertyNames(Symbol) Object.getOwnPropertyNames(Symbol)
// ios10.x Object.getOwnPropertyNames(Symbol) can enumerate 'arguments' and 'caller' // ios10.x Object.getOwnPropertyNames(Symbol) can enumerate 'arguments' and 'caller'
// but accessing them on Symbol leads to TypeError because Symbol is a strict mode // but accessing them on Symbol leads to TypeError because Symbol is a strict mode
@ -165,7 +165,12 @@ class MutableReactiveHandler extends BaseReactiveHandler {
isArray(target) && isIntegerKey(key) isArray(target) && isIntegerKey(key)
? Number(key) < target.length ? Number(key) < target.length
: hasOwn(target, key) : hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver) const result = Reflect.set(
target,
key,
value,
isRef(target) ? target : receiver,
)
// don't trigger if target is something up in the prototype chain of original // don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) { if (target === toRaw(receiver)) {
if (!hadKey) { if (!hadKey) {
@ -235,16 +240,16 @@ class ReadonlyReactiveHandler extends BaseReactiveHandler {
} }
export const mutableHandlers: ProxyHandler<object> = export const mutableHandlers: ProxyHandler<object> =
/*#__PURE__*/ new MutableReactiveHandler() /*@__PURE__*/ new MutableReactiveHandler()
export const readonlyHandlers: ProxyHandler<object> = export const readonlyHandlers: ProxyHandler<object> =
/*#__PURE__*/ new ReadonlyReactiveHandler() /*@__PURE__*/ new ReadonlyReactiveHandler()
export const shallowReactiveHandlers: MutableReactiveHandler = export const shallowReactiveHandlers: MutableReactiveHandler =
/*#__PURE__*/ new MutableReactiveHandler(true) /*@__PURE__*/ new MutableReactiveHandler(true)
// Props handlers are special in the sense that it should not unwrap top-level // Props handlers are special in the sense that it should not unwrap top-level
// refs (in order to allow refs to be explicitly passed down), but should // refs (in order to allow refs to be explicitly passed down), but should
// retain the reactivity of the normal readonly object. // retain the reactivity of the normal readonly object.
export const shallowReadonlyHandlers: ReadonlyReactiveHandler = export const shallowReadonlyHandlers: ReadonlyReactiveHandler =
/*#__PURE__*/ new ReadonlyReactiveHandler(true) /*@__PURE__*/ new ReadonlyReactiveHandler(true)

View File

@ -332,7 +332,7 @@ const [
readonlyInstrumentations, readonlyInstrumentations,
shallowInstrumentations, shallowInstrumentations,
shallowReadonlyInstrumentations, shallowReadonlyInstrumentations,
] = /* #__PURE__*/ createInstrumentations() ] = /* @__PURE__*/ createInstrumentations()
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) { function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
const instrumentations = shallow const instrumentations = shallow
@ -367,20 +367,20 @@ function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
} }
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = { export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: /*#__PURE__*/ createInstrumentationGetter(false, false), get: /*@__PURE__*/ createInstrumentationGetter(false, false),
} }
export const shallowCollectionHandlers: ProxyHandler<CollectionTypes> = { export const shallowCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: /*#__PURE__*/ createInstrumentationGetter(false, true), get: /*@__PURE__*/ createInstrumentationGetter(false, true),
} }
export const readonlyCollectionHandlers: ProxyHandler<CollectionTypes> = { export const readonlyCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: /*#__PURE__*/ createInstrumentationGetter(true, false), get: /*@__PURE__*/ createInstrumentationGetter(true, false),
} }
export const shallowReadonlyCollectionHandlers: ProxyHandler<CollectionTypes> = export const shallowReadonlyCollectionHandlers: ProxyHandler<CollectionTypes> =
{ {
get: /*#__PURE__*/ createInstrumentationGetter(true, true), get: /*@__PURE__*/ createInstrumentationGetter(true, true),
} }
function checkIdentityKeys( function checkIdentityKeys(

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