chore: merge main

This commit is contained in:
AlexVagrant 2024-11-09 14:59:30 +08:00
commit 0fc9925dce
216 changed files with 6194 additions and 2531 deletions

View File

@ -16,6 +16,7 @@ jobs:
uses: ./.github/workflows/test.yml
continuous-release:
if: github.repository == 'vuejs/core'
runs-on: ubuntu-latest
steps:
- name: Checkout

View File

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

View File

@ -18,6 +18,7 @@ jobs:
size-report:
runs-on: ubuntu-latest
if: >
github.repository == 'vuejs/core' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
steps:
@ -65,7 +66,7 @@ jobs:
if_no_artifact_found: warn
- 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
id: size-report

View File

@ -1,3 +1,299 @@
## [3.5.12](https://github.com/vuejs/core/compare/v3.5.11...v3.5.12) (2024-10-11)
### Bug Fixes
* **compiler-dom:** avoid stringify option with null value ([#12096](https://github.com/vuejs/core/issues/12096)) ([f6d9926](https://github.com/vuejs/core/commit/f6d99262364b7444ebab8742158599e8cdd79eaa)), closes [#12093](https://github.com/vuejs/core/issues/12093)
* **compiler-sfc:** do not skip TSInstantiationExpression when transforming props destructure ([#12064](https://github.com/vuejs/core/issues/12064)) ([d3ecde8](https://github.com/vuejs/core/commit/d3ecde8a696ff62c8d0ab067fd1d7ee0565b63c5))
* **compiler-sfc:** use sass modern api if available and avoid deprecation warning ([#11992](https://github.com/vuejs/core/issues/11992)) ([4474c11](https://github.com/vuejs/core/commit/4474c113d1fb1c26298dd6794275d5b5c7cc4d93))
* **compiler:** clone loc to `ifNode` ([#12131](https://github.com/vuejs/core/issues/12131)) ([cde2c06](https://github.com/vuejs/core/commit/cde2c0671b00d4f6111fcbd7aa76e45872f20b0c)), closes [vuejs/language-tools#4911](https://github.com/vuejs/language-tools/issues/4911)
* **custom-element:** properly remove hyphenated attribute ([#12143](https://github.com/vuejs/core/issues/12143)) ([e16e9a7](https://github.com/vuejs/core/commit/e16e9a7341e7cfb3c443da4e5e5b06e8158712c3)), closes [#12139](https://github.com/vuejs/core/issues/12139)
* **defineModel:** handle kebab-case model correctly ([#12063](https://github.com/vuejs/core/issues/12063)) ([c0418a3](https://github.com/vuejs/core/commit/c0418a3b8fa96a0b108ab71b7aab5d3388f90557)), closes [#12060](https://github.com/vuejs/core/issues/12060)
* **deps:** update dependency monaco-editor to ^0.52.0 ([#12119](https://github.com/vuejs/core/issues/12119)) ([f7cbea2](https://github.com/vuejs/core/commit/f7cbea2111c7770a180b640f36f6a5d4d6abc698))
* **hydration:** provide compat fallback for idle callback hydration strategy ([#11935](https://github.com/vuejs/core/issues/11935)) ([1ae545a](https://github.com/vuejs/core/commit/1ae545a3786abef983be1c969726489685569c92))
* **reactivity:** trigger reactivity for Map key `undefined` ([#12055](https://github.com/vuejs/core/issues/12055)) ([7ad289e](https://github.com/vuejs/core/commit/7ad289e1e7fea654524008ff91e43a8b8a55ef22)), closes [#12054](https://github.com/vuejs/core/issues/12054)
* **runtime-core:** allow symbol values for slot prop key ([#12069](https://github.com/vuejs/core/issues/12069)) ([d9d4d4e](https://github.com/vuejs/core/commit/d9d4d4e158cd51a9ddda249f29de8467f60b2792)), closes [#12068](https://github.com/vuejs/core/issues/12068)
* **runtime-core:** fix required prop check false positive for kebab-case edge cases ([#12034](https://github.com/vuejs/core/issues/12034)) ([9da1ac1](https://github.com/vuejs/core/commit/9da1ac156552ac449754e1373aac7e349841becb)), closes [#12011](https://github.com/vuejs/core/issues/12011)
* **runtime-dom:** prevent unnecessary updates in v-model checkbox when value is unchanged ([#12146](https://github.com/vuejs/core/issues/12146)) ([ea943af](https://github.com/vuejs/core/commit/ea943afe404c4ca4b729906c5e8daf7aa2ccde9b)), closes [#12144](https://github.com/vuejs/core/issues/12144)
* **teleport:** handle disabled teleport with updateCssVars ([#12113](https://github.com/vuejs/core/issues/12113)) ([76a8223](https://github.com/vuejs/core/commit/76a8223199c148b79a5c0ea19e235164809760cd)), closes [#12112](https://github.com/vuejs/core/issues/12112)
* **transition/ssr:** make transition appear work with Suspense in SSR ([#12047](https://github.com/vuejs/core/issues/12047)) ([f1a4f67](https://github.com/vuejs/core/commit/f1a4f67aedfe83e440c54222213f070774faa421)), closes [#12046](https://github.com/vuejs/core/issues/12046)
* **types:** ensure `this.$props` type does not include `string` ([#12123](https://github.com/vuejs/core/issues/12123)) ([704173e](https://github.com/vuejs/core/commit/704173e24276706de672cca6c9507e4dd9651197)), closes [#12122](https://github.com/vuejs/core/issues/12122)
* **types:** retain union type narrowing with defaults applied ([#12108](https://github.com/vuejs/core/issues/12108)) ([05685a9](https://github.com/vuejs/core/commit/05685a9d7c42d4cd37169b867833776b91154fed)), closes [#12106](https://github.com/vuejs/core/issues/12106)
* **useId:** ensure useId consistency when using serverPrefetch ([#12128](https://github.com/vuejs/core/issues/12128)) ([b4d3534](https://github.com/vuejs/core/commit/b4d35349d8bc39aa15bd3f1094d230e5928b177c)), closes [#12102](https://github.com/vuejs/core/issues/12102)
* **watch:** watchEffect clean-up with SSR ([#12097](https://github.com/vuejs/core/issues/12097)) ([b094c72](https://github.com/vuejs/core/commit/b094c72b3d40c52c7124f145a9db028509a11202)), closes [#11956](https://github.com/vuejs/core/issues/11956)
### Performance Improvements
* **reactivity:** avoid unnecessary recursion in removeSub ([#12135](https://github.com/vuejs/core/issues/12135)) ([ec917cf](https://github.com/vuejs/core/commit/ec917cfdb9d0169cd0835d3a0e28244242657dc9))
## [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)

View File

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

View File

@ -1,14 +1,14 @@
{
"private": true,
"version": "3.5.0-beta.3",
"packageManager": "pnpm@9.7.1",
"version": "3.5.12",
"packageManager": "pnpm@9.12.3",
"type": "module",
"scripts": {
"dev": "node scripts/dev.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",
"clean": "rimraf packages/*/dist temp .eslintcache",
"size": "run-s \"size-*\" && tsx scripts/usage-size.ts",
"build-dts": "tsc -p tsconfig.build.json --noCheck && rollup -c rollup.dts.config.js",
"clean": "rimraf --glob packages/*/dist temp .eslintcache",
"size": "run-s \"size-*\" && node scripts/usage-size.js",
"size-global": "node scripts/build.js vue runtime-dom -f global -p --size",
"size-esm-runtime": "node scripts/build.js vue -f esm-bundler-runtime",
"size-esm": "node scripts/build.js runtime-dom runtime-core reactivity shared -f esm-bundler",
@ -17,11 +17,11 @@
"format": "prettier --write --cache .",
"format-check": "prettier --check --cache .",
"test": "vitest",
"test-unit": "vitest -c vitest.unit.config.ts",
"test-e2e": "node scripts/build.js vue -f global -d && vitest -c vitest.e2e.config.ts",
"test-unit": "vitest --project unit",
"test-e2e": "node scripts/build.js vue -f global -d && vitest --project e2e",
"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-coverage": "vitest -c vitest.unit.config.ts --coverage",
"test-coverage": "vitest run --project unit --coverage",
"test-bench": "vitest bench",
"release": "node scripts/release.js",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
@ -32,7 +32,7 @@
"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",
"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-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",
@ -61,54 +61,53 @@
"devDependencies": {
"@babel/parser": "catalog:",
"@babel/types": "catalog:",
"@rollup/plugin-alias": "^5.1.0",
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-commonjs": "^28.0.1",
"@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",
"@swc/core": "^1.7.11",
"@swc/core": "^1.7.42",
"@types/hash-sum": "^1.0.2",
"@types/node": "^20.16.0",
"@types/node": "^22.8.7",
"@types/semver": "^7.5.8",
"@types/serve-handler": "^6.1.4",
"@vitest/coverage-istanbul": "^2.0.5",
"@vitest/coverage-v8": "^2.1.1",
"@vue/consolidate": "1.0.0",
"conventional-changelog-cli": "^5.0.0",
"enquirer": "^2.4.1",
"esbuild": "^0.23.1",
"esbuild": "^0.24.0",
"esbuild-plugin-polyfill-node": "^0.3.0",
"eslint": "^9.9.0",
"eslint-plugin-import-x": "^3.1.0",
"eslint-plugin-vitest": "^0.5.4",
"eslint": "^9.14.0",
"eslint-plugin-import-x": "^4.4.0",
"@vitest/eslint-plugin": "^1.0.1",
"estree-walker": "catalog:",
"jsdom": "^24.1.1",
"lint-staged": "^15.2.9",
"jsdom": "^25.0.0",
"lint-staged": "^15.2.10",
"lodash": "^4.17.21",
"magic-string": "^0.30.11",
"markdown-table": "^3.0.3",
"magic-string": "^0.30.12",
"markdown-table": "^3.0.4",
"marked": "13.0.3",
"npm-run-all2": "^6.2.2",
"picocolors": "^1.0.1",
"npm-run-all2": "^7.0.1",
"picocolors": "^1.1.1",
"prettier": "^3.3.3",
"pretty-bytes": "^6.1.1",
"pug": "^3.0.3",
"puppeteer": "~23.0.2",
"puppeteer": "~23.3.0",
"rimraf": "^6.0.1",
"rollup": "^4.21.0",
"rollup": "^4.24.3",
"rollup-plugin-dts": "^6.1.1",
"rollup-plugin-esbuild": "^6.1.1",
"rollup-plugin-polyfill-node": "^0.13.0",
"semver": "^7.6.3",
"serve": "^14.2.3",
"serve-handler": "^6.1.5",
"serve": "^14.2.4",
"serve-handler": "^6.1.6",
"simple-git-hooks": "^2.11.1",
"todomvc-app-css": "^2.4.3",
"tslib": "^2.6.3",
"tsx": "^4.17.0",
"typescript": "~5.5.4",
"typescript-eslint": "^8.1.0",
"tslib": "^2.8.1",
"typescript": "~5.6.2",
"typescript-eslint": "^8.12.2",
"vite": "catalog:",
"vitest": "^2.0.5"
"vitest": "^2.1.1"
},
"pnpm": {
"peerDependencyRules": {

View File

@ -3,12 +3,17 @@ import { expectType } from './utils'
const app = createApp({})
app.directive<HTMLElement, string>('custom', {
mounted(el, binding) {
expectType<HTMLElement>(el)
expectType<string>(binding.value)
app.directive<HTMLElement, string, 'prevent' | 'stop', 'arg1' | 'arg2'>(
'custom',
{
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
expectType<number>(binding.value)
// @ts-expect-error not any
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', () => {
const MixinA = defineComponent({
emits: ['bar'],
@ -1021,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
defineComponent({
setup(props, { emit }) {
@ -1790,6 +1822,15 @@ describe('__typeRefs backdoor, object syntax', () => {
expectType<number>(refs.child.$refs.foo)
})
describe('__typeEl backdoor', () => {
const Comp = defineComponent({
__typeEl: {} as HTMLAnchorElement,
})
const c = new Comp()
expectType<HTMLAnchorElement>(c.$el)
})
defineComponent({
props: {
foo: [String, null],
@ -2027,3 +2068,13 @@ expectString(instance.actionText)
// public prop on $props should be optional
// @ts-expect-error
expectString(instance.$props.actionText)
// #12122
defineComponent({
props: { foo: String },
render() {
expectType<{ readonly foo?: string }>(this.$props)
// @ts-expect-error
expectType<string>(this.$props)
},
})

View File

@ -189,6 +189,24 @@ describe('allow getter and setter types to be unrelated', <T>() => {
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
describe('allow computed getter and setter types to be unrelated', () => {
const obj = ref({

View File

@ -240,6 +240,23 @@ describe('withDefaults w/ defineProp type is different from the defaults type',
res1.value
})
describe('withDefaults w/ defineProp discriminate union type', () => {
const props = withDefaults(
defineProps<
{ type: 'button'; buttonType?: 'submit' } | { type: 'link'; href: string }
>(),
{
type: 'button',
},
)
if (props.type === 'button') {
expectType<'submit' | undefined>(props.buttonType)
}
if (props.type === 'link') {
expectType<string>(props.href)
}
})
describe('defineProps w/ runtime declaration', () => {
// runtime declaration
const props = defineProps({
@ -427,6 +444,51 @@ describe('defineModel', () => {
defineModel<string>({ default: 123 })
// @ts-expect-error unknown props option
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', () => {

View File

@ -121,3 +121,5 @@ expectType<JSX.Element>(
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:"
},
"dependencies": {
"@vue/repl": "^4.3.1",
"@vue/repl": "^4.4.2",
"file-saver": "^2.0.5",
"jszip": "^3.10.1",
"vue": "workspace:*"

View File

@ -14,6 +14,12 @@ setVH()
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({
runtimeDev: import.meta.env.PROD
? `${location.origin}/vue.runtime.esm-browser.js`
@ -89,6 +95,11 @@ function toggleSSR() {
useSSRMode.value = !useSSRMode.value
}
function toggleAutoSave() {
autoSave.value = !autoSave.value
localStorage.setItem(AUTO_SAVE_STORAGE_KEY, String(autoSave.value))
}
function reloadPage() {
replRef.value?.reload()
}
@ -111,9 +122,12 @@ onMounted(() => {
:store="store"
:prod="productionMode"
:ssr="useSSRMode"
:autoSave="autoSave"
:theme="theme"
@toggle-theme="toggleTheme"
@toggle-prod="toggleProdMode"
@toggle-ssr="toggleSSR"
@toggle-autosave="toggleAutoSave"
@reload-page="reloadPage"
/>
<Repl
@ -123,6 +137,8 @@ onMounted(() => {
@keydown.ctrl.s.prevent
@keydown.meta.s.prevent
:ssr="useSSRMode"
:model-value="autoSave"
:editorOptions="{ autoSaveText: false }"
:store="store"
:showCompileOutput="true"
:autoResize="true"

View File

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

View File

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

View File

@ -11,7 +11,7 @@
"enableNonBrowserBranches": true
},
"dependencies": {
"monaco-editor": "^0.50.0",
"source-map-js": "^1.2.0"
"monaco-editor": "^0.52.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',
rawName: 'v-on.enter',
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,
loc: {
start: { offset: 5, line: 1, column: 6 },
@ -1377,7 +1397,46 @@ describe('compiler: parse', () => {
name: 'on',
rawName: 'v-on.enter.exact',
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,
loc: {
start: { offset: 5, line: 1, column: 6 },
@ -1406,7 +1465,46 @@ describe('compiler: parse', () => {
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,
loc: {
start: { offset: 5, line: 1, column: 6 },
@ -1435,7 +1533,27 @@ describe('compiler: parse', () => {
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,
loc: {
start: { offset: 5, line: 1, column: 6 },
@ -1530,7 +1648,27 @@ describe('compiler: parse', () => {
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: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'b',
@ -1569,7 +1707,27 @@ describe('compiler: parse', () => {
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: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'b',
@ -1649,7 +1807,27 @@ describe('compiler: parse', () => {
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: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'b',
@ -1841,6 +2019,21 @@ describe('compiler: parse', () => {
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', () => {
@ -2176,6 +2369,7 @@ describe('compiler: parse', () => {
test('should remove leading newline character immediately following the pre element start tag', () => {
const ast = parse(`<pre>\n foo bar </pre>`, {
isPreTag: tag => tag === 'pre',
isIgnoreNewlineTag: tag => tag === 'pre',
})
expect(ast.children).toHaveLength(1)
const preElement = ast.children[0] as ElementNode

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -72,6 +72,7 @@ export const defaultParserOptions: MergedParserOptions = {
getNamespace: () => Namespaces.HTML,
isVoidTag: NO,
isPreTag: NO,
isIgnoreNewlineTag: NO,
isCustomElement: NO,
onError: defaultOnError,
onWarn: defaultOnWarn,
@ -225,7 +226,7 @@ const tokenizer = new Tokenizer(stack, {
rawName: raw,
exp: undefined,
arg: undefined,
modifiers: raw === '.' ? ['prop'] : [],
modifiers: raw === '.' ? [createSimpleExpression('prop')] : [],
loc: getLoc(start),
}
if (name === 'pre') {
@ -273,7 +274,8 @@ const tokenizer = new Tokenizer(stack, {
setLocEnd(arg.loc, end)
}
} 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 (
__COMPAT__ &&
currentProp.name === 'bind' &&
(syncIndex = currentProp.modifiers.indexOf('sync')) > -1 &&
(syncIndex = currentProp.modifiers.findIndex(
mod => mod.content === 'sync',
)) > -1 &&
checkCompatEnabled(
CompilerDeprecationTypes.COMPILER_V_BIND_SYNC,
currentOptions,
@ -630,7 +634,7 @@ function onCloseTag(el: ElementNode, end: number, isImplied = false) {
}
// refine element type
const { tag, ns } = el
const { tag, ns, children } = el
if (!inVPre) {
if (tag === 'slot') {
el.tagType = ElementTypes.SLOT
@ -643,8 +647,18 @@ function onCloseTag(el: ElementNode, end: number, isImplied = false) {
// whitespace management
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)) {
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
}
@ -927,6 +933,10 @@ function getLoc(start: number, end?: number): SourceLocation {
}
}
export function cloneLoc(loc: SourceLocation): SourceLocation {
return getLoc(loc.start.offset, loc.end.offset)
}
function setLocEnd(loc: SourceLocation, end: number) {
loc.end = tokenizer.getPos(end)
loc.source = getSlice(loc.start.offset, end)

View File

@ -438,7 +438,7 @@ export default class Tokenizer {
// We have to parse entities in <title> and <textarea> tags.
if (!__BROWSER__ && c === CharCodes.Amp) {
this.startEntity()
} else if (c === this.delimiterOpen[0]) {
} else if (!this.inVPre && c === this.delimiterOpen[0]) {
// We also need to handle interpolation
this.state = State.InterpolationOpen
this.delimiterIndex = 0

View File

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

View File

@ -665,7 +665,7 @@ export function buildProps(
}
// 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
}

View File

@ -44,7 +44,7 @@ import { parseExpression } from '@babel/parser'
import { IS_REF, UNREF } from '../runtimeHelpers'
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) => {
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
if (modifiers.includes('camel')) {
if (modifiers.some(mod => mod.content === 'camel')) {
if (arg.type === NodeTypes.SIMPLE_EXPRESSION) {
if (arg.isStatic) {
arg.content = camelize(arg.content)
@ -83,10 +83,10 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => {
}
if (!context.inSSR) {
if (modifiers.includes('prop')) {
if (modifiers.some(mod => mod.content === 'prop')) {
injectPrefix(arg, '.')
}
if (modifiers.includes('attr')) {
if (modifiers.some(mod => mod.content === 'attr')) {
injectPrefix(arg, '^')
}
}

View File

@ -30,9 +30,10 @@ import {
import { ErrorCodes, createCompilerError } from '../errors'
import { processExpression } from './transformExpression'
import { validateBrowserExpression } from '../validateExpression'
import { cloneLoc } from '../parser'
import { CREATE_COMMENT, FRAGMENT } from '../runtimeHelpers'
import { findDir, findProp, getMemoedVNodeCall, injectProp } from '../utils'
import { PatchFlagNames, PatchFlags } from '@vue/shared'
import { PatchFlags } from '@vue/shared'
export const transformIf: NodeTransform = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
@ -110,7 +111,7 @@ export function processIf(
const branch = createIfBranch(node, dir)
const ifNode: IfNode = {
type: NodeTypes.IF,
loc: node.loc,
loc: cloneLoc(node.loc),
branches: [branch],
}
context.replaceNode(ifNode)
@ -264,7 +265,6 @@ function createChildrenCodegenNode(
return vnodeCall
} else {
let patchFlag = PatchFlags.STABLE_FRAGMENT
let patchFlagText = PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
// check if the fragment actually contains a single valid child with
// the rest being comments
if (
@ -273,7 +273,6 @@ function createChildrenCodegenNode(
children.filter(c => c.type !== NodeTypes.COMMENT).length === 1
) {
patchFlag |= PatchFlags.DEV_ROOT_FRAGMENT
patchFlagText += `, ${PatchFlagNames[PatchFlags.DEV_ROOT_FRAGMENT]}`
}
return createVNodeCall(

View File

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

View File

@ -273,7 +273,7 @@ export function advancePositionWithMutation(
}
export function assert(condition: boolean, msg?: string): void {
/* istanbul ignore if */
/* v8 ignore next 3 */
if (!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', () => {
const ast = parse(
'<TextArea>some<div>text</div>and<!--comment--></TextArea>',

View File

@ -1,5 +1,17 @@
// 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`] = `
"const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
@ -20,6 +32,23 @@ return function render(_ctx, _cache) {
}"
`;
exports[`stringify static html > should bail for <option> elements with null values 1`] = `
"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("select", null, [
_createElementVNode("option", { value: null }),
_createElementVNode("option", { value: "1" }),
_createElementVNode("option", { value: "1" }),
_createElementVNode("option", { value: "1" }),
_createElementVNode("option", { value: "1" }),
_createElementVNode("option", { value: "1" })
], -1 /* HOISTED */)
])))
}"
`;
exports[`stringify static html > should bail for <option> elements with number values 1`] = `
"const { createElementVNode: _createElementVNode, 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
test('stringify v-html', () => {
const { code } = compileWithStringify(`
@ -451,4 +469,29 @@ describe('stringify static html', () => {
expect(ast.cached).toMatchObject([cachedArrayBailedMatcher()])
expect(code).toMatchSnapshot()
})
test('should bail for <option> elements with null values', () => {
const { ast, code } = compileWithStringify(
`<div><select><option :value="null" />${repeat(
`<option value="1" />`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</select></div>`,
)
expect(ast.cached).toMatchObject([cachedArrayBailedMatcher()])
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",
"version": "3.5.0-beta.3",
"version": "3.5.12",
"description": "@vue/compiler-dom",
"main": "index.js",
"module": "dist/compiler-dom.esm-bundler.js",

View File

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

View File

@ -16,8 +16,6 @@ import {
type TemplateChildNode,
type TextCallNode,
type TransformContext,
type VNodeCall,
createArrayExpression,
createCallExpression,
isStaticArgOf,
} from '@vue/compiler-core'
@ -26,6 +24,7 @@ import {
isArray,
isBooleanAttr,
isKnownHtmlAttr,
isKnownMathMLAttr,
isKnownSvgAttr,
isString,
isSymbol,
@ -106,15 +105,23 @@ export const stringifyStatic: HoistTransform = (children, context, parent) => {
String(currentChunk.length),
])
const deleteCount = currentChunk.length - 1
if (isParentCached) {
;((parent.codegenNode as VNodeCall).children as CacheExpression).value =
createArrayExpression([staticCall])
// if the parent is cached, then `children` is also the value of the
// CacheExpression. Just replace the corresponding range in the cached
// list with staticCall.
children.splice(
currentIndex - currentChunk.length,
currentChunk.length,
// @ts-expect-error
staticCall,
)
} else {
// replace the first node's hoisted expression with the static vnode call
;(currentChunk[0].codegenNode as CacheExpression).value = staticCall
if (currentChunk.length > 1) {
// remove merged nodes from children
const deleteCount = currentChunk.length - 1
children.splice(currentIndex - currentChunk.length + 1, deleteCount)
// also adjust index for the remaining cache items
const cacheIndex = context.cached.indexOf(
@ -128,9 +135,9 @@ export const stringifyStatic: HoistTransform = (children, context, parent) => {
}
context.cached.splice(cacheIndex - deleteCount + 1, deleteCount)
}
return deleteCount
}
}
return deleteCount
}
return 0
}
@ -184,11 +191,13 @@ const isStringifiableAttr = (name: string, ns: Namespaces) => {
? isKnownHtmlAttr(name)
: ns === Namespaces.SVG
? 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`,
)
@ -252,8 +261,7 @@ function analyzeNode(node: StringifiableNode): [number, number] | false {
isOptionTag &&
isStaticArgOf(p.arg, 'value') &&
p.exp &&
p.exp.ast &&
p.exp.ast.type !== 'StringLiteral'
!p.exp.isStatic
) {
return bail()
}

View File

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

View File

@ -20,7 +20,7 @@ exports[`SFC analyze <script> bindings > auto name inference > do not overwrite
name: 'Baz'
})
export default /*#__PURE__*/Object.assign(__default__, {
export default /*@__PURE__*/Object.assign(__default__, {
setup(__props, { expose: __expose }) {
__expose();
const a = 1
@ -36,7 +36,7 @@ exports[`SFC analyze <script> bindings > auto name inference > do not overwrite
name: 'Baz'
}
export default /*#__PURE__*/Object.assign(__default__, {
export default /*@__PURE__*/Object.assign(__default__, {
setup(__props, { expose: __expose }) {
__expose();
const a = 1
@ -53,7 +53,7 @@ exports[`SFC compile <script setup> > <script> and <script setup> co-usage > exp
}
const __default__ = fn();
export default /*#__PURE__*/Object.assign(__default__, {
export default /*@__PURE__*/Object.assign(__default__, {
setup(__props, { expose: __expose }) {
__expose();
@ -91,7 +91,7 @@ exports[`SFC compile <script setup> > <script> and <script setup> co-usage > scr
const __default__ = {}
export default /*#__PURE__*/Object.assign(__default__, {
export default /*@__PURE__*/Object.assign(__default__, {
setup(__props, { expose: __expose }) {
__expose();
@ -110,7 +110,7 @@ exports[`SFC compile <script setup> > <script> and <script setup> co-usage > scr
const __default__ = {}
export default /*#__PURE__*/Object.assign(__default__, {
export default /*@__PURE__*/Object.assign(__default__, {
setup(__props, { expose: __expose }) {
__expose();
@ -131,7 +131,7 @@ import { x } from './x'
}
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
...__default__,
setup(__props, { expose: __expose }) {
__expose();
@ -154,7 +154,7 @@ exports[`SFC compile <script setup> > <script> and <script setup> co-usage > scr
const __default__ = def
export default /*#__PURE__*/Object.assign(__default__, {
export default /*@__PURE__*/Object.assign(__default__, {
setup(__props, { expose: __expose }) {
__expose();
@ -174,7 +174,7 @@ exports[`SFC compile <script setup> > <script> and <script setup> co-usage > spa
some:'option'
}
export default /*#__PURE__*/Object.assign(__default__, {
export default /*@__PURE__*/Object.assign(__default__, {
setup(__props, { expose: __expose }) {
__expose();
@ -194,7 +194,7 @@ exports[`SFC compile <script setup> > <script> and <script setup> co-usage > spa
some:'option'
}
export default /*#__PURE__*/Object.assign(__default__, {
export default /*@__PURE__*/Object.assign(__default__, {
setup(__props, { expose: __expose }) {
__expose();
@ -880,11 +880,13 @@ export default {
const count = ref(0)
const style = { color: 'red' }
const height = ref(0)
return (_ctx, _push, _parent, _attrs) => {
const _cssVars = { style: {
"--xxxxxxxx-count": (count.value),
"--xxxxxxxx-style\\\\.color": (style.color)
"--xxxxxxxx-style\\\\.color": (style.color),
"--xxxxxxxx-height\\\\ \\\\+\\\\ \\\\\\"px\\\\\\"": (height.value + "px")
}}
_push(\`<!--[--><div\${
_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`] = `
"
export default {
@ -1140,7 +1165,7 @@ exports[`SFC compile <script setup> > with TypeScript > const Enum 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
const enum Foo { A = 123 }
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) {
__expose();
@ -1156,7 +1181,7 @@ exports[`SFC compile <script setup> > with TypeScript > hoist type declarations
export interface Foo {}
type Bar = {}
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
setup(__props, { 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 Bar, Baz } from './main.ts'
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) {
__expose();
@ -1187,7 +1212,7 @@ exports[`SFC compile <script setup> > with TypeScript > runtime Enum 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
enum Foo { A = 123 }
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) {
__expose();
@ -1205,7 +1230,7 @@ exports[`SFC compile <script setup> > with TypeScript > runtime Enum in normal s
const enum C { C = "C" }
enum B { B = "B" }
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) {
__expose();
@ -1221,7 +1246,7 @@ exports[`SFC compile <script setup> > with TypeScript > with generic attribute 1
"import { defineComponent as _defineComponent } from 'vue'
type Bar = {}
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) {
__expose();
@ -1250,7 +1275,7 @@ exports[`SFC genDefaultAs > <script setup> only w/ ts 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
const a = 1
const _sfc_ = /*#__PURE__*/_defineComponent({
const _sfc_ = /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) {
__expose();
@ -1265,7 +1290,7 @@ exports[`SFC genDefaultAs > <script> + <script setup> 1`] = `
"
const __default__ = {}
const _sfc_ = /*#__PURE__*/Object.assign(__default__, {
const _sfc_ = /*@__PURE__*/Object.assign(__default__, {
setup(__props, { expose: __expose }) {
__expose();
@ -1281,7 +1306,7 @@ exports[`SFC genDefaultAs > <script> + <script setup> 2`] = `
"
const __default__ = {}
const _sfc_ = /*#__PURE__*/Object.assign(__default__, {
const _sfc_ = /*@__PURE__*/Object.assign(__default__, {
setup(__props, { expose: __expose }) {
__expose();
@ -1298,7 +1323,7 @@ exports[`SFC genDefaultAs > <script> + <script setup> w/ ts 1`] = `
const __default__ = {}
const _sfc_ = /*#__PURE__*/_defineComponent({
const _sfc_ = /*@__PURE__*/_defineComponent({
...__default__,
setup(__props, { expose: __expose }) {
__expose();

View File

@ -472,6 +472,23 @@ describe('SFC compile <script setup>', () => {
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', () => {
const { content } = compile(
`<script setup>
@ -606,6 +623,7 @@ describe('SFC compile <script setup>', () => {
import { ref } from 'vue'
const count = ref(0)
const style = { color: 'red' }
const height = ref(0)
</script>
<template>
<div>{{ count }}</div>
@ -614,6 +632,7 @@ describe('SFC compile <script setup>', () => {
<style>
div { color: v-bind(count) }
span { color: v-bind(style.color) }
span { color: v-bind(height + "px") }
</style>
`,
{
@ -629,6 +648,9 @@ describe('SFC compile <script setup>', () => {
expect(content).not.toMatch(`useCssVars`)
expect(content).toMatch(`"--${mockId}-count": (count.value)`)
expect(content).toMatch(`"--${mockId}-style\\\\.color": (style.color)`)
expect(content).toMatch(
`"--${mockId}-height\\\\ \\\\+\\\\ \\\\\\"px\\\\\\"": (height.value + "px")`,
)
assertCode(content)
})
@ -1352,7 +1374,7 @@ describe('SFC genDefaultAs', () => {
)
expect(content).not.toMatch('export default')
expect(content).toMatch(
`const _sfc_ = /*#__PURE__*/Object.assign(__default__`,
`const _sfc_ = /*@__PURE__*/Object.assign(__default__`,
)
assertCode(content)
})
@ -1371,7 +1393,7 @@ describe('SFC genDefaultAs', () => {
)
expect(content).not.toMatch('export default')
expect(content).toMatch(
`const _sfc_ = /*#__PURE__*/Object.assign(__default__`,
`const _sfc_ = /*@__PURE__*/Object.assign(__default__`,
)
assertCode(content)
})
@ -1400,7 +1422,7 @@ describe('SFC genDefaultAs', () => {
},
)
expect(content).not.toMatch('export default')
expect(content).toMatch(`const _sfc_ = /*#__PURE__*/_defineComponent(`)
expect(content).toMatch(`const _sfc_ = /*@__PURE__*/_defineComponent(`)
assertCode(content)
})
@ -1418,7 +1440,7 @@ describe('SFC genDefaultAs', () => {
)
expect(content).not.toMatch('export default')
expect(content).toMatch(
`const _sfc_ = /*#__PURE__*/_defineComponent({\n ...__default__`,
`const _sfc_ = /*@__PURE__*/_defineComponent({\n ...__default__`,
)
assertCode(content)
})

View File

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

View File

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

View File

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

View File

@ -24,7 +24,7 @@ interface Props {
foo?: number;
}
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
__name: 'app.ce',
props: {
foo: { default: 5.5, type: Number }
@ -43,7 +43,7 @@ return { props }
exports[`defineProps > custom element retains the props type & production mode 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
__name: 'app.ce',
props: {
foo: {type: Number}
@ -62,7 +62,7 @@ return { props }
exports[`defineProps > defineProps w/ runtime options 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
props: { foo: String },
setup(__props, { expose: __expose }) {
__expose();
@ -78,7 +78,7 @@ return { props }
exports[`defineProps > destructure without enabling reactive destructure 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
props: {
foo: { type: null, required: true }
},
@ -96,7 +96,7 @@ return { foo }
exports[`defineProps > should escape names w/ special symbols 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
props: {
"spa ce": { type: null, required: true },
"exclamation!mark": { type: null, required: true },
@ -141,7 +141,7 @@ return { }
exports[`defineProps > w/ TS assertion 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
props: ['foo'],
setup(__props, { expose: __expose }) {
__expose();
@ -158,7 +158,7 @@ exports[`defineProps > w/ exported interface 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
export interface Props { x?: number }
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
props: {
x: { type: Number, required: false }
},
@ -178,7 +178,7 @@ exports[`defineProps > w/ exported interface in normal script 1`] = `
export interface Props { x?: number }
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
props: {
x: { type: Number, required: false }
},
@ -197,7 +197,7 @@ exports[`defineProps > w/ exported type alias 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
export type Props = { x?: number }
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
props: {
x: { type: Number, required: false }
},
@ -222,7 +222,34 @@ interface Bar extends Foo { y?: number }
interface Foo { x?: number }
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
props: {
z: { type: Number, required: true },
y: { type: String, required: true },
x: { type: Number, required: false }
},
setup(__props: any, { expose: __expose }) {
__expose();
return { }
}
})"
`;
exports[`defineProps > w/ extends intersection type 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
type Foo = {
x?: number;
};
interface Props extends Foo {
z: number
y: string
}
export default /*@__PURE__*/_defineComponent({
props: {
z: { type: Number, required: true },
y: { type: String, required: true },
@ -259,7 +286,7 @@ exports[`defineProps > w/ interface 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
interface Props { x?: number }
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
props: {
x: { type: Number, required: false }
},
@ -268,6 +295,31 @@ export default /*#__PURE__*/_defineComponent({
return { }
}
})"
`;
exports[`defineProps > w/ intersection type 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
type Foo = {
x?: number;
};
type Bar = {
y: string;
};
export default /*@__PURE__*/_defineComponent({
props: {
x: { type: Number, required: false },
y: { type: String, required: true }
},
setup(__props: any, { expose: __expose }) {
__expose();
return { }
}
@ -296,7 +348,7 @@ interface Test {}
type Alias = number[]
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
props: {
string: { type: String, required: true },
number: { type: Number, required: true },
@ -353,7 +405,7 @@ exports[`defineProps > w/ type alias 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
type Props = { x?: number }
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
props: {
x: { type: Number, required: false }
},
@ -372,8 +424,8 @@ exports[`defineProps > withDefaults (dynamic) 1`] = `
"import { mergeDefaults as _mergeDefaults, defineComponent as _defineComponent } from 'vue'
import { defaults } from './foo'
export default /*#__PURE__*/_defineComponent({
props: /*#__PURE__*/_mergeDefaults({
export default /*@__PURE__*/_defineComponent({
props: /*@__PURE__*/_mergeDefaults({
foo: { type: String, required: false },
bar: { type: Number, required: false },
baz: { type: Boolean, required: true }
@ -393,8 +445,8 @@ exports[`defineProps > withDefaults (dynamic) w/ production mode 1`] = `
"import { mergeDefaults as _mergeDefaults, defineComponent as _defineComponent } from 'vue'
import { defaults } from './foo'
export default /*#__PURE__*/_defineComponent({
props: /*#__PURE__*/_mergeDefaults({
export default /*@__PURE__*/_defineComponent({
props: /*@__PURE__*/_mergeDefaults({
foo: { type: Function },
bar: { type: Boolean },
baz: { type: [Boolean, Function] },
@ -415,8 +467,8 @@ exports[`defineProps > withDefaults (reference) 1`] = `
"import { mergeDefaults as _mergeDefaults, defineComponent as _defineComponent } from 'vue'
import { defaults } from './foo'
export default /*#__PURE__*/_defineComponent({
props: /*#__PURE__*/_mergeDefaults({
export default /*@__PURE__*/_defineComponent({
props: /*@__PURE__*/_mergeDefaults({
foo: { type: String, required: false },
bar: { type: Number, required: false },
baz: { type: Boolean, required: true }
@ -439,7 +491,7 @@ exports[`defineProps > withDefaults (static) + normal script 1`] = `
a?: string;
}
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
props: {
a: { type: String, required: false, default: "a" }
},
@ -457,7 +509,7 @@ return { props }
exports[`defineProps > withDefaults (static) 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
props: {
foo: { type: String, required: false, default: 'hi' },
bar: { type: Number, required: false },
@ -481,7 +533,7 @@ return { props }
exports[`defineProps > withDefaults (static) w/ production mode 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
props: {
foo: {},
bar: { type: Boolean },
@ -502,8 +554,8 @@ return { props }
exports[`defineProps > withDefaults w/ dynamic object method 1`] = `
"import { mergeDefaults as _mergeDefaults, defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({
props: /*#__PURE__*/_mergeDefaults({
export default /*@__PURE__*/_defineComponent({
props: /*@__PURE__*/_mergeDefaults({
foo: { type: Function, required: false }
}, {
['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'
export default {
props: /*#__PURE__*/_mergeDefaults(['foo', 'bar', 'baz'], {
props: /*@__PURE__*/_mergeDefaults(['foo', 'bar', 'baz'], {
foo: 1,
bar: () => ({}),
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'
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,
bar: () => ({}),
func: () => {}, __skip_func: true,
@ -101,7 +101,7 @@ exports[`sfc reactive props destructure > default values w/ runtime declaration
"import { mergeDefaults as _mergeDefaults } from 'vue'
export default {
props: /*#__PURE__*/_mergeDefaults(['foo', 'foo:bar'], {
props: /*@__PURE__*/_mergeDefaults(['foo', 'foo:bar'], {
foo: 1,
"foo:bar": 'foo-bar'
}),
@ -118,7 +118,7 @@ return () => {}
exports[`sfc reactive props destructure > default values w/ type declaration & key is string 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
props: {
foo: { type: Number, required: true, default: 1 },
bar: { type: Number, required: true, default: 2 },
@ -138,7 +138,7 @@ return () => {}
exports[`sfc reactive props destructure > default values w/ type declaration 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
props: {
foo: { type: Number, required: false, default: 1 },
bar: { type: Object, required: false, default: () => ({}) },
@ -157,7 +157,7 @@ return () => {}
exports[`sfc reactive props destructure > default values w/ type declaration, prod mode 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({
export default /*@__PURE__*/_defineComponent({
props: {
foo: { default: 1 },
bar: { default: () => ({}) },
@ -320,3 +320,22 @@ return { rest }
}"
`;
exports[`sfc reactive props destructure > with TSInstantiationExpression 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
type Foo = <T extends string | number>(data: T) => void
export default /*@__PURE__*/_defineComponent({
props: {
value: { type: Function }
},
setup(__props: any) {
const foo = __props.value<123>
return () => {}
}
})"
`;

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ describe('defineOptions()', () => {
expect(content).not.toMatch('defineOptions')
// should include context options in default export
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>
`)
assertCode(content)
expect(content).toMatch(`export default /*#__PURE__*/_defineComponent({
expect(content).toMatch(`export default /*@__PURE__*/_defineComponent({
props: { foo: String },
setup(__props, { expose: __expose }) {`)
})
@ -261,6 +261,51 @@ const props = defineProps({ foo: String })
})
})
test('w/ extends intersection type', () => {
const { content, bindings } = compile(`
<script setup lang="ts">
type Foo = {
x?: number;
};
interface Props extends Foo {
z: number
y: string
}
defineProps<Props>()
</script>
`)
assertCode(content)
expect(content).toMatch(`z: { type: Number, required: true }`)
expect(content).toMatch(`y: { type: String, required: true }`)
expect(content).toMatch(`x: { type: Number, required: false }`)
expect(bindings).toStrictEqual({
x: BindingTypes.PROPS,
y: BindingTypes.PROPS,
z: BindingTypes.PROPS,
})
})
test('w/ intersection type', () => {
const { content, bindings } = compile(`
<script setup lang="ts">
type Foo = {
x?: number;
};
type Bar = {
y: string;
};
defineProps<Foo & Bar>()
</script>
`)
assertCode(content)
expect(content).toMatch(`y: { type: String, required: true }`)
expect(content).toMatch(`x: { type: Number, required: false }`)
expect(bindings).toStrictEqual({
x: BindingTypes.PROPS,
y: BindingTypes.PROPS,
})
})
test('w/ exported interface', () => {
const { content, bindings } = compile(`
<script setup lang="ts">

View File

@ -78,7 +78,7 @@ describe('sfc reactive props destructure', () => {
// function
// functions need to be marked with a skip marker
expect(content)
.toMatch(`props: /*#__PURE__*/_mergeDefaults(['foo', 'bar', 'baz'], {
.toMatch(`props: /*@__PURE__*/_mergeDefaults(['foo', 'bar', 'baz'], {
foo: 1,
bar: () => ({}),
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
// is imported, or spreads another object)
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,
bar: () => ({}),
func: () => {}, __skip_func: true,
@ -122,7 +122,7 @@ describe('sfc reactive props destructure', () => {
})
expect(content).toMatch(`
props: /*#__PURE__*/_mergeDefaults(['foo', 'foo:bar'], {
props: /*@__PURE__*/_mergeDefaults(['foo', 'foo:bar'], {
foo: 1,
"foo:bar": 'foo-bar'
}),`)
@ -198,6 +198,21 @@ describe('sfc reactive props destructure', () => {
}`)
})
test('with TSInstantiationExpression', () => {
const { content } = compile(
`
<script setup lang="ts">
type Foo = <T extends string | number>(data: T) => void
const { value } = defineProps<{ value: Foo }>()
const foo = value<123>
</script>
`,
{ isProd: true },
)
assertCode(content)
expect(content).toMatch(`const foo = __props.value<123>`)
})
test('aliasing', () => {
const { content, bindings } = compile(`
<script setup>
@ -378,14 +393,15 @@ describe('sfc reactive props destructure', () => {
).toThrow(`destructure cannot use computed key`)
})
test('should error when used with withDefaults', () => {
expect(() =>
compile(
`<script setup lang="ts">
const { foo } = withDefaults(defineProps<{ foo: string }>(), { foo: 'foo' })
</script>`,
),
).toThrow(`withDefaults() is unnecessary when using destructure`)
test('should warn when used with withDefaults', () => {
compile(
`<script setup lang="ts">
const { foo } = withDefaults(defineProps<{ foo: string }>(), { foo: 'foo' })
</script>`,
)
expect(
`withDefaults() is unnecessary when using destructure`,
).toHaveBeenWarned()
})
test('should error if destructure reference local vars', () => {

View File

@ -250,3 +250,18 @@ test('check when has explicit parse options', () => {
)
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

@ -1185,6 +1185,49 @@ describe('resolveType', () => {
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', () => {
const files = {
'/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', () => {
expect(compileScoped(`h1 .foo, .bar, .baz { color: red; }`)).toMatch(
`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;
}"
`)
expect(compileScoped(`:deep(.foo) { color: red; .bar { color: red; } }`))
.toMatchInlineSnapshot(`
"[data-v-test] .foo { color: red;
.bar { color: red;
}
}"
`)
})
test('::v-slotted', () => {

View File

@ -1,5 +1,6 @@
import {
type SFCParseOptions,
type SFCScriptBlock,
type SFCScriptCompileOptions,
compileScript,
parse,
@ -12,7 +13,7 @@ export function compileSFCScript(
src: string,
options?: Partial<SFCScriptCompileOptions>,
parseOptions?: SFCParseOptions,
) {
): SFCScriptBlock {
const { descriptor, errors } = parse(src, parseOptions)
if (errors.length) {
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
try {
babelParse(code, {

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-sfc",
"version": "3.5.0-beta.3",
"version": "3.5.12",
"description": "@vue/compiler-sfc",
"main": "dist/compiler-sfc.cjs.js",
"module": "dist/compiler-sfc.esm-browser.js",
@ -49,7 +49,7 @@
"@vue/shared": "workspace:*",
"estree-walker": "catalog:",
"magic-string": "catalog:",
"postcss": "^8.4.41",
"postcss": "^8.4.47",
"source-map-js": "catalog:"
},
"devDependencies": {
@ -62,6 +62,6 @@
"postcss-modules": "^6.0.0",
"postcss-selector-parser": "^6.1.2",
"pug": "^3.0.3",
"sass": "^1.77.8"
"sass": "^1.80.6"
}
}

View File

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

View File

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

View File

@ -18,6 +18,7 @@ import { createCache } from './cache'
import type { ImportBinding } from './compileScript'
import { isImportUsed } from './script/importUsageCheck'
import type { LRUCache } from 'lru-cache'
import { genCacheKey } from '@vue/shared'
export const DEFAULT_FILENAME = 'anonymous.vue'
@ -103,24 +104,14 @@ export const parseCache:
| Map<string, 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(
source: string,
options: SFCParseOptions = {},
): SFCParseResult {
const sourceKey = genCacheKey(source, options)
const sourceKey = genCacheKey(source, {
...options,
compiler: { parse: options.compiler?.parse },
})
const cache = parseCache.get(sourceKey)
if (cache) {
return cache
@ -235,7 +226,7 @@ export function parse(
if (!descriptor.template && !descriptor.script && !descriptor.scriptSetup) {
errors.push(
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 MagicString from 'magic-string'
import type { TypeScope } from './resolveType'
import { warn } from '../warn'
export class ScriptCompileContext {
isJS: boolean
@ -145,20 +146,31 @@ export class ScriptCompileContext {
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 {
const offset = scope ? scope.offset : this.startOffset!
throw new Error(
`[@vue/compiler-sfc] ${msg}\n\n${
(scope || this.descriptor).filename
}\n${generateCodeFrame(
(scope || this.descriptor).source,
node.start! + offset,
node.end! + offset,
)}`,
`[@vue/compiler-sfc] ${generateError(msg, node, this, scope)}`,
)
}
}
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(
lang: string,
userPlugins?: ParserPlugin[],

View File

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

View File

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

View File

@ -10,6 +10,7 @@ import type {
import { walk } from 'estree-walker'
import {
BindingTypes,
TS_NODE_TYPES,
extractIdentifiers,
isFunctionType,
isInDestructureAssignment,
@ -102,7 +103,7 @@ export function transformDestructuredProps(
return
}
const rootScope: Scope = {}
const rootScope: Scope = Object.create(null)
const scopeStack: Scope[] = [rootScope]
let currentScope: Scope = rootScope
const excludedIds = new WeakSet<Identifier>()
@ -240,9 +241,7 @@ export function transformDestructuredProps(
if (
parent &&
parent.type.startsWith('TS') &&
parent.type !== 'TSAsExpression' &&
parent.type !== 'TSNonNullExpression' &&
parent.type !== 'TSTypeAssertion'
!TS_NODE_TYPES.includes(parent.type)
) {
return this.skip()
}

View File

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

View File

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

View File

@ -121,15 +121,3 @@ export const propNameEscapeSymbolsRE: RegExp =
export function getEscapedPropName(key: string): string {
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,
} from '@vue/compiler-dom'
import type { SFCDescriptor } from '../parse'
import { getEscapedCssVarName } from '../script/utils'
import type { PluginCreator } from 'postcss'
import hash from 'hash-sum'
import { getEscapedCssVarName } from '@vue/shared'
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 { warn } from '../warn'
@ -71,21 +77,32 @@ function processRule(id: string, rule: Rule) {
return
}
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 => {
selectorRoot.each(selector => {
rewriteSelector(id, selector, selectorRoot)
rewriteSelector(id, rule, selector, selectorRoot, deep)
})
}).processSync(rule.selector)
}
function rewriteSelector(
id: string,
rule: Rule,
selector: selectorParser.Selector,
selectorRoot: selectorParser.Root,
deep: boolean,
slotted = false,
) {
let node: selectorParser.Node | null = null
let shouldInject = true
let shouldInject = !deep
// find the last child node to insert attribute selector
selector.each(n => {
// DEPRECATED ">>>" and "/deep/" combinator
@ -107,6 +124,7 @@ function rewriteSelector(
// deep: inject [id] attribute at the node before the ::v-deep
// combinator.
if (value === ':deep' || value === '::v-deep') {
;(rule as any).__deep = true
if (n.nodes.length) {
// .foo ::v-deep(.bar) -> .foo[xxxxxxx] .bar
// replace the current node with ::v-deep's inner selector
@ -147,7 +165,14 @@ function rewriteSelector(
// instead.
// ::v-slotted(.foo) -> .foo[xxxxxxx-s]
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
n.nodes[0].each(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) {
const { type, value } = node as selectorParser.Node
if (type === 'pseudo' && (value === ':is' || value === ':where')) {
;(node as selectorParser.Pseudo).nodes.forEach(value =>
rewriteSelector(id, value, selectorRoot, slotted),
rewriteSelector(id, rule, value, selectorRoot, deep, slotted),
)
shouldInject = false
}
@ -245,5 +282,22 @@ function isSpaceCombinator(node: selectorParser.Node) {
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
export default scopedPlugin

View File

@ -23,28 +23,48 @@ export interface StylePreprocessorResults {
// .scss/.sass processor
const scss: StylePreprocessor = (source, map, options, load = require) => {
const nodeSass = load('sass')
const finalOptions = {
...options,
data: getSource(source, options.filename, options.additionalData),
file: options.filename,
outFile: options.filename,
sourceMap: !!map,
}
const nodeSass: typeof import('sass') = load('sass')
const { compileString, renderSync } = nodeSass
const data = getSource(source, options.filename, options.additionalData)
let css: string
let dependencies: string[]
let sourceMap: any
try {
const result = nodeSass.renderSync(finalOptions)
const dependencies = result.stats.includedFiles
if (map) {
return {
code: result.css.toString(),
map: merge(map, JSON.parse(result.map.toString())),
errors: [],
dependencies,
}
if (compileString) {
const { pathToFileURL, fileURLToPath }: typeof import('url') = load('url')
const result = compileString(data, {
...options,
url: pathToFileURL(options.filename),
sourceMap: !!map,
})
css = result.css
dependencies = result.loadedUrls.map(url => fileURLToPath(url))
sourceMap = map ? result.sourceMap! : undefined
} else {
const result = renderSync({
...options,
data,
file: options.filename,
outFile: options.filename,
sourceMap: !!map,
})
css = result.css.toString()
dependencies = result.stats.includedFiles
sourceMap = map ? JSON.parse(result.map!.toString()) : undefined
}
return { code: result.css.toString(), errors: [], dependencies }
if (map) {
return {
code: css,
errors: [],
dependencies,
map: merge(map, sourceMap!),
}
}
return { code: css, errors: [], dependencies }
} catch (e: any) {
return { code: '', errors: [e], dependencies: [] }
}

View File

@ -39,7 +39,7 @@ describe('transition-group', () => {
})
// #11514
test('with static tag + comment', () => {
test('with static tag + v-if comment', () => {
expect(
compile(
`<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', () => {
expect(
compile(

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,6 @@
import {
type TestElement,
defineComponent,
h,
nextTick,
nodeOps,
@ -6,6 +8,7 @@ import {
onUnmounted,
render,
serializeInner,
triggerEvent,
} from '@vue/runtime-test'
import {
type DebuggerEvent,
@ -20,6 +23,7 @@ import {
ref,
shallowRef,
toRaw,
triggerRef,
} from '../src'
import { EffectFlags, pauseTracking, resetTracking } from '../src/effect'
import type { ComputedRef, ComputedRefImpl } from '../src/computed'
@ -33,6 +37,20 @@ describe('reactivity/computed', () => {
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', () => {
const value = reactive<{ foo?: number }>({})
const getter = vi.fn(() => value.foo)
@ -577,7 +595,7 @@ describe('reactivity/computed', () => {
v.value += ' World'
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()
})
@ -875,7 +893,7 @@ describe('reactivity/computed', () => {
v.value += ' World'
await nextTick()
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,181 @@ describe('reactivity/computed', () => {
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])
})
test('performance when removing dependencies from deeply nested computeds', () => {
const base = ref(1)
const trigger = ref(true)
const computeds: ComputedRef<number>[] = []
const LAYERS = 30
for (let i = 0; i < LAYERS; i++) {
const earlier = [...computeds]
computeds.push(
computed(() => {
return base.value + earlier.reduce((sum, c) => sum + c.value, 0)
}),
)
}
const tail = computed(() =>
trigger.value ? computeds[computeds.length - 1].value : 0,
)
const t0 = performance.now()
expect(tail.value).toBe(2 ** (LAYERS - 1))
const t1 = performance.now()
expect(t1 - t0).toBeLessThan(process.env.CI ? 100 : 30)
trigger.value = false
expect(tail.value).toBe(0)
const t2 = performance.now()
expect(t2 - t1).toBeLessThan(process.env.CI ? 100 : 30)
})
})

View File

@ -13,6 +13,7 @@ import {
} from '../src/reactive'
import { computed } from '../src/computed'
import { effect } from '../src/effect'
import { targetMap } from '../src/dep'
describe('reactivity/reactive', () => {
test('Object', () => {
@ -293,6 +294,13 @@ describe('reactivity/reactive', () => {
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', () => {
const obj = reactive({
foo: Object.preventExtensions({ a: 1 }),
@ -382,4 +390,33 @@ describe('reactivity/reactive', () => {
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()
})
test('should trigger reactivity when Map key is undefined', () => {
const map = reactive(new Map())
const c = computed(() => map.get(void 0))
expect(c.value).toBe(void 0)
map.set(void 0, 1)
expect(c.value).toBe(1)
})
})

View File

@ -51,6 +51,7 @@ describe('reactivity/reactive/Array', () => {
const raw = {}
const arr = reactive([{}, {}])
arr.push(raw)
expect(arr.indexOf(raw)).toBe(2)
expect(arr.indexOf(raw, 3)).toBe(-1)
expect(arr.includes(raw)).toBe(true)
@ -89,6 +90,84 @@ describe('reactivity/reactive/Array', () => {
expect(index).toBe(1)
})
// only non-existent reactive will try to search by using its raw value
describe('Array identity methods should not be called more than necessary', () => {
const identityMethods = ['includes', 'indexOf', 'lastIndexOf'] as const
function instrumentArr(rawTarget: any[]) {
identityMethods.forEach(key => {
const spy = vi.fn(rawTarget[key] as any)
rawTarget[key] = spy
})
}
function searchValue(target: any[], ...args: unknown[]) {
return identityMethods.map(key => (target[key] as any)(...args))
}
function unInstrumentArr(rawTarget: any[]) {
identityMethods.forEach(key => {
;(rawTarget[key] as any).mockClear()
// relink to prototype method
rawTarget[key] = Array.prototype[key] as any
})
}
function expectHaveBeenCalledTimes(rawTarget: any[], times: number) {
identityMethods.forEach(key => {
expect(rawTarget[key]).toHaveBeenCalledTimes(times)
})
}
test('should be called once with a non-existent raw value', () => {
const reactiveArr = reactive([])
instrumentArr(toRaw(reactiveArr))
const searchResult = searchValue(reactiveArr, {})
expectHaveBeenCalledTimes(toRaw(reactiveArr), 1)
expect(searchResult).toStrictEqual([false, -1, -1])
unInstrumentArr(toRaw(reactiveArr))
})
test('should be called once with an existent reactive value', () => {
const existReactiveValue = reactive({})
const reactiveArr = reactive([existReactiveValue, existReactiveValue])
instrumentArr(toRaw(reactiveArr))
const searchResult = searchValue(reactiveArr, existReactiveValue)
expectHaveBeenCalledTimes(toRaw(reactiveArr), 1)
expect(searchResult).toStrictEqual([true, 0, 1])
unInstrumentArr(toRaw(reactiveArr))
})
test('should be called twice with a non-existent reactive value', () => {
const reactiveArr = reactive([])
instrumentArr(toRaw(reactiveArr))
const searchResult = searchValue(reactiveArr, reactive({}))
expectHaveBeenCalledTimes(toRaw(reactiveArr), 2)
expect(searchResult).toStrictEqual([false, -1, -1])
unInstrumentArr(toRaw(reactiveArr))
})
test('should be called twice with a non-existent reactive value, but the raw value exists', () => {
const existRaw = {}
const reactiveArr = reactive([existRaw, existRaw])
instrumentArr(toRaw(reactiveArr))
const searchResult = searchValue(reactiveArr, reactive(existRaw))
expectHaveBeenCalledTimes(toRaw(reactiveArr), 2)
expect(searchResult).toStrictEqual([true, 0, 1])
unInstrumentArr(toRaw(reactiveArr))
})
})
test('delete on Array should not trigger length dependency', () => {
const arr = reactive([1, 2, 3])
const fn = vi.fn()
@ -303,19 +382,35 @@ describe('reactivity/reactive/Array', () => {
const a2 = reactive([{ val: 3 }])
const a3 = [4, 5]
let result = computed(() => a1.concat(a2, a3))
expect(result.value).toStrictEqual([1, { val: 2 }, { val: 3 }, 4, 5])
let result = computed(() => a1.concat(a2, a3, 6, { val: 7 }))
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[2])).toBe(true)
expect(isReactive(result.value[6])).toBe(false)
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()
expect(result.value).toStrictEqual([{ val: 2 }, 4, 5])
expect(result.value).toStrictEqual([{ val: 2 }, 4, 5, 6, { val: 7 }])
a3.pop()
expect(result.value).toStrictEqual([{ val: 2 }, 4, 5])
expect(result.value).toStrictEqual([{ val: 2 }, 4, 5, 6, { val: 7 }])
})
test('entries', () => {
@ -724,6 +819,27 @@ describe('reactivity/reactive/Array', () => {
expect(state.things.forEach('foo', 'bar', 'baz')).toBeUndefined()
expect(state.things.map('foo', 'bar', 'baz')).toEqual(['1', '2', '3'])
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)
})
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', () => {
const a = ref({
count: 1,

View File

@ -4,6 +4,7 @@ import {
WatchErrorCodes,
type WatchOptions,
type WatchScheduler,
computed,
onWatcherCleanup,
ref,
watch,
@ -13,7 +14,7 @@ const queue: (() => void)[] = []
// a simple scheduler for testing purposes
let isFlushPending = false
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
const resolvedPromise = /*@__PURE__*/ Promise.resolve() as Promise<any>
const nextTick = (fn?: () => any) =>
fn ? resolvedPromise.then(fn) : resolvedPromise
@ -193,4 +194,87 @@ describe('watch', () => {
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",
"version": "3.5.0-beta.3",
"version": "3.5.12",
"description": "@vue/reactivity",
"main": "index.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 { isProxy, isShallow, toRaw, toReactive } from './reactive'
import { ARRAY_ITERATE_KEY, track } from './dep'
import { isArray } from '@vue/shared'
/**
* Track array iteration and return:
@ -30,9 +31,9 @@ export const arrayInstrumentations: Record<string | symbol, Function> = <any>{
return iterator(this, Symbol.iterator, toReactive)
},
concat(...args: unknown[][]) {
concat(...args: unknown[]) {
return reactiveReadArray(this).concat(
...args.map(x => reactiveReadArray(x)),
...args.map(x => (isArray(x) ? reactiveReadArray(x) : x)),
)
},
@ -242,9 +243,13 @@ function apply(
const needsWrap = arr !== self && !isShallow(self)
// @ts-expect-error our code is limited to es2016 but user code is not
const methodFn = arr[method]
// @ts-expect-error
if (methodFn !== arrayProto[method]) {
const result = 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
}

View File

@ -25,10 +25,10 @@ import {
import { isRef } from './ref'
import { warn } from './warning'
const isNonTrackableKeys = /*#__PURE__*/ makeMap(`__proto__,__v_isRef,__isVue`)
const isNonTrackableKeys = /*@__PURE__*/ makeMap(`__proto__,__v_isRef,__isVue`)
const builtInSymbols = new Set(
/*#__PURE__*/
/*@__PURE__*/
Object.getOwnPropertyNames(Symbol)
// ios10.x Object.getOwnPropertyNames(Symbol) can enumerate 'arguments' and 'caller'
// 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)
? Number(key) < target.length
: 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
if (target === toRaw(receiver)) {
if (!hadKey) {
@ -235,16 +240,16 @@ class ReadonlyReactiveHandler extends BaseReactiveHandler {
}
export const mutableHandlers: ProxyHandler<object> =
/*#__PURE__*/ new MutableReactiveHandler()
/*@__PURE__*/ new MutableReactiveHandler()
export const readonlyHandlers: ProxyHandler<object> =
/*#__PURE__*/ new ReadonlyReactiveHandler()
/*@__PURE__*/ new ReadonlyReactiveHandler()
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
// refs (in order to allow refs to be explicitly passed down), but should
// retain the reactivity of the normal readonly object.
export const shallowReadonlyHandlers: ReadonlyReactiveHandler =
/*#__PURE__*/ new ReadonlyReactiveHandler(true)
/*@__PURE__*/ new ReadonlyReactiveHandler(true)

View File

@ -8,7 +8,14 @@ import {
} from './reactive'
import { ITERATE_KEY, MAP_KEY_ITERATE_KEY, track, trigger } from './dep'
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
import { capitalize, hasChanged, hasOwn, isMap, toRawType } from '@vue/shared'
import {
capitalize,
extend,
hasChanged,
hasOwn,
isMap,
toRawType,
} from '@vue/shared'
import { warn } from './warning'
type CollectionTypes = IterableCollections | WeakCollections
@ -23,152 +30,6 @@ const toShallow = <T extends unknown>(value: T): T => value
const getProto = <T extends CollectionTypes>(v: T): any =>
Reflect.getPrototypeOf(v)
function get(
target: MapTypes,
key: unknown,
isReadonly = false,
isShallow = false,
) {
// #1772: readonly(reactive(Map)) should return readonly + reactive version
// of the value
target = target[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
if (!isReadonly) {
if (hasChanged(key, rawKey)) {
track(rawTarget, TrackOpTypes.GET, key)
}
track(rawTarget, TrackOpTypes.GET, rawKey)
}
const { has } = getProto(rawTarget)
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
if (has.call(rawTarget, key)) {
return wrap(target.get(key))
} else if (has.call(rawTarget, rawKey)) {
return wrap(target.get(rawKey))
} else if (target !== rawTarget) {
// #3602 readonly(reactive(Map))
// ensure that the nested reactive `Map` can do tracking for itself
target.get(key)
}
}
function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
const target = this[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
if (!isReadonly) {
if (hasChanged(key, rawKey)) {
track(rawTarget, TrackOpTypes.HAS, key)
}
track(rawTarget, TrackOpTypes.HAS, rawKey)
}
return key === rawKey
? target.has(key)
: target.has(key) || target.has(rawKey)
}
function size(target: IterableCollections, isReadonly = false) {
target = target[ReactiveFlags.RAW]
!isReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.get(target, 'size', target)
}
function add(this: SetTypes, value: unknown, _isShallow = false) {
if (!_isShallow && !isShallow(value) && !isReadonly(value)) {
value = toRaw(value)
}
const target = toRaw(this)
const proto = getProto(target)
const hadKey = proto.has.call(target, value)
if (!hadKey) {
target.add(value)
trigger(target, TriggerOpTypes.ADD, value, value)
}
return this
}
function set(this: MapTypes, key: unknown, value: unknown, _isShallow = false) {
if (!_isShallow && !isShallow(value) && !isReadonly(value)) {
value = toRaw(value)
}
const target = toRaw(this)
const { has, get } = getProto(target)
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
checkIdentityKeys(target, has, key)
}
const oldValue = get.call(target, key)
target.set(key, value)
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
return this
}
function deleteEntry(this: CollectionTypes, key: unknown) {
const target = toRaw(this)
const { has, get } = getProto(target)
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
checkIdentityKeys(target, has, key)
}
const oldValue = get ? get.call(target, key) : undefined
// forward the operation before queueing reactions
const result = target.delete(key)
if (hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
function clear(this: IterableCollections) {
const target = toRaw(this)
const hadItems = target.size !== 0
const oldTarget = __DEV__
? isMap(target)
? new Map(target)
: new Set(target)
: undefined
// forward the operation before queueing reactions
const result = target.clear()
if (hadItems) {
trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)
}
return result
}
function createForEach(isReadonly: boolean, isShallow: boolean) {
return function forEach(
this: IterableCollections,
callback: Function,
thisArg?: unknown,
) {
const observed = this
const target = observed[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
!isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
return target.forEach((value: unknown, key: unknown) => {
// important: make sure the callback is
// 1. invoked with the reactive map as `this` and 3rd arg
// 2. the value received should be a corresponding reactive/readonly.
return callback.call(thisArg, wrap(value), wrap(key), observed)
})
}
}
function createIterableMethod(
method: string | symbol,
isReadonly: boolean,
@ -232,74 +93,158 @@ function createReadonlyMethod(type: TriggerOpTypes): Function {
type Instrumentations = Record<string | symbol, Function | number>
function createInstrumentations() {
const mutableInstrumentations: Instrumentations = {
function createInstrumentations(
readonly: boolean,
shallow: boolean,
): Instrumentations {
const instrumentations: Instrumentations = {
get(this: MapTypes, key: unknown) {
return get(this, key)
// #1772: readonly(reactive(Map)) should return readonly + reactive version
// of the value
const target = this[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
if (!readonly) {
if (hasChanged(key, rawKey)) {
track(rawTarget, TrackOpTypes.GET, key)
}
track(rawTarget, TrackOpTypes.GET, rawKey)
}
const { has } = getProto(rawTarget)
const wrap = shallow ? toShallow : readonly ? toReadonly : toReactive
if (has.call(rawTarget, key)) {
return wrap(target.get(key))
} else if (has.call(rawTarget, rawKey)) {
return wrap(target.get(rawKey))
} else if (target !== rawTarget) {
// #3602 readonly(reactive(Map))
// ensure that the nested reactive `Map` can do tracking for itself
target.get(key)
}
},
get size() {
return size(this as unknown as IterableCollections)
const target = (this as unknown as IterableCollections)[ReactiveFlags.RAW]
!readonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.get(target, 'size', target)
},
has(this: CollectionTypes, key: unknown): boolean {
const target = this[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
if (!readonly) {
if (hasChanged(key, rawKey)) {
track(rawTarget, TrackOpTypes.HAS, key)
}
track(rawTarget, TrackOpTypes.HAS, rawKey)
}
return key === rawKey
? target.has(key)
: target.has(key) || target.has(rawKey)
},
forEach(this: IterableCollections, callback: Function, thisArg?: unknown) {
const observed = this
const target = observed[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const wrap = shallow ? toShallow : readonly ? toReadonly : toReactive
!readonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
return target.forEach((value: unknown, key: unknown) => {
// important: make sure the callback is
// 1. invoked with the reactive map as `this` and 3rd arg
// 2. the value received should be a corresponding reactive/readonly.
return callback.call(thisArg, wrap(value), wrap(key), observed)
})
},
has,
add,
set,
delete: deleteEntry,
clear,
forEach: createForEach(false, false),
}
const shallowInstrumentations: Instrumentations = {
get(this: MapTypes, key: unknown) {
return get(this, key, false, true)
},
get size() {
return size(this as unknown as IterableCollections)
},
has,
add(this: SetTypes, value: unknown) {
return add.call(this, value, true)
},
set(this: MapTypes, key: unknown, value: unknown) {
return set.call(this, key, value, true)
},
delete: deleteEntry,
clear,
forEach: createForEach(false, true),
}
extend(
instrumentations,
readonly
? {
add: createReadonlyMethod(TriggerOpTypes.ADD),
set: createReadonlyMethod(TriggerOpTypes.SET),
delete: createReadonlyMethod(TriggerOpTypes.DELETE),
clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
}
: {
add(this: SetTypes, value: unknown) {
if (!shallow && !isShallow(value) && !isReadonly(value)) {
value = toRaw(value)
}
const target = toRaw(this)
const proto = getProto(target)
const hadKey = proto.has.call(target, value)
if (!hadKey) {
target.add(value)
trigger(target, TriggerOpTypes.ADD, value, value)
}
return this
},
set(this: MapTypes, key: unknown, value: unknown) {
if (!shallow && !isShallow(value) && !isReadonly(value)) {
value = toRaw(value)
}
const target = toRaw(this)
const { has, get } = getProto(target)
const readonlyInstrumentations: Instrumentations = {
get(this: MapTypes, key: unknown) {
return get(this, key, true)
},
get size() {
return size(this as unknown as IterableCollections, true)
},
has(this: MapTypes, key: unknown) {
return has.call(this, key, true)
},
add: createReadonlyMethod(TriggerOpTypes.ADD),
set: createReadonlyMethod(TriggerOpTypes.SET),
delete: createReadonlyMethod(TriggerOpTypes.DELETE),
clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
forEach: createForEach(true, false),
}
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
checkIdentityKeys(target, has, key)
}
const shallowReadonlyInstrumentations: Instrumentations = {
get(this: MapTypes, key: unknown) {
return get(this, key, true, true)
},
get size() {
return size(this as unknown as IterableCollections, true)
},
has(this: MapTypes, key: unknown) {
return has.call(this, key, true)
},
add: createReadonlyMethod(TriggerOpTypes.ADD),
set: createReadonlyMethod(TriggerOpTypes.SET),
delete: createReadonlyMethod(TriggerOpTypes.DELETE),
clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
forEach: createForEach(true, true),
}
const oldValue = get.call(target, key)
target.set(key, value)
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
return this
},
delete(this: CollectionTypes, key: unknown) {
const target = toRaw(this)
const { has, get } = getProto(target)
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
checkIdentityKeys(target, has, key)
}
const oldValue = get ? get.call(target, key) : undefined
// forward the operation before queueing reactions
const result = target.delete(key)
if (hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
},
clear(this: IterableCollections) {
const target = toRaw(this)
const hadItems = target.size !== 0
const oldTarget = __DEV__
? isMap(target)
? new Map(target)
: new Set(target)
: undefined
// forward the operation before queueing reactions
const result = target.clear()
if (hadItems) {
trigger(
target,
TriggerOpTypes.CLEAR,
undefined,
undefined,
oldTarget,
)
}
return result
},
},
)
const iteratorMethods = [
'keys',
@ -309,39 +254,14 @@ function createInstrumentations() {
] as const
iteratorMethods.forEach(method => {
mutableInstrumentations[method] = createIterableMethod(method, false, false)
readonlyInstrumentations[method] = createIterableMethod(method, true, false)
shallowInstrumentations[method] = createIterableMethod(method, false, true)
shallowReadonlyInstrumentations[method] = createIterableMethod(
method,
true,
true,
)
instrumentations[method] = createIterableMethod(method, readonly, shallow)
})
return [
mutableInstrumentations,
readonlyInstrumentations,
shallowInstrumentations,
shallowReadonlyInstrumentations,
]
return instrumentations
}
const [
mutableInstrumentations,
readonlyInstrumentations,
shallowInstrumentations,
shallowReadonlyInstrumentations,
] = /* #__PURE__*/ createInstrumentations()
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
const instrumentations = shallow
? isReadonly
? shallowReadonlyInstrumentations
: shallowInstrumentations
: isReadonly
? readonlyInstrumentations
: mutableInstrumentations
const instrumentations = createInstrumentations(isReadonly, shallow)
return (
target: CollectionTypes,
@ -367,20 +287,20 @@ function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
}
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: /*#__PURE__*/ createInstrumentationGetter(false, false),
get: /*@__PURE__*/ createInstrumentationGetter(false, false),
}
export const shallowCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: /*#__PURE__*/ createInstrumentationGetter(false, true),
get: /*@__PURE__*/ createInstrumentationGetter(false, true),
}
export const readonlyCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: /*#__PURE__*/ createInstrumentationGetter(true, false),
get: /*@__PURE__*/ createInstrumentationGetter(true, false),
}
export const shallowReadonlyCollectionHandlers: ProxyHandler<CollectionTypes> =
{
get: /*#__PURE__*/ createInstrumentationGetter(true, true),
get: /*@__PURE__*/ createInstrumentationGetter(true, true),
}
function checkIdentityKeys(

View File

@ -3,14 +3,14 @@ import {
type DebuggerEvent,
type DebuggerOptions,
EffectFlags,
type Link,
type Subscriber,
activeSub,
batch,
refreshComputed,
} from './effect'
import type { Ref } from './ref'
import { warn } from './warning'
import { Dep, globalVersion } from './dep'
import { Dep, type Link, globalVersion } from './dep'
import { ReactiveFlags, TrackOpTypes } from './constants'
declare const ComputedRefSymbol: unique symbol
@ -84,9 +84,13 @@ export class ComputedRefImpl<T = any> implements Subscriber {
* @internal
*/
isSSR: boolean
/**
* @internal
*/
next?: Subscriber = undefined
// for backwards compat
effect: this = this
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
@ -110,11 +114,15 @@ export class ComputedRefImpl<T = any> implements Subscriber {
/**
* @internal
*/
notify(): void {
// avoid infinite self recursion
if (activeSub !== this) {
this.flags |= EffectFlags.DIRTY
this.dep.notify()
notify(): true | void {
this.flags |= EffectFlags.DIRTY
if (
!(this.flags & EffectFlags.NOTIFIED) &&
// avoid infinite self recursion
activeSub !== this
) {
batch(this, true)
return true
} else if (__DEV__) {
// TODO warn
}

View File

@ -4,7 +4,7 @@ import { type TrackOpTypes, TriggerOpTypes } from './constants'
import {
type DebuggerEventExtraInfo,
EffectFlags,
type Link,
type Subscriber,
activeSub,
endBatch,
shouldTrack,
@ -18,6 +18,49 @@ import {
*/
export let globalVersion = 0
/**
* Represents a link between a source (Dep) and a subscriber (Effect or Computed).
* Deps and subs have a many-to-many relationship - each link between a
* dep and a sub is represented by a Link instance.
*
* A Link is also a node in two doubly-linked lists - one for the associated
* sub to track all its deps, and one for the associated dep to track all its
* subs.
*
* @internal
*/
export class Link {
/**
* - Before each effect run, all previous dep links' version are reset to -1
* - During the run, a link's version is synced with the source dep on access
* - After the run, links with version -1 (that were never used) are cleaned
* up
*/
version: number
/**
* Pointers for doubly-linked lists
*/
nextDep?: Link
prevDep?: Link
nextSub?: Link
prevSub?: Link
prevActiveLink?: Link
constructor(
public sub: Subscriber,
public dep: Dep,
) {
this.version = dep.version
this.nextDep =
this.prevDep =
this.nextSub =
this.prevSub =
this.prevActiveLink =
undefined
}
}
/**
* @internal
*/
@ -39,6 +82,17 @@ export class Dep {
*/
subsHead?: Link
/**
* For object property deps cleanup
*/
map?: KeyToDepMap = undefined
key?: unknown = undefined
/**
* Subscriber counter
*/
sc: number = 0
constructor(public computed?: ComputedRefImpl | undefined) {
if (__DEV__) {
this.subsHead = undefined
@ -46,22 +100,13 @@ export class Dep {
}
track(debugInfo?: DebuggerEventExtraInfo): Link | undefined {
if (!activeSub || !shouldTrack) {
if (!activeSub || !shouldTrack || activeSub === this.computed) {
return
}
let link = this.activeLink
if (link === undefined || link.sub !== activeSub) {
link = this.activeLink = {
dep: this,
sub: activeSub,
version: this.version,
nextDep: undefined,
prevDep: undefined,
nextSub: undefined,
prevSub: undefined,
prevActiveLink: undefined,
}
link = this.activeLink = new Link(activeSub, this)
// add the link to the activeEffect as a dep (as tail)
if (!activeSub.deps) {
@ -72,9 +117,7 @@ export class Dep {
activeSub.depsTail = link
}
if (activeSub.flags & EffectFlags.TRACKING) {
addSub(link)
}
addSub(link)
} else if (link.version === -1) {
// reused from last run - already a sub, just sync version
link.version = this.version
@ -129,11 +172,7 @@ export class Dep {
// original order at the end of the batch, but onTrigger hooks should
// be invoked in original order here.
for (let head = this.subsHead; head; head = head.nextSub) {
if (
__DEV__ &&
head.sub.onTrigger &&
!(head.sub.flags & EffectFlags.NOTIFIED)
) {
if (head.sub.onTrigger && !(head.sub.flags & EffectFlags.NOTIFIED)) {
head.sub.onTrigger(
extend(
{
@ -146,7 +185,12 @@ export class Dep {
}
}
for (let link = this.subs; link; link = link.prevSub) {
link.sub.notify()
if (link.sub.notify()) {
// if notify() returns `true`, this is a computed. Also call notify
// on its dep - it's called here instead of inside computed's notify
// in order to reduce call stack depth.
;(link.sub as ComputedRefImpl).dep.notify()
}
}
} finally {
endBatch()
@ -155,27 +199,30 @@ export class Dep {
}
function addSub(link: Link) {
const computed = link.dep.computed
// computed getting its first subscriber
// enable tracking + lazily subscribe to all its deps
if (computed && !link.dep.subs) {
computed.flags |= EffectFlags.TRACKING | EffectFlags.DIRTY
for (let l = computed.deps; l; l = l.nextDep) {
addSub(l)
link.dep.sc++
if (link.sub.flags & EffectFlags.TRACKING) {
const computed = link.dep.computed
// computed getting its first subscriber
// enable tracking + lazily subscribe to all its deps
if (computed && !link.dep.subs) {
computed.flags |= EffectFlags.TRACKING | EffectFlags.DIRTY
for (let l = computed.deps; l; l = l.nextDep) {
addSub(l)
}
}
}
const currentTail = link.dep.subs
if (currentTail !== link) {
link.prevSub = currentTail
if (currentTail) currentTail.nextSub = link
}
const currentTail = link.dep.subs
if (currentTail !== link) {
link.prevSub = currentTail
if (currentTail) currentTail.nextSub = link
}
if (__DEV__ && link.dep.subsHead === undefined) {
link.dep.subsHead = link
}
if (__DEV__ && link.dep.subsHead === undefined) {
link.dep.subsHead = link
}
link.dep.subs = link
link.dep.subs = link
}
}
// The main WeakMap that stores {target -> key -> dep} connections.
@ -183,7 +230,8 @@ function addSub(link: Link) {
// which maintains a Set of subscribers, but we simply store them as
// raw Maps to reduce memory overhead.
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<object, KeyToDepMap>()
export const targetMap: WeakMap<object, KeyToDepMap> = new WeakMap()
export const ITERATE_KEY: unique symbol = Symbol(
__DEV__ ? 'Object iterate' : '',
@ -214,6 +262,8 @@ export function track(target: object, type: TrackOpTypes, key: unknown): void {
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Dep()))
dep.map = depsMap
dep.key = key
}
if (__DEV__) {
dep.track({
@ -250,11 +300,29 @@ export function trigger(
return
}
let deps: Dep[] = []
const run = (dep: Dep | undefined) => {
if (dep) {
if (__DEV__) {
dep.trigger({
target,
type,
key,
newValue,
oldValue,
oldTarget,
})
} else {
dep.trigger()
}
}
}
startBatch()
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
deps = [...depsMap.values()]
depsMap.forEach(run)
} else {
const targetIsArray = isArray(target)
const isArrayIndex = targetIsArray && isIntegerKey(key)
@ -267,77 +335,57 @@ export function trigger(
key === ARRAY_ITERATE_KEY ||
(!isSymbol(key) && key >= newLength)
) {
deps.push(dep)
run(dep)
}
})
} else {
const push = (dep: Dep | undefined) => dep && deps.push(dep)
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
push(depsMap.get(key))
if (key !== void 0 || depsMap.has(void 0)) {
run(depsMap.get(key))
}
// schedule ARRAY_ITERATE for any numeric key change (length is handled above)
if (isArrayIndex) {
push(depsMap.get(ARRAY_ITERATE_KEY))
run(depsMap.get(ARRAY_ITERATE_KEY))
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD:
if (!targetIsArray) {
push(depsMap.get(ITERATE_KEY))
run(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
push(depsMap.get(MAP_KEY_ITERATE_KEY))
run(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isArrayIndex) {
// new index added to array -> length changes
push(depsMap.get('length'))
run(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!targetIsArray) {
push(depsMap.get(ITERATE_KEY))
run(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
push(depsMap.get(MAP_KEY_ITERATE_KEY))
run(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
push(depsMap.get(ITERATE_KEY))
run(depsMap.get(ITERATE_KEY))
}
break
}
}
}
startBatch()
for (const dep of deps) {
if (__DEV__) {
dep.trigger({
target,
type,
key,
newValue,
oldValue,
oldTarget,
})
} else {
dep.trigger()
}
}
endBatch()
}
/**
* Test only
*/
export function getDepFromReactive(
object: any,
key: string | number | symbol,
): Dep | undefined {
// eslint-disable-next-line
return targetMap.get(object)?.get(key)
const depMap = targetMap.get(object)
return depMap && depMap.get(key)
}

View File

@ -1,7 +1,7 @@
import { extend, hasChanged } from '@vue/shared'
import type { ComputedRefImpl } from './computed'
import type { TrackOpTypes, TriggerOpTypes } from './constants'
import { type Dep, globalVersion } from './dep'
import { type Link, globalVersion } from './dep'
import { activeEffectScope } from './effectScope'
import { warn } from './warning'
@ -39,6 +39,9 @@ export interface ReactiveEffectRunner<T = any> {
export let activeSub: Subscriber | undefined
export enum EffectFlags {
/**
* ReactiveEffect only
*/
ACTIVE = 1 << 0,
RUNNING = 1 << 1,
TRACKING = 1 << 2,
@ -69,42 +72,13 @@ export interface Subscriber extends DebuggerOptions {
/**
* @internal
*/
notify(): void
}
/**
* Represents a link between a source (Dep) and a subscriber (Effect or Computed).
* Deps and subs have a many-to-many relationship - each link between a
* dep and a sub is represented by a Link instance.
*
* A Link is also a node in two doubly-linked lists - one for the associated
* sub to track all its deps, and one for the associated dep to track all its
* subs.
*
* @internal
*/
export interface Link {
dep: Dep
sub: Subscriber
next?: Subscriber
/**
* - Before each effect run, all previous dep links' version are reset to -1
* - During the run, a link's version is synced with the source dep on access
* - After the run, links with version -1 (that were never used) are cleaned
* up
* returning `true` indicates it's a computed that needs to call notify
* on its dep too
* @internal
*/
version: number
/**
* Pointers for doubly-linked lists
*/
nextDep?: Link
prevDep?: Link
nextSub?: Link
prevSub?: Link
prevActiveLink?: Link
notify(): true | void
}
const pausedQueueEffects = new WeakSet<ReactiveEffect>()
@ -127,7 +101,7 @@ export class ReactiveEffect<T = any>
/**
* @internal
*/
nextEffect?: ReactiveEffect = undefined
next?: Subscriber = undefined
/**
* @internal
*/
@ -169,9 +143,7 @@ export class ReactiveEffect<T = any>
return
}
if (!(this.flags & EffectFlags.NOTIFIED)) {
this.flags |= EffectFlags.NOTIFIED
this.nextEffect = batchedEffect
batchedEffect = this
batch(this)
}
}
@ -243,8 +215,37 @@ export class ReactiveEffect<T = any>
}
}
/**
* For debugging
*/
// function printDeps(sub: Subscriber) {
// let d = sub.deps
// let ds = []
// while (d) {
// ds.push(d)
// d = d.nextDep
// }
// return ds.map(d => ({
// id: d.id,
// prev: d.prevDep?.id,
// next: d.nextDep?.id,
// }))
// }
let batchDepth = 0
let batchedEffect: ReactiveEffect | undefined
let batchedSub: Subscriber | undefined
let batchedComputed: Subscriber | undefined
export function batch(sub: Subscriber, isComputed = false): void {
sub.flags |= EffectFlags.NOTIFIED
if (isComputed) {
sub.next = batchedComputed
batchedComputed = sub
return
}
sub.next = batchedSub
batchedSub = sub
}
/**
* @internal
@ -258,23 +259,33 @@ export function startBatch(): void {
* @internal
*/
export function endBatch(): void {
if (batchDepth > 1) {
batchDepth--
if (--batchDepth > 0) {
return
}
batchDepth--
let error: unknown
while (batchedEffect) {
let e: ReactiveEffect | undefined = batchedEffect
batchedEffect = undefined
if (batchedComputed) {
let e: Subscriber | undefined = batchedComputed
batchedComputed = undefined
while (e) {
const next: ReactiveEffect | undefined = e.nextEffect
e.nextEffect = undefined
const next: Subscriber | undefined = e.next
e.next = undefined
e.flags &= ~EffectFlags.NOTIFIED
e = next
}
}
let error: unknown
while (batchedSub) {
let e: Subscriber | undefined = batchedSub
batchedSub = undefined
while (e) {
const next: Subscriber | undefined = e.next
e.next = undefined
e.flags &= ~EffectFlags.NOTIFIED
if (e.flags & EffectFlags.ACTIVE) {
try {
e.trigger()
// ACTIVE flag is effect-only
;(e as ReactiveEffect).trigger()
} catch (err) {
if (!error) error = err
}
@ -302,9 +313,11 @@ function cleanupDeps(sub: Subscriber) {
// Cleanup unsued deps
let head
let tail = sub.depsTail
for (let link = tail; link; link = link.prevDep) {
let link = tail
while (link) {
const prev = link.prevDep
if (link.version === -1) {
if (link === tail) tail = link.prevDep
if (link === tail) tail = prev
// unused - remove it from the dep's subscribing effect list
removeSub(link)
// also remove it from this effect's dep list
@ -318,6 +331,7 @@ function cleanupDeps(sub: Subscriber) {
// restore previous active link if any
link.dep.activeLink = link.prevActiveLink
link.prevActiveLink = undefined
link = prev
}
// set the new head & tail
sub.deps = head
@ -328,8 +342,9 @@ function isDirty(sub: Subscriber): boolean {
for (let link = sub.deps; link; link = link.nextDep) {
if (
link.dep.version !== link.version ||
(link.dep.computed && refreshComputed(link.dep.computed) === false) ||
link.dep.version !== link.version
(link.dep.computed &&
(refreshComputed(link.dep.computed) ||
link.dep.version !== link.version))
) {
return true
}
@ -346,10 +361,7 @@ function isDirty(sub: Subscriber): boolean {
* Returning false indicates the refresh failed
* @internal
*/
export function refreshComputed(computed: ComputedRefImpl): false | undefined {
if (computed.flags & EffectFlags.RUNNING) {
return false
}
export function refreshComputed(computed: ComputedRefImpl): undefined {
if (
computed.flags & EffectFlags.TRACKING &&
!(computed.flags & EffectFlags.DIRTY)
@ -371,7 +383,12 @@ export function refreshComputed(computed: ComputedRefImpl): false | undefined {
// and therefore tracks no deps, thus we cannot rely on the dirty check.
// Instead, computed always re-evaluate and relies on the globalVersion
// fast path above for caching.
if (dep.version > 0 && !computed.isSSR && !isDirty(computed)) {
if (
dep.version > 0 &&
!computed.isSSR &&
computed.deps &&
!isDirty(computed)
) {
computed.flags &= ~EffectFlags.RUNNING
return
}
@ -383,7 +400,7 @@ export function refreshComputed(computed: ComputedRefImpl): false | undefined {
try {
prepareDeps(computed)
const value = computed.fn()
const value = computed.fn(computed._value)
if (dep.version === 0 || hasChanged(value, computed._value)) {
computed._value = value
dep.version++
@ -399,7 +416,7 @@ export function refreshComputed(computed: ComputedRefImpl): false | undefined {
}
}
function removeSub(link: Link) {
function removeSub(link: Link, soft = false) {
const { dep, prevSub, nextSub } = link
if (prevSub) {
prevSub.nextSub = nextSub
@ -409,19 +426,33 @@ function removeSub(link: Link) {
nextSub.prevSub = prevSub
link.nextSub = undefined
}
if (__DEV__ && dep.subsHead === link) {
// was previous head, point new head to next
dep.subsHead = nextSub
}
if (dep.subs === link) {
// was previous tail, point new tail to prev
dep.subs = prevSub
if (!prevSub && dep.computed) {
// if computed, unsubscribe it from all its deps so this computed and its
// value can be GCed
dep.computed.flags &= ~EffectFlags.TRACKING
for (let l = dep.computed.deps; l; l = l.nextDep) {
// here we are only "soft" unsubscribing because the computed still keeps
// referencing the deps and the dep should not decrease its sub count
removeSub(l, true)
}
}
}
if (!dep.subs && dep.computed) {
// last subscriber removed
// if computed, unsubscribe it from all its deps so this computed and its
// value can be GCed
dep.computed.flags &= ~EffectFlags.TRACKING
for (let l = dep.computed.deps; l; l = l.nextDep) {
removeSub(l)
}
if (!soft && !--dep.sc && dep.map) {
// #11979
// property dep no longer has effect subscribers, delete it
// this mostly is for the case where an object is kept in memory but only a
// subset of its properties is tracked at one time
dep.map.delete(dep.key)
}
}
@ -509,7 +540,7 @@ export function resetTracking(): void {
* The cleanup function is called right before the next effect run, or when the
* effect is stopped.
*
* Throws a warning iff there is no currenct active effect. The warning can be
* Throws a warning if there is no current active effect. The warning can be
* suppressed by passing `true` to the second argument.
*
* @param fn - the cleanup function to be registered

View File

@ -53,12 +53,13 @@ export class EffectScope {
pause(): void {
if (this._active) {
this._isPaused = true
let i, l
if (this.scopes) {
for (let i = 0, l = this.scopes.length; i < l; i++) {
for (i = 0, l = this.scopes.length; i < l; i++) {
this.scopes[i].pause()
}
}
for (let i = 0, l = this.effects.length; i < l; i++) {
for (i = 0, l = this.effects.length; i < l; i++) {
this.effects[i].pause()
}
}
@ -71,12 +72,13 @@ export class EffectScope {
if (this._active) {
if (this._isPaused) {
this._isPaused = false
let i, l
if (this.scopes) {
for (let i = 0, l = this.scopes.length; i < l; i++) {
for (i = 0, l = this.scopes.length; i < l; i++) {
this.scopes[i].resume()
}
}
for (let i = 0, l = this.effects.length; i < l; i++) {
for (i = 0, l = this.effects.length; i < l; i++) {
this.effects[i].resume()
}
}

View File

@ -1,4 +1,4 @@
import { def, isObject, toRawType } from '@vue/shared'
import { def, hasOwn, isObject, toRawType } from '@vue/shared'
import {
mutableHandlers,
readonlyHandlers,
@ -167,7 +167,7 @@ export type DeepReadonly<T> = T extends Builtin
? WeakSet<DeepReadonly<U>>
: T extends Promise<infer U>
? Promise<DeepReadonly<U>>
: T extends Ref<infer U>
: T extends Ref<infer U, unknown>
? Readonly<Ref<DeepReadonly<U>>>
: T extends {}
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
@ -405,7 +405,7 @@ export type Raw<T> = T & { [RawSymbol]?: true }
* @see {@link https://vuejs.org/api/reactivity-advanced.html#markraw}
*/
export function markRaw<T extends object>(value: T): Raw<T> {
if (Object.isExtensible(value)) {
if (!hasOwn(value, ReactiveFlags.SKIP) && Object.isExtensible(value)) {
def(value, ReactiveFlags.SKIP, true)
}
return value

View File

@ -62,7 +62,9 @@ export function ref(value?: unknown) {
declare const ShallowRefMarker: unique symbol
export type ShallowRef<T = any> = Ref<T> & { [ShallowRefMarker]?: true }
export type ShallowRef<T = any, S = T> = Ref<T, S> & {
[ShallowRefMarker]?: true
}
/**
* Shallow version of {@link ref()}.
@ -182,15 +184,18 @@ class RefImpl<T = any> {
* @see {@link https://vuejs.org/api/reactivity-advanced.html#triggerref}
*/
export function triggerRef(ref: Ref): void {
if (__DEV__) {
;(ref as unknown as RefImpl).dep.trigger({
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: (ref as unknown as RefImpl)._value,
})
} else {
;(ref as unknown as RefImpl).dep.trigger()
// ref may be an instance of ObjectRefImpl
if ((ref as unknown as RefImpl).dep) {
if (__DEV__) {
;(ref as unknown as RefImpl).dep.trigger({
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: (ref as unknown as RefImpl)._value,
})
} else {
;(ref as unknown as RefImpl).dep.trigger()
}
}
}
@ -243,7 +248,10 @@ export function toValue<T>(source: MaybeRefOrGetter<T>): T {
}
const shallowUnwrapHandlers: ProxyHandler<any> = {
get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)),
get: (target, key, receiver) =>
key === ReactiveFlags.RAW
? target
: unref(Reflect.get(target, key, receiver)),
set: (target, key, value, receiver) => {
const oldValue = target[key]
if (isRef(oldValue) && !isRef(value)) {
@ -481,12 +489,12 @@ export type ShallowUnwrapRef<T> = {
[K in keyof T]: DistributeRef<T[K]>
}
type DistributeRef<T> = T extends Ref<infer V> ? V : T
type DistributeRef<T> = T extends Ref<infer V, unknown> ? V : T
export type UnwrapRef<T> =
T extends ShallowRef<infer V>
T extends ShallowRef<infer V, unknown>
? V
: T extends Ref<infer V>
: T extends Ref<infer V, unknown>
? UnwrapRefSimple<V>
: UnwrapRefSimple<T>

View File

@ -95,6 +95,10 @@ export function getCurrentWatcher(): ReactiveEffect<any> | undefined {
* associated effect re-runs.
*
* @param cleanupFn - The callback function to attach to the effect's cleanup.
* @param failSilently - if `true`, will not throw warning when called without
* an active effect.
* @param owner - The effect that this cleanup function should be attached to.
* By default, the current active effect.
*/
export function onWatcherCleanup(
cleanupFn: () => void,
@ -214,19 +218,11 @@ export function watch(
}
}
if (once) {
if (cb) {
const _cb = cb
cb = (...args) => {
_cb(...args)
watchHandle()
}
} else {
const _getter = getter
getter = () => {
_getter()
watchHandle()
}
if (once && cb) {
const _cb = cb
cb = (...args) => {
_cb(...args)
watchHandle()
}
}

View File

@ -843,4 +843,37 @@ describe('api: defineAsyncComponent', () => {
await timeout()
expect(serializeInner(root)).toBe('Bar')
})
// 11916
test('with KeepAlive + include', async () => {
const spy = vi.fn()
let resolve: (comp: Component) => void
const Foo = defineAsyncComponent(
() =>
new Promise(r => {
resolve = r as any
}),
)
const root = nodeOps.createElement('div')
const app = createApp({
render: () => h(KeepAlive, { include: 'Foo' }, [h(Foo)]),
})
app.mount(root)
await nextTick()
resolve!({
name: 'Foo',
setup() {
onActivated(spy)
return () => 'Foo'
},
})
await timeout()
expect(serializeInner(root)).toBe('Foo')
expect(spy).toBeCalledTimes(1)
})
})

View File

@ -37,6 +37,7 @@ import {
toRef,
triggerRef,
} from '@vue/reactivity'
import { renderToString } from '@vue/server-renderer'
describe('api: watch', () => {
it('effect', async () => {
@ -373,6 +374,43 @@ describe('api: watch', () => {
expect(dummy).toBe(0)
})
it('stopping the watcher (SSR)', async () => {
let dummy = 0
const count = ref<number>(1)
const captureValue = (value: number) => {
dummy = value
}
const watchCallback = vi.fn(newValue => {
captureValue(newValue)
})
const Comp = defineComponent({
created() {
const getter = () => this.count
captureValue(getter()) // sets dummy to 1
const stop = this.$watch(getter, watchCallback)
stop()
this.count = 2 // shouldn't trigger side effect
},
render() {
return h('div', this.count)
},
setup() {
return { count }
},
})
let html
html = await renderToString(h(Comp))
// should not throw here
expect(html).toBe(`<div>2</div>`)
expect(watchCallback).not.toHaveBeenCalled()
expect(dummy).toBe(1)
await nextTick()
count.value = 3 // shouldn't trigger side effect
await nextTick()
expect(watchCallback).not.toHaveBeenCalled()
expect(dummy).toBe(1)
})
it('stopping the watcher (with source)', async () => {
const state = reactive({ count: 0 })
let dummy
@ -1892,7 +1930,7 @@ describe('api: watch', () => {
warn.mockRestore()
})
it('should be executed correctly', () => {
test('should be executed correctly', () => {
const v = ref(1)
let foo = ''
@ -1919,4 +1957,30 @@ describe('api: watch', () => {
v.value++
expect(foo).toBe('12')
})
// 12045
test('sync watcher should not break pre watchers', async () => {
const count1 = ref(0)
const count2 = ref(0)
watch(
count1,
() => {
count2.value++
},
{ flush: 'sync' },
)
const spy1 = vi.fn()
watch([count1, count2], spy1)
const spy2 = vi.fn()
watch(count1, spy2)
count1.value++
await nextTick()
expect(spy1).toHaveBeenCalled()
expect(spy2).toHaveBeenCalled()
})
})

View File

@ -333,6 +333,30 @@ describe('component props', () => {
})
})
//#12011
test('replace camelize with hyphenate to handle props key', () => {
const Comp = {
props: {
hasB4BProp: { type: Boolean, required: true },
},
setup() {
return () => null
},
}
render(
h('div', {}, [
h(Comp, {
'has-b-4-b-prop': true,
}),
h(Comp, {
'has-b4-b-prop': true,
}),
]),
nodeOps.createElement('div'),
)
expect(`Missing required prop: "hasB4BProp"`).not.toHaveBeenWarned()
})
test('warn props mutation', () => {
let instance: ComponentInternalInstance
let setupProps: any

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