diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..8dc9efaf --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,21 @@ +name: Github Release + +on: + push: + tags: + - v* + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-node@v3 + - name: Create Release + run: npx conventional-github-releaser -p angular + env: + CI: true + CONVENTIONAL_GITHUB_RELEASER_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000..868cc2b6 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,27 @@ +# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. +# +# You can adjust the behavior by modifying this file. +# For more information, see: +# https://github.com/actions/stale +name: Mark stale issues and pull requests + +on: + schedule: + - cron: '39 23 * * *' + +jobs: + stale: + + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + + steps: + - uses: actions/stale@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'Stale issue message' + stale-pr-message: 'Stale pull request message' + stale-issue-label: 'no-issue-activity' + stale-pr-label: 'no-pr-activity' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..b045758f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,37 @@ +name: Build & Test + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [14.x, 16.x] + + steps: + - uses: actions/checkout@v3 + + - name: Install pnpm + uses: pnpm/action-setup@v2 + + - name: Set node version to ${{ matrix.node_version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node_version }} + cache: "pnpm" + + - name: Install + run: pnpm i + + - name: Unit Tests + run: pnpm run test + env: + CI: true + + - name: Typing Declaration Tests + run: pnpm run test:dts + env: + CI: true + diff --git a/.gitignore b/.gitignore index 58a815e2..3928fc89 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ lib .idea .rpt2_cache TODO.md +typings \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..bf2e7648 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +shamefully-hoist=true diff --git a/CHANGELOG.md b/CHANGELOG.md index f408cf09..c09fd621 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,31 +1,1305 @@ + +## [1.7.2](https://github.com/vuejs/composition-api/compare/v1.7.1...v1.7.2) (2023-08-15) + + +### Bug Fixes + +* **types:** function constructor should be a Function ([#972](https://github.com/vuejs/composition-api/issues/972)) ([6247ba3](https://github.com/vuejs/composition-api/commit/6247ba3)) + + +### Features + +* support inject key types ([#986](https://github.com/vuejs/composition-api/issues/986)) ([599423c](https://github.com/vuejs/composition-api/commit/599423c)) + + + + +## [1.7.1](https://github.com/vuejs/composition-api/compare/v1.7.0...v1.7.1) (2022-09-23) + + +### Bug Fixes + +* **types:** add `Date` to `BaseTypes` ([#957](https://github.com/vuejs/composition-api/issues/957)) ([c3f4e91](https://github.com/vuejs/composition-api/commit/c3f4e91)) +* **types:** add types to package export ([#968](https://github.com/vuejs/composition-api/issues/968)) ([39f2036](https://github.com/vuejs/composition-api/commit/39f2036)) + + + + +# [1.7.0](https://github.com/vuejs/composition-api/compare/v1.6.3...v1.7.0) (2022-07-01) + + +### Bug Fixes + +* add note about Vue 2.7 ([6117f81](https://github.com/vuejs/composition-api/commit/6117f81)) + + + + +## [1.6.3](https://github.com/vuejs/composition-api/compare/v1.6.2...v1.6.3) (2022-06-24) + + +### Features + +* support Vue3 style app-level provide ([#945](https://github.com/vuejs/composition-api/issues/945)) ([d167b9b](https://github.com/vuejs/composition-api/commit/d167b9b)) + + + + +## [1.6.2](https://github.com/vuejs/composition-api/compare/v1.6.1...v1.6.2) (2022-05-23) + + +### Features + +* **types:** fix props JSDoc loss ([#935](https://github.com/vuejs/composition-api/issues/935)) ([fcee038](https://github.com/vuejs/composition-api/commit/fcee038)) + + + + +## [1.6.1](https://github.com/vuejs/composition-api/compare/v1.6.0...v1.6.1) (2022-05-05) + + +### Bug Fixes + +* **inject:** allow default value to be undefined ([#930](https://github.com/vuejs/composition-api/issues/930)) ([17d3fc1](https://github.com/vuejs/composition-api/commit/17d3fc1)) + + + + +# [1.6.0](https://github.com/vuejs/composition-api/compare/v1.5.0...v1.6.0) (2022-04-27) + + +### Bug Fixes + +* nullish check for vnode ([#925](https://github.com/vuejs/composition-api/issues/925)) ([293f03b](https://github.com/vuejs/composition-api/commit/293f03b)) +* **type:** align watch types with vue3 ([#927](https://github.com/vuejs/composition-api/issues/927)) ([679f5c2](https://github.com/vuejs/composition-api/commit/679f5c2)) + + + + +# [1.5.0](https://github.com/vuejs/composition-api/compare/v1.4.10...v1.5.0) (2022-04-25) + + +### Features + +* **createElement:** allow createElement to bind vm ([#920](https://github.com/vuejs/composition-api/issues/920)) ([564a5a4](https://github.com/vuejs/composition-api/commit/564a5a4)) +* Unified as the key of raw in vue3 ([#922](https://github.com/vuejs/composition-api/issues/922)) ([5874eb5](https://github.com/vuejs/composition-api/commit/5874eb5)) + + + + +## [1.4.10](https://github.com/vuejs/composition-api/compare/v1.4.9...v1.4.10) (2022-03-16) + + + + +## [1.4.9](https://github.com/vuejs/composition-api/compare/v1.4.8...v1.4.9) (2022-02-26) + + + + +## [1.4.8](https://github.com/vuejs/composition-api/compare/v1.4.7...v1.4.8) (2022-02-26) + + +### Bug Fixes + +* **types:** optional Boolean props as default props ([#909](https://github.com/vuejs/composition-api/issues/909)) ([8f88ae6](https://github.com/vuejs/composition-api/commit/8f88ae6)) +* use registered Vue instance for warning ([b01f1e4](https://github.com/vuejs/composition-api/commit/b01f1e4)) + + + + +## [1.4.7](https://github.com/vuejs/composition-api/compare/v1.4.6...v1.4.7) (2022-02-24) + + +### Bug Fixes + +* markRaw in watch ([#903](https://github.com/vuejs/composition-api/issues/903)) ([192f4c1](https://github.com/vuejs/composition-api/commit/192f4c1)) + + + + +## [1.4.6](https://github.com/vuejs/composition-api/compare/v1.4.5...v1.4.6) (2022-02-07) + + +### Bug Fixes + +* dynamically update deep scopedSlot refs ([#899](https://github.com/vuejs/composition-api/issues/899)) ([ef312a3](https://github.com/vuejs/composition-api/commit/ef312a3)) + + + + +## [1.4.5](https://github.com/vuejs/composition-api/compare/v1.4.4...v1.4.5) (2022-01-27) + + +### Bug Fixes + +* **reactive:** remove useless proxy logic of shallowReactive ([#890](https://github.com/vuejs/composition-api/issues/890)) ([7243ffa](https://github.com/vuejs/composition-api/commit/7243ffa)) +* **shallowReactive:** don't trigger watchers for oldVal === newVal ([#894](https://github.com/vuejs/composition-api/issues/894)) ([2a88e71](https://github.com/vuejs/composition-api/commit/2a88e71)) + + + + +## [1.4.4](https://github.com/vuejs/composition-api/compare/v1.4.3...v1.4.4) (2022-01-16) + + +### Bug Fixes + +* **types:** `emit` type in SetupContext ([#884](https://github.com/vuejs/composition-api/issues/884)) ([5c35403](https://github.com/vuejs/composition-api/commit/5c35403)) +* **types:** update inject and unref type ([#888](https://github.com/vuejs/composition-api/issues/888)) ([315f6ab](https://github.com/vuejs/composition-api/commit/315f6ab)) + + + + +## [1.4.3](https://github.com/vuejs/composition-api/compare/v1.4.2...v1.4.3) (2021-12-27) + + +### Bug Fixes + +* [#874](https://github.com/vuejs/composition-api/issues/874) setup return object with type of Module ([#875](https://github.com/vuejs/composition-api/issues/875)) ([a1a3e11](https://github.com/vuejs/composition-api/commit/a1a3e11)) +* improve register check, close [#876](https://github.com/vuejs/composition-api/issues/876) ([#878](https://github.com/vuejs/composition-api/issues/878)) ([3c2ad93](https://github.com/vuejs/composition-api/commit/3c2ad93)) + + + + +## [1.4.2](https://github.com/vuejs/composition-api/compare/v1.4.1...v1.4.2) (2021-12-17) + + +### Bug Fixes + +* export Directives type ([#864](https://github.com/vuejs/composition-api/issues/864)) ([9276e30](https://github.com/vuejs/composition-api/commit/9276e30)) +* resolve conflicts with vue2 interface ([#869](https://github.com/vuejs/composition-api/issues/869)) ([e84b488](https://github.com/vuejs/composition-api/commit/e84b488)) + + + + +## [1.4.1](https://github.com/vuejs/composition-api/compare/v1.4.0...v1.4.1) (2021-11-30) + + +### Bug Fixes + +* **toRef:** issue [#855](https://github.com/vuejs/composition-api/issues/855) ([#859](https://github.com/vuejs/composition-api/issues/859)) ([b3e61a4](https://github.com/vuejs/composition-api/commit/b3e61a4)) + + +### Features + +* add directives type support ([#863](https://github.com/vuejs/composition-api/issues/863)) ([678a4b3](https://github.com/vuejs/composition-api/commit/678a4b3)) + + + + +# [1.4.0](https://github.com/vuejs/composition-api/compare/v1.4.0-beta.0...v1.4.0) (2021-11-14) + + + + +# [1.4.0-beta.0](https://github.com/vuejs/composition-api/compare/v1.3.3...v1.4.0-beta.0) (2021-11-07) + + +### Features + +* add component $emit typing support ([#846](https://github.com/vuejs/composition-api/issues/846)) ([b980175](https://github.com/vuejs/composition-api/commit/b980175)) + + + + +## [1.3.3](https://github.com/vuejs/composition-api/compare/v1.3.2...v1.3.3) (2021-11-03) + + +### Features + +* **types:** allow a generic in App type ([#845](https://github.com/vuejs/composition-api/issues/845)) ([48729d9](https://github.com/vuejs/composition-api/commit/48729d9)) + + + + +## [1.3.2](https://github.com/vuejs/composition-api/compare/v1.3.1...v1.3.2) (2021-11-03) + + +### Bug Fixes + +* changing prop causes rerender to lose attributes [#840](https://github.com/vuejs/composition-api/issues/840) ([#843](https://github.com/vuejs/composition-api/issues/843)) ([a43090d](https://github.com/vuejs/composition-api/commit/a43090d)) + + + + +## [1.3.1](https://github.com/vuejs/composition-api/compare/v1.3.0...v1.3.1) (2021-11-01) + + +### Bug Fixes + +* **types:** defineComponent object format with no props type ([#839](https://github.com/vuejs/composition-api/issues/839)) ([8a31c78](https://github.com/vuejs/composition-api/commit/8a31c78)) + + + + +# [1.3.0](https://github.com/vuejs/composition-api/compare/v1.2.4...v1.3.0) (2021-10-28) + + +### Bug Fixes + +* attrs update not correctly mapped to props [#833](https://github.com/vuejs/composition-api/issues/833) ([#835](https://github.com/vuejs/composition-api/issues/835)) ([90b086b](https://github.com/vuejs/composition-api/commit/90b086b)) + + + + +## [1.2.4](https://github.com/vuejs/composition-api/compare/v1.2.3...v1.2.4) (2021-10-07) + + +### Bug Fixes + +* add `type` to component instance ([#828](https://github.com/vuejs/composition-api/issues/828)) ([b9b603f](https://github.com/vuejs/composition-api/commit/b9b603f)) + + + + +## [1.2.3](https://github.com/vuejs/composition-api/compare/v1.2.2...v1.2.3) (2021-10-05) + + +### Bug Fixes + +* **proxyRefs:** When using proxyRefs, the internal variable composition-api.refKey is exposed on the object itself [#817](https://github.com/vuejs/composition-api/issues/817) ([#818](https://github.com/vuejs/composition-api/issues/818)) ([92b7eb1](https://github.com/vuejs/composition-api/commit/92b7eb1)) +* **ssr:** `set()` twice lose reactivity ([#821](https://github.com/vuejs/composition-api/issues/821)) ([416845a](https://github.com/vuejs/composition-api/commit/416845a)) +* correct prop type inference when using PropType ([#825](https://github.com/vuejs/composition-api/issues/825)) ([9c9f8e8](https://github.com/vuejs/composition-api/commit/9c9f8e8)) + + +### Features + +* **computed:** allow differentiating refs from computed ([#820](https://github.com/vuejs/composition-api/issues/820)) ([68b5d97](https://github.com/vuejs/composition-api/commit/68b5d97)) + + + + +## [1.2.2](https://github.com/vuejs/composition-api/compare/v1.2.1...v1.2.2) (2021-09-24) + + +### Reverts + +* "fix: use `.mjs` by default", close [#815](https://github.com/vuejs/composition-api/issues/815) ([96899ce](https://github.com/vuejs/composition-api/commit/96899ce)) + + + + +## [1.2.1](https://github.com/vuejs/composition-api/compare/v1.2.0...v1.2.1) (2021-09-21) + + +### Features + +* **types:** align ComponentPublicInstance type ([2f9cfbf](https://github.com/vuejs/composition-api/commit/2f9cfbf)) + + + + +# [1.2.0](https://github.com/vuejs/composition-api/compare/v1.1.5...v1.2.0) (2021-09-21) + + +### Bug Fixes + +* importing from esm in node ([#814](https://github.com/vuejs/composition-api/issues/814)) ([8c61b07](https://github.com/vuejs/composition-api/commit/8c61b07)) +* use `.mjs` by default ([2699348](https://github.com/vuejs/composition-api/commit/2699348)) + + + + +## [1.1.5](https://github.com/vuejs/composition-api/compare/v1.1.4...v1.1.5) (2021-09-09) + + +### Bug Fixes + +* improve `isReadonly` behaviour, close [#811](https://github.com/vuejs/composition-api/issues/811), close [#812](https://github.com/vuejs/composition-api/issues/812) ([d3c456a](https://github.com/vuejs/composition-api/commit/d3c456a)) +* **api-watch:** watching nested ref array w/ deep doesn't work ([#808](https://github.com/vuejs/composition-api/issues/808)) ([b625420](https://github.com/vuejs/composition-api/commit/b625420)) + + + + +## [1.1.4](https://github.com/vuejs/composition-api/compare/v1.1.3...v1.1.4) (2021-08-31) + + +### Bug Fixes + +* **types:** align emits type with vue-next ([565cbd1](https://github.com/vuejs/composition-api/commit/565cbd1)) + + + + +## [1.1.3](https://github.com/vuejs/composition-api/compare/v1.1.2...v1.1.3) (2021-08-22) + + + + +## [1.1.2](https://github.com/vuejs/composition-api/compare/v1.1.1...v1.1.2) (2021-08-21) + + +### Bug Fixes + +* **set:** reactive in SSR w/ set ([#796](https://github.com/vuejs/composition-api/issues/796)) ([3a1837f](https://github.com/vuejs/composition-api/commit/3a1837f)) +* **setup:** should not trigger getter w/ object computed nested ([#799](https://github.com/vuejs/composition-api/issues/799)) ([72a878d](https://github.com/vuejs/composition-api/commit/72a878d)) +* **watch:** always triggers when watching multiple refs ([#791](https://github.com/vuejs/composition-api/issues/791)) ([8beffc3](https://github.com/vuejs/composition-api/commit/8beffc3)) +* typos ([#788](https://github.com/vuejs/composition-api/issues/788)) ([59653ac](https://github.com/vuejs/composition-api/commit/59653ac)) + + +### Features + +* implement api `useSlots` and `useAttrs` ([#800](https://github.com/vuejs/composition-api/issues/800)) ([1e6e3a9](https://github.com/vuejs/composition-api/commit/1e6e3a9)) + + + + +## [1.1.1](https://github.com/vuejs/composition-api/compare/v1.1.0...v1.1.1) (2021-08-14) + + +### Bug Fixes + +* don't invoke Vue getters in setter ([#786](https://github.com/vuejs/composition-api/issues/786)) ([e67940f](https://github.com/vuejs/composition-api/commit/e67940f)), closes [#498](https://github.com/vuejs/composition-api/issues/498) + + + + +# [1.1.0](https://github.com/vuejs/composition-api/compare/v1.1.0-beta.7...v1.1.0) (2021-08-09) + + +### Features + +* align with Vue [v3.2](https://blog.vuejs.org/posts/vue-3.2.html) +* new `watchPostEffect` api ([92fe90c](https://github.com/vuejs/composition-api/commit/92fe90c)) +* new `watchSyncEffect` api ([e12c23d](https://github.com/vuejs/composition-api/commit/e12c23d)) +* new `effectScope` api ([#762](https://github.com/vuejs/composition-api/issues/762)) ([fcadec2](https://github.com/vuejs/composition-api/commit/fcadec2)) + + +## [1.0.6](https://github.com/vuejs/composition-api/compare/v1.1.0-beta.5...v1.0.6) (2021-08-09) + + +### Features + +* support second target argument for lifecycle functions ([3f3b9c6](https://github.com/vuejs/composition-api/commit/3f3b9c6)) + + + + +# [1.1.0-beta.7](https://github.com/vuejs/composition-api/compare/v1.1.0-beta.6...v1.1.0-beta.7) (2021-08-09) + +### Bug Fixes + +* **effectScope:** should have a vaild scope with component ([da21873](https://github.com/vuejs/composition-api/commit/da21873)) + + + +# [1.1.0-beta.6](https://github.com/vuejs/composition-api/compare/v1.1.0-beta.5...v1.1.0-beta.6) (2021-08-09) + +* new watchPostEffect api ([92fe90c](https://github.com/vuejs/composition-api/commit/92fe90c)) +* support second target argument for lifecycle functions ([0133c1e](https://github.com/vuejs/composition-api/commit/0133c1e)) + + + +# [1.1.0-beta.4](https://github.com/vuejs/composition-api/compare/v1.0.4...v1.1.0-beta.4) (2021-07-22) + + +### Bug Fixes + +* revert module field to `esm.js` version, close [#769](https://github.com/vuejs/composition-api/issues/769) ([92afa6f](https://github.com/vuejs/composition-api/commit/92afa6f)) + + + + +# [1.1.0-beta.3](https://github.com/vuejs/composition-api/compare/v1.0.3...v1.1.0-beta.3) (2021-07-18) + + +### Bug Fixes + +* build for mjs and exports all submodules ([c116714](https://github.com/vuejs/composition-api/commit/c116714)) + + + + +# [1.1.0-beta.2](https://github.com/vuejs/composition-api/compare/v1.1.0-beta.1...v1.1.0-beta.2) (2021-07-16) + + +### Bug Fixes + +* **effectScope:** should stop along with parent component ([784d96c](https://github.com/vuejs/composition-api/commit/784d96c)) + + + + +# [1.1.0-beta.1](https://github.com/vuejs/composition-api/compare/v1.0.2...v1.1.0-beta.1) (2021-07-16) + + +### Features + +* implement `effectScope` api ([#762](https://github.com/vuejs/composition-api/issues/762)) ([fcadec2](https://github.com/vuejs/composition-api/commit/fcadec2)) + + +### Features + +* implement `effectScope` api ([#762](https://github.com/vuejs/composition-api/issues/762)) ([fcadec2](https://github.com/vuejs/composition-api/commit/fcadec2)) +* support second target argument for lifecycle functions ([3f3b9c6](https://github.com/vuejs/composition-api/commit/3f3b9c6)) + + + +## [1.0.6](https://github.com/vuejs/composition-api/compare/v1.0.5...v1.0.6) (2021-08-09) + + +### Features + + +## [1.0.5](https://github.com/vuejs/composition-api/compare/v1.0.4...v1.0.5) (2021-08-01) + + +### Bug Fixes + +* **function:** properties of function should not disappear. ([#778](https://github.com/vuejs/composition-api/issues/778)) ([68c1a35](https://github.com/vuejs/composition-api/commit/68c1a35)) + + + + +## [1.0.4](https://github.com/vuejs/composition-api/compare/v1.0.3...v1.0.4) (2021-07-22) + + +### Bug Fixes + +* revert module field to `esm.js` version, close [#769](https://github.com/vuejs/composition-api/issues/769) ([4ac545c](https://github.com/vuejs/composition-api/commit/4ac545c)) + + + + +## [1.0.3](https://github.com/vuejs/composition-api/compare/v1.0.2...v1.0.3) (2021-07-18) + + +### Bug Fixes + +* build for mjs and exports all submodules ([69538ee](https://github.com/vuejs/composition-api/commit/69538ee)) + + + + +## [1.0.2](https://github.com/vuejs/composition-api/compare/v1.0.1...v1.0.2) (2021-07-16) + + +### Bug Fixes + +* **readonly:** align behavior with vue-next. ([#765](https://github.com/vuejs/composition-api/issues/765)) ([42104aa](https://github.com/vuejs/composition-api/commit/42104aa)) +* **type:** remove unnecessary type assertion ([#766](https://github.com/vuejs/composition-api/issues/766)) ([ebb7975](https://github.com/vuejs/composition-api/commit/ebb7975)) +* should dynamically update refs in context ([#764](https://github.com/vuejs/composition-api/issues/764)) ([d7de23e](https://github.com/vuejs/composition-api/commit/d7de23e)) + + + + +## [1.0.1](https://github.com/vuejs/composition-api/compare/v1.0.0...v1.0.1) (2021-07-16) + + + + +# [1.0.0](https://github.com/vuejs/composition-api/compare/v1.0.0-rc.14...v1.0.0) (2021-07-15) + + +### Bug Fixes + +* **mockReactivityDeep:** add parameter seen for mockReactivityDeep. ([#759](https://github.com/vuejs/composition-api/issues/759)) ([40cb14a](https://github.com/vuejs/composition-api/commit/40cb14a)) +* **runtime-core:** trigger warning when the injectionKey is undefined ([#760](https://github.com/vuejs/composition-api/issues/760)) ([2ccad9b](https://github.com/vuejs/composition-api/commit/2ccad9b)) + + + + +# [1.0.0-rc.14](https://github.com/vuejs/composition-api/compare/v1.0.0-rc.13...v1.0.0-rc.14) (2021-07-12) + + +### Bug Fixes + +* **customReactive:** avoid circular reference. ([#758](https://github.com/vuejs/composition-api/issues/758)) ([2bd6ea5](https://github.com/vuejs/composition-api/commit/2bd6ea5)) +* **watch:** traverse refs in deep watch ([#753](https://github.com/vuejs/composition-api/issues/753)) ([55a0a20](https://github.com/vuejs/composition-api/commit/55a0a20)) +* only trigger warning in the dev environment ([#755](https://github.com/vuejs/composition-api/issues/755)) ([bc7c2af](https://github.com/vuejs/composition-api/commit/bc7c2af)) +* **watch:** errors thrown in the asynchronous callback function in watch will not be caught. ([#751](https://github.com/vuejs/composition-api/issues/751)) ([f0e423f](https://github.com/vuejs/composition-api/commit/f0e423f)) +* **watch:** only trigger warning in the dev environment ([#754](https://github.com/vuejs/composition-api/issues/754)) ([0fe0088](https://github.com/vuejs/composition-api/commit/0fe0088)) + + + + +# [1.0.0-rc.13](https://github.com/vuejs/composition-api/compare/v1.0.0-rc.12...v1.0.0-rc.13) (2021-07-02) + + +### Bug Fixes + +* **observe:** solve the Ref not unwrapping on the ssr side issue with recursive way. ([#723](https://github.com/vuejs/composition-api/issues/723)) ([debd37d](https://github.com/vuejs/composition-api/commit/debd37d)) +* the hasOwn should be used to determine whether an attribute exists. ([#737](https://github.com/vuejs/composition-api/issues/737)) ([65abcb4](https://github.com/vuejs/composition-api/commit/65abcb4)) +* **shallowReadonly:** align behavior with vue-next ([#741](https://github.com/vuejs/composition-api/issues/741)) ([14d1c7b](https://github.com/vuejs/composition-api/commit/14d1c7b)) +* **types:** use AnyObject insteads of any ([#742](https://github.com/vuejs/composition-api/issues/742)) ([efb4195](https://github.com/vuejs/composition-api/commit/efb4195)) + + + + +# [1.0.0-rc.12](https://github.com/vuejs/composition-api/compare/v1.0.0-rc.11...v1.0.0-rc.12) (2021-06-17) + + +### Bug Fixes + +* **proxyRefs:** infinite loop when using proxyRefs. ([#730](https://github.com/vuejs/composition-api/issues/730)) ([0b6ab25](https://github.com/vuejs/composition-api/commit/0b6ab25)) +* **reactivity:** check type of __ob__ in isRaw and isReactive ([#732](https://github.com/vuejs/composition-api/issues/732)) ([97dd671](https://github.com/vuejs/composition-api/commit/97dd671)) +* **watch:** watched previous values can't be destructure on first fire. ([#727](https://github.com/vuejs/composition-api/issues/727)) ([b3ab6f9](https://github.com/vuejs/composition-api/commit/b3ab6f9)) + + + + +# [1.0.0-rc.11](https://github.com/vuejs/composition-api/compare/v1.0.0-rc.10...v1.0.0-rc.11) (2021-06-04) + + +### Bug Fixes + +* **reactivity:** should trigger watchEffect when using set to change value of array length ([#720](https://github.com/vuejs/composition-api/issues/720)) ([9c03a45](https://github.com/vuejs/composition-api/commit/9c03a45)) +* **reactivity:** unexpected behaviors for array index out of valid array length when set and del ([#719](https://github.com/vuejs/composition-api/issues/719)) ([f08a1d6](https://github.com/vuejs/composition-api/commit/f08a1d6)) +* **shallowReactive:** should keep array as array ([#717](https://github.com/vuejs/composition-api/issues/717)) ([620d09b](https://github.com/vuejs/composition-api/commit/620d09b)) +* **shallowReadonly:** watch should work for ref/reactive with shallowReadonly ([#714](https://github.com/vuejs/composition-api/issues/714)) ([b6fc1f7](https://github.com/vuejs/composition-api/commit/b6fc1f7)) + + +### Features + +* **reactivity:** unwrap value when using `set` ([#722](https://github.com/vuejs/composition-api/issues/722)) ([bd198e7](https://github.com/vuejs/composition-api/commit/bd198e7)) + + + + +# [1.0.0-rc.10](https://github.com/vuejs/composition-api/compare/v1.0.0-rc.9...v1.0.0-rc.10) (2021-05-27) + + +### Bug Fixes + +* **dev:** setup data in nextTick is proxied to vm._data. ([#697](https://github.com/vuejs/composition-api/issues/697)) ([e231837](https://github.com/vuejs/composition-api/commit/e231837)) +* **watch:** align behavior with vue-next(doWatch). ([#710](https://github.com/vuejs/composition-api/issues/710)) ([fcf8bc3](https://github.com/vuejs/composition-api/commit/fcf8bc3)) + + + + +# [1.0.0-rc.9](https://github.com/vuejs/composition-api/compare/v1.0.0-rc.8...v1.0.0-rc.9) (2021-05-19) + + +### Bug Fixes + +* The behavior of development and production merge should be consistent. ([#694](https://github.com/vuejs/composition-api/issues/694)) ([7ca7010](https://github.com/vuejs/composition-api/commit/7ca7010)) +* **shallowReactive:** align behavior with vue-next ([#696](https://github.com/vuejs/composition-api/issues/696)) ([3485ecb](https://github.com/vuejs/composition-api/commit/3485ecb)) + + +### Features + +* add and delete object attributes would trigger update. ([#692](https://github.com/vuejs/composition-api/issues/692)) ([8c27d80](https://github.com/vuejs/composition-api/commit/8c27d80)) + + + + +# [1.0.0-rc.8](https://github.com/vuejs/composition-api/compare/v1.0.0-rc.7...v1.0.0-rc.8) (2021-04-29) + + +### Bug Fixes + +* **reactive:** align behavior with vue-next ([#689](https://github.com/vuejs/composition-api/issues/689)) ([37fcbaa](https://github.com/vuejs/composition-api/commit/37fcbaa)) +* Memory leak caused by global variables. ([#686](https://github.com/vuejs/composition-api/issues/686)) ([badff82](https://github.com/vuejs/composition-api/commit/badff82)) + + + + +# [1.0.0-rc.7](https://github.com/vuejs/composition-api/compare/v1.0.0-rc.6...v1.0.0-rc.7) (2021-04-18) + + +### Bug Fixes + +* **types:** optional Boolean prop types [#636](https://github.com/vuejs/composition-api/issues/636) ([#678](https://github.com/vuejs/composition-api/issues/678)) ([a081227](https://github.com/vuejs/composition-api/commit/a081227)) + + +### Features + +* **types:** export ComponentInternalInstance, close [#677](https://github.com/vuejs/composition-api/issues/677), close [#675](https://github.com/vuejs/composition-api/issues/675) ([ccae670](https://github.com/vuejs/composition-api/commit/ccae670)) + + + + +# [1.0.0-rc.6](https://github.com/vuejs/composition-api/compare/v1.0.0-rc.5...v1.0.0-rc.6) (2021-03-29) + + +### Bug Fixes + +* **types:** allow any object in toRefs ([#668](https://github.com/vuejs/composition-api/issues/668)) ([7284ad9](https://github.com/vuejs/composition-api/commit/7284ad9)) + + + + +# [1.0.0-rc.5](https://github.com/vuejs/composition-api/compare/v1.0.0-rc.4...v1.0.0-rc.5) (2021-03-11) + + + + +# [1.0.0-rc.4](https://github.com/vuejs/composition-api/compare/v1.0.0-rc.3...v1.0.0-rc.4) (2021-03-11) + + +### Bug Fixes + +* **types:** RequiredKeys type ([#655](https://github.com/vuejs/composition-api/issues/655)) ([0677a18](https://github.com/vuejs/composition-api/commit/0677a18)) + + + + +# [1.0.0-rc.3](https://github.com/vuejs/composition-api/compare/v1.0.0-rc.2...v1.0.0-rc.3) (2021-03-03) + + +### Bug Fixes + +* update types to algin with vue-next ([#653](https://github.com/vuejs/composition-api/issues/653)) ([24eaa56](https://github.com/vuejs/composition-api/commit/24eaa56)) + + + + +# [1.0.0-rc.2](https://github.com/vuejs/composition-api/compare/v1.0.0-rc.1...v1.0.0-rc.2) (2021-02-18) + + +### Bug Fixes + +* add missing App export ([#640](https://github.com/vuejs/composition-api/issues/640)) ([eda6b22](https://github.com/vuejs/composition-api/commit/eda6b22)) + + +### Features + +* add defineAsyncComponent API ([#644](https://github.com/vuejs/composition-api/issues/644)) ([8409f48](https://github.com/vuejs/composition-api/commit/8409f48)) + + + + +# [1.0.0-rc.1](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.26...v1.0.0-rc.1) (2021-01-20) + + + + +# [1.0.0-beta.26](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.25...v1.0.0-beta.26) (2021-01-14) + + +### Bug Fixes + +* **types:** expose ExtractPropTypes type, close [#628](https://github.com/vuejs/composition-api/issues/628) ([903a0aa](https://github.com/vuejs/composition-api/commit/903a0aa)) +* change duplicate installation from error to warn, close [#631](https://github.com/vuejs/composition-api/issues/631) ([#632](https://github.com/vuejs/composition-api/issues/632)) ([5301d49](https://github.com/vuejs/composition-api/commit/5301d49)) +* Date infer string in props ([#627](https://github.com/vuejs/composition-api/issues/627)) ([b2acb2d](https://github.com/vuejs/composition-api/commit/b2acb2d)) + + + + +# [1.0.0-beta.25](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.24...v1.0.0-beta.25) (2021-01-08) + +## BREAKING CHANGES + +- `useCSSModule` renamed to `useCssModule` to align with Vue 3 (#626) +- `useCSSModule` is depreacted. + + + +# [1.0.0-beta.24](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.23...v1.0.0-beta.24) (2021-01-06) + + +### Bug Fixes + +* **getCurrentInstance:** emit event ([#624](https://github.com/vuejs/composition-api/issues/624)) ([cf5fa2b](https://github.com/vuejs/composition-api/commit/cf5fa2b)) + + + + +# [1.0.0-beta.23](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.22...v1.0.0-beta.23) (2021-01-05) + + +### Bug Fixes + +* useCSSModule to adapt the change of getCurrentInstance, close [#620](https://github.com/vuejs/composition-api/issues/620) ([#622](https://github.com/vuejs/composition-api/issues/622)) ([2ddead0](https://github.com/vuejs/composition-api/commit/2ddead0)) +* **README:** The correct option name is `emits` ([#617](https://github.com/vuejs/composition-api/issues/617)) ([4b2f1ab](https://github.com/vuejs/composition-api/commit/4b2f1ab)) + + + + +# [1.0.0-beta.22](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.21...v1.0.0-beta.22) (2020-12-19) + + +### Features + +* **getCurrentInstance:** Aligning with vue3 ([#520](https://github.com/vuejs/composition-api/issues/520)) ([1495a46](https://github.com/vuejs/composition-api/commit/1495a46)) + + +### BREAKING CHANGES + +* **getCurrentInstance:** The internal vm can be accessed with `getCurrentInstance().proxy` + +```js +const vm = getCurrentInstance() + +// becomes + +const vm = getCurrentInstance().proxy +``` + + + +# [1.0.0-beta.21](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.20...v1.0.0-beta.21) (2020-12-07) + + +### Bug Fixes + +* destructure `attrs` from context keep reactive, close [#264](https://github.com/vuejs/composition-api/issues/264) ([#594](https://github.com/vuejs/composition-api/issues/594)) ([4eecd66](https://github.com/vuejs/composition-api/commit/4eecd66)) + + +### Features + +* add type-level `readonly()` api ([#593](https://github.com/vuejs/composition-api/issues/593)) ([3b726d4](https://github.com/vuejs/composition-api/commit/3b726d4)) + + + + +# [1.0.0-beta.20](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.19...v1.0.0-beta.20) (2020-11-24) + + +### Bug Fixes + +* **types:** improve SetupContext types ([#595](https://github.com/vuejs/composition-api/issues/595)) ([49766bf](https://github.com/vuejs/composition-api/commit/49766bf)) + + +### Features + +* add warn ([#596](https://github.com/vuejs/composition-api/issues/596)) ([dd2cd6b](https://github.com/vuejs/composition-api/commit/dd2cd6b)) + + + + +# [1.0.0-beta.19](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.18...v1.0.0-beta.19) (2020-11-02) + + +### Bug Fixes + +* **types:** allow any custom options for defineComponent, fix [#579](https://github.com/vuejs/composition-api/issues/579) ([#584](https://github.com/vuejs/composition-api/issues/584)) ([7cdf1e5](https://github.com/vuejs/composition-api/commit/7cdf1e5)) +* **types:** attrs in SetupContext fix[#562](https://github.com/vuejs/composition-api/issues/562) ([#582](https://github.com/vuejs/composition-api/issues/582)) ([2d6de26](https://github.com/vuejs/composition-api/commit/2d6de26)) +* **types:** this type in data(), fix [#570](https://github.com/vuejs/composition-api/issues/570) ([#576](https://github.com/vuejs/composition-api/issues/576)) ([9a5b438](https://github.com/vuejs/composition-api/commit/9a5b438)) + + + + +# [1.0.0-beta.18](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.17...v1.0.0-beta.18) (2020-10-21) + + +### Bug Fixes + +* **type:** vue constructor should not require props with default values ([#567](https://github.com/vuejs/composition-api/issues/567)) ([964f9f3](https://github.com/vuejs/composition-api/commit/964f9f3)) +* better `vueDependency` importing, close [#564](https://github.com/vuejs/composition-api/issues/564) ([#572](https://github.com/vuejs/composition-api/issues/572)) ([555f20a](https://github.com/vuejs/composition-api/commit/555f20a)) + + +### Features + +* **reactivity:** add Vue.delete workaround ([#571](https://github.com/vuejs/composition-api/issues/571)) ([b41da83](https://github.com/vuejs/composition-api/commit/b41da83)) + + + + +# [1.0.0-beta.17](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.16...v1.0.0-beta.17) (2020-10-17) + + +### Bug Fixes + +* **types:** prop type infer, fix [#555](https://github.com/vuejs/composition-api/issues/555) ([#561](https://github.com/vuejs/composition-api/issues/561)) ([35f8fec](https://github.com/vuejs/composition-api/commit/35f8fec)) + + +### Code Refactoring + +* watch APIs default to trigger pre-flush ([#566](https://github.com/vuejs/composition-api/issues/566)) ([ded5ab7](https://github.com/vuejs/composition-api/commit/ded5ab7)), closes [#1706](https://github.com/vuejs/composition-api/issues/1706) + + +### BREAKING CHANGES + +* watch APIs now default to use `flush: 'pre'` instead of +`flush: 'post'`. + + - Check https://github.com/vuejs/vue-next/commit/49bb44756fda0a7019c69f2fa6b880d9e41125aa + + - This change affects `watch`, `watchEffect`, the `watch` component + option, and `this.$watch`. + + - As pointed out by @skirtles-code in + + + + +# [1.0.0-beta.16](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.15...v1.0.0-beta.16) (2020-10-10) + + +### Bug Fixes + +* **SSR:** value set for props, fix [#550](https://github.com/vuejs/composition-api/issues/550) ([#551](https://github.com/vuejs/composition-api/issues/551)) ([5b1b094](https://github.com/vuejs/composition-api/commit/5b1b094)) +* add emits options to defineComponent(fix [#553](https://github.com/vuejs/composition-api/issues/553)) ([#554](https://github.com/vuejs/composition-api/issues/554)) ([e44311f](https://github.com/vuejs/composition-api/commit/e44311f)) + + + + +# [1.0.0-beta.15](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.14...v1.0.0-beta.15) (2020-10-04) + + +### Bug Fixes + +* **reactive:** fix issue when using reactive `array` in the template ([#532](https://github.com/vuejs/composition-api/issues/532)) ([d99b91d](https://github.com/vuejs/composition-api/commit/d99b91d)) +* `reactive` in SSR ([#546](https://github.com/vuejs/composition-api/issues/546)) ([535c829](https://github.com/vuejs/composition-api/commit/535c829)) +* incorrect warning for `getRegisteredVueOrDefault`, resolve [#544](https://github.com/vuejs/composition-api/issues/544) ([3a1d992](https://github.com/vuejs/composition-api/commit/3a1d992)) +* reactive for props ([#547](https://github.com/vuejs/composition-api/issues/547)) ([4d39443](https://github.com/vuejs/composition-api/commit/4d39443)) +* **vue-test:** prevent warning when using multiple `localVue` ([#531](https://github.com/vuejs/composition-api/issues/531)) ([5484bb7](https://github.com/vuejs/composition-api/commit/5484bb7)) + + + + +# [1.0.0-beta.14](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.13...v1.0.0-beta.14) (2020-09-15) + + +### Bug Fixes + +* circular objects and making all Vue.observable objects isReactive ([#512](https://github.com/vuejs/composition-api/issues/512)) ([f204daa](https://github.com/vuejs/composition-api/commit/f204daa)) + + +### Features + +* **reactive:** allow usage of reactive before `Vue.use` ([#515](https://github.com/vuejs/composition-api/issues/515)) ([89fd11c](https://github.com/vuejs/composition-api/commit/89fd11c)) + + + + +# [1.0.0-beta.13](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.12...v1.0.0-beta.13) (2020-09-12) + + +### Bug Fixes + +* **sets:** check for window to avoid SSR errors ([#511](https://github.com/vuejs/composition-api/issues/511)) ([9ea7230](https://github.com/vuejs/composition-api/commit/9ea7230)) + + + + +# [1.0.0-beta.12](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.11...v1.0.0-beta.12) (2020-09-12) + + +### Features + +* allow plugin to be installed in localVue ([#497](https://github.com/vuejs/composition-api/issues/497)) ([07be9d7](https://github.com/vuejs/composition-api/commit/07be9d7)) +* improve reactive checks ([#502](https://github.com/vuejs/composition-api/issues/502)) ([255dc72](https://github.com/vuejs/composition-api/commit/255dc72)) +* **inject:** add `treatDefaultAsFactory` argument ([#503](https://github.com/vuejs/composition-api/issues/503)) ([78592bf](https://github.com/vuejs/composition-api/commit/78592bf)) + + + + +# [1.0.0-beta.11](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.9...v1.0.0-beta.11) (2020-08-22) + + +### Bug Fixes + +* **setup:** handle updates for directly return a reactive object ([#488](https://github.com/vuejs/composition-api/issues/488)) ([a7f2c25](https://github.com/vuejs/composition-api/commit/a7f2c25)), closes [#487](https://github.com/vuejs/composition-api/issues/487) +* **watch:** check if __ob__ has value before addSub ([#477](https://github.com/vuejs/composition-api/issues/477)) ([d8cd30d](https://github.com/vuejs/composition-api/commit/d8cd30d)) + + + + +# [1.0.0-beta.10](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.9...v1.0.0-beta.10) (2020-08-15) + + +### Bug Fixes + +* **watch:** check if __ob__ has value before addSub ([#477](https://github.com/vuejs/composition-api/issues/477)) ([d8cd30d](https://github.com/vuejs/composition-api/commit/d8cd30d)) + + + + +# [1.0.0-beta.9](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.8...v1.0.0-beta.9) (2020-08-11) + + +### Bug Fixes + +* **watch:** watch will trigger when added new keys using `set` ([#468](https://github.com/vuejs/composition-api/issues/468)) ([13bfed1](https://github.com/vuejs/composition-api/commit/13bfed1)) + + + + +# [1.0.0-beta.8](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.7...v1.0.0-beta.8) (2020-08-07) + + +### Bug Fixes + +* SSR renderComponent computed error, [#464](https://github.com/vuejs/composition-api/issues/464) ([#465](https://github.com/vuejs/composition-api/issues/465)) ([123e60e](https://github.com/vuejs/composition-api/commit/123e60e)) + + +# [1.0.0-beta.7](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2020-08-07) + +### BREAKING CHANGES + +* template auto ref unwrapping are now applied shallowly, +i.e. only at the root level. See https://github.com/vuejs/vue-next/pull/1682 for +more details. + + +### Features + +* `proxyRefs` method and `ShallowUnwrapRefs` type ([#456](https://github.com/vuejs/composition-api/issues/456)) ([149821a](https://github.com/vuejs/composition-api/commit/149821a)) + + +### Performance Improvements + +* more light-weight computed ([#452](https://github.com/vuejs/composition-api/issues/452)) ([95d87f1](https://github.com/vuejs/composition-api/commit/95d87f1)) + + + + +# [1.0.0-beta.6](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2020-07-22) + + +### Features + +* **shallowReadonly:** add shallowReadonly and set computed to be shallowReadonly ([#447](https://github.com/vuejs/composition-api/issues/447)) ([cfbbcec](https://github.com/vuejs/composition-api/commit/cfbbcec)) + + + + +# [1.0.0-beta.5](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2020-07-20) + + + + +# [1.0.0-beta.4](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2020-07-18) + + +### Bug Fixes + +* **IE11:** replace `startsWith` to be IE11 compatible ([#442](https://github.com/vuejs/composition-api/issues/442)) ([b31c74a](https://github.com/vuejs/composition-api/commit/b31c74a)) +* **type:** fix tying issues in [#428](https://github.com/vuejs/composition-api/issues/428) ([#444](https://github.com/vuejs/composition-api/issues/444)) ([98c7041](https://github.com/vuejs/composition-api/commit/98c7041)) + + +### Features + +* ie11 isReactive fix ([#441](https://github.com/vuejs/composition-api/issues/441)) ([e8ea208](https://github.com/vuejs/composition-api/commit/e8ea208)) + + + + +# [1.0.0-beta.3](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2020-07-09) + + +### Bug Fixes + +* unwrap warning, fix [#425](https://github.com/vuejs/composition-api/issues/425) ([#430](https://github.com/vuejs/composition-api/issues/430)) ([d5123ec](https://github.com/vuejs/composition-api/commit/d5123ec)) + + + + +# [1.0.0-beta.2](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2020-07-05) + + +### Bug Fixes + +* prevent multiple plugins get installed ([#427](https://github.com/vuejs/composition-api/issues/427)) ([94d4d87](https://github.com/vuejs/composition-api/commit/94d4d87)) +* remove "browser" field in package.json ([#424](https://github.com/vuejs/composition-api/issues/424)) ([4ebeda4](https://github.com/vuejs/composition-api/commit/4ebeda4)) +* toRaw typo ([#429](https://github.com/vuejs/composition-api/issues/429)) ([9468f72](https://github.com/vuejs/composition-api/commit/9468f72)) + + +### Features + +* add customRef ([#423](https://github.com/vuejs/composition-api/issues/423)) ([a8686bb](https://github.com/vuejs/composition-api/commit/a8686bb)) +* add useCSSModule api ([#420](https://github.com/vuejs/composition-api/issues/420)) ([1ceac1d](https://github.com/vuejs/composition-api/commit/1ceac1d)) + + + + +# [1.0.0-beta.1](https://github.com/vuejs/composition-api/compare/v0.6.7...v1.0.0-beta.1) (2020-06-30) + +## BREAKING CHANGES +* change umd exported name to `VueCompositionAPI` ([#399](https://github.com/vuejs/composition-api/issues/399)) +* rename `createElement` to `h` ([#400](https://github.com/vuejs/composition-api/issues/400)) +* drop `createComponent` ([#389](https://github.com/vuejs/composition-api/issues/389)) +* match dist file naming with vue-next ([#413](https://github.com/vuejs/composition-api/issues/413)) + +### Bug Fixes + +* **unwrapRef:** copy __ob__ and make toRaw work in props ([#409](https://github.com/vuejs/composition-api/issues/409)) ([5f23886](https://github.com/vuejs/composition-api/commit/5f23886)), closes [#392](https://github.com/vuejs/composition-api/issues/392) +* nextTick await ([#414](https://github.com/vuejs/composition-api/issues/414)) ([85ffede](https://github.com/vuejs/composition-api/commit/85ffede)) +* **type:** accept undefined return for setup() ([#417](https://github.com/vuejs/composition-api/issues/417)) ([64b16ff](https://github.com/vuejs/composition-api/commit/64b16ff)) + + +### Features + +* add createApp ([#415](https://github.com/vuejs/composition-api/issues/415)) ([391a0d9](https://github.com/vuejs/composition-api/commit/391a0d9)) +* Vue version warning in dev mode ([#408](https://github.com/vuejs/composition-api/issues/408)) ([0840aa8](https://github.com/vuejs/composition-api/commit/0840aa8)) + + + + +## [0.6.7](https://github.com/vuejs/composition-api/compare/v0.6.6...v0.6.7) (2020-06-24) + + +### Bug Fixes + +* **toRefs:** do not warn when toRefs is called in a prop value ([#405](https://github.com/vuejs/composition-api/issues/405)) ([048b6d3](https://github.com/vuejs/composition-api/commit/048b6d3)) +* **type:** improve defineComponent type for option apis ([#406](https://github.com/vuejs/composition-api/issues/406)) ([1c64108](https://github.com/vuejs/composition-api/commit/1c64108)) + + +### Features + +* auto install when using CDN ([#403](https://github.com/vuejs/composition-api/issues/403)) ([77ba15b](https://github.com/vuejs/composition-api/commit/77ba15b)) +* export nextTick ([#401](https://github.com/vuejs/composition-api/issues/401)) ([d70c904](https://github.com/vuejs/composition-api/commit/d70c904)) + + + + +## [0.6.6](https://github.com/vuejs/composition-api/compare/v0.6.5...v0.6.6) (2020-06-21) + + +### Reverts + +* [#387](https://github.com/vuejs/composition-api/issues/387) ([#395](https://github.com/vuejs/composition-api/issues/395)) ([b9fab71](https://github.com/vuejs/composition-api/commit/b9fab71)) + + + + +## [0.6.5](https://github.com/vuejs/composition-api/compare/v0.6.4...v0.6.5) (2020-06-19) + + +### Bug Fixes + +* **watchEffect:** prevent recursive calls when using `flush:sync` ([#389](https://github.com/vuejs/composition-api/issues/389)) ([f7f1e77](https://github.com/vuejs/composition-api/commit/f7f1e77)) +* not unwrapping `markRaw` objects ([#386](https://github.com/vuejs/composition-api/issues/386)) ([575d100](https://github.com/vuejs/composition-api/commit/575d100)) +* unwrap refs returned by `data` ([#387](https://github.com/vuejs/composition-api/issues/387)) ([1f07075](https://github.com/vuejs/composition-api/commit/1f07075)) + + + + +## [0.6.4](https://github.com/vuejs/composition-api/compare/v0.6.3...v0.6.4) (2020-06-16) + + +### Bug Fixes + +* **setup:** call stack exceeded when returning circular dependency ([#380](https://github.com/vuejs/composition-api/issues/380)) ([66f58ba](https://github.com/vuejs/composition-api/commit/66f58ba)) +* **setup:** Vue.extend(Comp).extend({}) - vue-test-utils ([#383](https://github.com/vuejs/composition-api/issues/383)) ([ce932bf](https://github.com/vuejs/composition-api/commit/ce932bf)) + + + + +## [0.6.3](https://github.com/vuejs/composition-api/compare/v0.6.2...v0.6.3) (2020-06-12) + + +### Bug Fixes + +* unwrapRefProxy native objects handling ([#376](https://github.com/vuejs/composition-api/issues/376)) ([8322fc7](https://github.com/vuejs/composition-api/commit/8322fc7)), closes [#375](https://github.com/vuejs/composition-api/issues/375) + + + + +## [0.6.2](https://github.com/vuejs/composition-api/compare/v0.6.1...v0.6.2) (2020-06-11) + + +### Bug Fixes + +* **reactivity:** unwrap nested refs on the template ([#361](https://github.com/vuejs/composition-api/issues/361)) ([1fd48f5](https://github.com/vuejs/composition-api/commit/1fd48f5)) +* defineComponent() with array props ([#364](https://github.com/vuejs/composition-api/issues/364)) ([d7048d4](https://github.com/vuejs/composition-api/commit/d7048d4)) +* **setup:** Allow retuning frozen objects on the setup ([#366](https://github.com/vuejs/composition-api/issues/366)) ([bca3a69](https://github.com/vuejs/composition-api/commit/bca3a69)) +* mark props as reactive ([#359](https://github.com/vuejs/composition-api/issues/359)) ([bc78428](https://github.com/vuejs/composition-api/commit/bc78428)) + +# 0.6.1 + +## Fix + +- `__DEV__` is not defined, #355, @yoyo930021 + +# 0.6.0 + +Great thanks to @pikax for #311, making most of the APIs better aligned with the latest vue-next. + +## BREAKING CHANGE + +- The `lazy` option of `watch` has been replaced by the opposite `immediate` option, which defaults to false. (It's ignored when using the effect signature). [more details](https://github.com/vuejs/vue-next/blob/master/CHANGELOG.md#breaking-changes-12) (#266) +- Rename `nonReactive` to `markRaw` +- `watchEffect` now follows the same behaviour as v3 (triggers immediately). +- `UnwrapRef` types from `vue-next` this can cause some incompatibilities. + +## Bug Fixes + +- Added missing reactivity API from vue-next, #311, @pikax +- Fix return type of `toRefs`, #315 +- Fix incorrect ref typing, #344, @antfu +- Binding context vm when using function without parentheses, #148, @pikax +- **computed**: destroy helper vm of computed to prevent memleak, #277, @LinusBorg +- Remove the surplus Function type from PropType, #352, @pikax + +## Features + +- Added `unref`(#309), `isReactive` (#327), `toRef` (#313), `UnwrapRef` (#247) +- Added `shallowReactive`, `shallowRef` +- Added `toRaw` +- `getCurrentInstance` available on the lifecycle hooks (`onMounted`, etc) +- `getCurrentInstance` returns `undefined` when called outside setup instead of throwing exception + +## Types + +- Align reactivity types with `vue-next` + + +# 0.5.0 + +- New: `watchEffect` function, lingin up with the latest version of the RFC ([RFC docs](https://vue-composition-api-rfc.netlify.com/api.html#watcheffect)) (#275) +- Fix: `setup` from a mixin should called before the component's own (#276) +- Fix(types): Fix corner case in `UnWrapRef` internal type (#261) +- types: Add `Element` to bailout types for unwrapping (#278) + +# 0.4.0 + +- **Refactor: rename `createComponent` to `defineComponent`** (the `createComponent` function is still there but deprecated) [#230](https://github.com/vuejs/composition-api/issues/230) +- Fix: correct the symbol check; fixes the compatibility issue in iOS 9 [#218](https://github.com/vuejs/composition-api/pull/218) +- Fix: avoid accessing undeclared instance fields on type-level; fixes Vetur template type checking; fixes vue-router type compatibility [#189](https://github.com/vuejs/composition-api/pull/189) +- Fix: `onUnmounted` should not be run on `deactivated` [#217](https://github.com/vuejs/composition-api/pull/217) + +# 0.3.4 + +- Fixed `reactive` setter not working on the server. +- New `isServer` setup context property. + +# 0.3.3 + +- Fixed make `__ob__` unenumerable [#149](https://github.com/vuejs/composition-api/issues/149). +- Fixed computed type +- Expose `getCurrentInstance` for advanced usage in Vue plugins. +- New `onServerPrefetch` lifecycle hook and new `ssrContext` setup context property [#198](https://github.com/vuejs/composition-api/issues/198). + +# 0.3.2 + +- Improve TypeScript type infer for `props` option [#106](https://github.com/vuejs/composition-api/issues/106). +- Fix return type of `createComponent` not being compatible with `vue-router` [#130](https://github.com/vuejs/composition-api/issues/130). +- Expose `listeners` on `SetupContext` [#132](https://github.com/vuejs/composition-api/issues/132). + +# 0.3.1 + +- Fix cleaup callback not running when watcher stops [#113](https://github.com/vuejs/composition-api/issues/113). +- Fix watcher callback not flushing at right timing [#120](https://github.com/vuejs/composition-api/issues/120). + +# 0.3.0 + +- Improve TypeScript type definitions. +- Fix `context.slots` not being available before render [#84](https://github.com/vuejs/composition-api/issues/84). + +## Changed + +The `render` function returned from `setup` no longer receives any parameters. + +### Previous + +```js +export default { + setup() { + return props => h('div', prop.msg); + }, +}; +``` + +### Now + +```js +export default { + setup(props) { + return () => h('div', prop.msg); + }, +}; +``` + +# 0.2.1 + +- Declare your expected prop types directly in TypeScript: + + ```js + import { createComponent, createElement as h } from '@vue/composition-api'; + + interface Props { + msg: string; + } + + const MyComponent = + createComponent < + Props > + { + props: { + msg: {}, // required by vue 2 runtime + }, + setup(props) { + return () => h('div', props.msg); + }, + }; + ``` + +- Declare ref type in TypeScript: + ```js + const dateRef = ref < Date > new Date(); + ``` +- Fix `createComponent` not working with `import()` [#81](https://github.com/vuejs/composition-api/issues/81). +- Fix `inject` type declaration [#83](https://github.com/vuejs/composition-api/issues/83). + +# 0.2.0 + +## Fixed + +- `computed` property is called immediately in `reactive()` [#79](https://github.com/vuejs/composition-api/issues/79). + +## Changed + +- rename `onBeforeDestroy()` to `onBeforeUnmount()` [lifecycle-hooks](https://vue-composition-api-rfc.netlify.com/api.html#lifecycle-hooks). +- Remove `onCreated()` [lifecycle-hooks](https://vue-composition-api-rfc.netlify.com/api.html#lifecycle-hooks). +- Remove `onDestroyed()` [lifecycle-hooks](https://vue-composition-api-rfc.netlify.com/api.html#lifecycle-hooks). + +# 0.1.0 + +**The package has been renamed to `@vue/composition-api` to be consistent with RFC.** + +The `@vue/composition-api` reflects the [Composition API](https://vue-composition-api-rfc.netlify.com/) RFC. + +# 2.2.0 + +- Improve typescript support. +- Export `createElement`. +- Export `SetupContext`. +- Support returning a render function from `setup`. +- Allow string keys in `provide`/`inject`. + +# 2.1.2 + +- Remove auto-unwrapping for Array ([#53](https://github.com/vuejs/composition-api/issues/53)). + +# 2.1.1 + +- Export `set()` function. Using exported `set` whenever you need to use [Vue.set](https://vuejs.org/v2/api/#Vue-set) or [vm.\$set](https://vuejs.org/v2/api/#vm-set). The custom `set` ensures that auto-unwrapping works for the new property. +- Add a new signature of `provide`: `provide(key, value)`. +- Fix multiple `provide` invoking per component. +- Fix order of `setup` invoking. +- `onErrorCaptured` not triggered ([#25](https://github.com/vuejs/composition-api/issues/25)). +- Fix `this` losing in nested setup call ([#38](https://github.com/vuejs/composition-api/issues/38)). +- Fix some edge cases of unwarpping. +- Change `context.slots`'s value. It now proxies to `$scopeSlots` instead of `$slots`. + # 2.0.6 + ## Fixed -* watch callback is called repeatedly with multi-sources + +- watch callback is called repeatedly with multi-sources ## Improved -* reduce `watch()` memory overhead + +- reduce `watch()` memory overhead # 2.0.0 + Implement the [newest version of RFC](https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/0000-function-api.md) ## Breaking Changes + `this` is not available inside `setup()`. See [setup](https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/0000-function-api.md#the-setup-function) for details. ## Features + Complex Prop Types: ```ts -import { createComponent, PropType } from 'vue' +import { createComponent, PropType } from 'vue'; createComponent({ props: { - options: (null as any) as PropType<{ msg: string }> + options: (null as any) as PropType<{ msg: string }>, }, setup(props) { - props.options // { msg: string } | undefined - } -}) + props.options; // { msg: string } | undefined + }, +}); ``` # 1.x - Implement the [init version of RFC](https://github.com/vuejs/rfcs/blob/903f429696524d8f93b4976d5b09dfb3632e89ef/active-rfcs/0000-function-api.md) + +Implement the [init version of RFC](https://github.com/vuejs/rfcs/blob/903f429696524d8f93b4976d5b09dfb3632e89ef/active-rfcs/0000-function-api.md) diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..1c4bdb5d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019-present, liximomo(X.L) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 648dfcb4..91880818 100644 --- a/README.md +++ b/README.md @@ -1,349 +1,558 @@ -# Vue Function API +# @vue/composition-api -> [Function-based Component API RFC](https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/0000-function-api.md) +Vue 2 plugin for **Composition API** -Future-Oriented Programming, `vue-function-api` provides function api from `Vue3.x` to `Vue2.x` for developing next-generation Vue applications. +[![npm](https://img.shields.io/npm/v/@vue/composition-api)](https://www.npmjs.com/package/@vue/composition-api) +[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/vuejs/composition-api/test.yml)](https://github.com/vuejs/composition-api/actions?query=workflow%3A%22Build+%26+Test%22) +[![Minzipped size](https://badgen.net/bundlephobia/minzip/@vue/composition-api)](https://bundlephobia.com/result?p=@vue/composition-api) -[**中文文档**](./README.zh-CN.md) +English | [中文](./README.zh-CN.md) ・ [**Composition API Docs**](https://v3.vuejs.org/guide/composition-api-introduction.html) ---- -# Navigation +> ⚠️ With the release of [Vue 2.7](https://blog.vuejs.org/posts/vue-2-7-naruto.html), which has Composition API built-in, **you no longer need this plugin**. Thereby this plugin has entered maintenance mode and will only support Vue 2.6 or earlier. This project will reach End of Life by the end of 2022. -- [Changelog](https://github.com/vuejs/vue-function-api/blob/master/CHANGELOG.md) -- [Installation](#Installation) -- [Usage](#Usage) -- [Example](#Example) - - [Todo App Compare with Vue2 API](https://codesandbox.io/s/todo-example-6d7ep) - - [CodePen Live Demo](https://codepen.io/liximomo/pen/dBOvgg) - - [Single-File Component](#single-file-Component) -- [API](#API) - - [setup](#setup) - - [value](#value) - - [state](#state) - - [computed](#computed) - - [watch](#watch) - - [lifecycle](#lifecycle) - - [provide, inject](#provide-inject) -- [Misc](#Misc) +## Installation -# Installation +### NPM -**npm** ```bash -npm install vue-function-api --save +npm install @vue/composition-api +# or +yarn add @vue/composition-api ``` -**yarn** -```bash -yarn add vue-function-api +You must install `@vue/composition-api` as a plugin via `Vue.use()` before you can use the [Composition API](https://composition-api.vuejs.org/) to compose your component. + +```js +import Vue from 'vue' +import VueCompositionAPI from '@vue/composition-api' + +Vue.use(VueCompositionAPI) ``` -**CDN** +```js +// use the APIs +import { ref, reactive } from '@vue/composition-api' +``` + +> :bulb: When you migrate to Vue 3, just replacing `@vue/composition-api` to `vue` and your code should just work. + +### CDN +Include `@vue/composition-api` after Vue and it will install itself automatically. + + ```html - + + ``` -By using the global variable `window.vueFunctionApi` + + +`@vue/composition-api` will be exposed to global variable `window.VueCompositionAPI`. + +```ts +const { ref, reactive } = VueCompositionAPI +``` + +## TypeScript Support + +> TypeScript version **>4.2** is required + +To let TypeScript properly infer types inside Vue component options, you need to define components with `defineComponent` + +```ts +import { defineComponent } from '@vue/composition-api' + +export default defineComponent({ + // type inference enabled +}) +``` + +### JSX/TSX +JSX is now officially supported on [vuejs/jsx](https://github.com/vuejs/jsx). You can enable it by following [this document](https://github.com/vuejs/jsx/tree/dev/packages/babel-preset-jsx#usage). A community maintained version can be found at [babel-preset-vca-jsx](https://github.com/luwanquan/babel-preset-vca-jsx) by [@luwanquan](https://github.com/luwanquan). -# Usage +To support TSX, create a declaration file with the following content in your project. -You must explicitly install `vue-function-api` via `Vue.use()`: +```ts +// file: shim-tsx.d.ts +import Vue, { VNode } from 'vue'; +import { ComponentRenderProxy } from '@vue/composition-api'; + +declare global { + namespace JSX { + interface Element extends VNode {} + interface ElementClass extends ComponentRenderProxy {} + interface ElementAttributesProperty { + $props: any; // specify the property name to use + } + interface IntrinsicElements { + [elem: string]: any; + } + } +} +``` + +## SSR + +Even if there is no definitive Vue 3 API for SSR yet, this plugin implements the `onServerPrefetch` lifecycle hook that allows you to use the `serverPrefetch` hook found in the classic API. ```js -import Vue from 'vue' -import { plugin } from 'vue-function-api' +import { onServerPrefetch } from '@vue/composition-api' + +export default { + setup(props, { ssrContext }) { + const result = ref() + + onServerPrefetch(async () => { + result.value = await callApi(ssrContext.someId) + }) + + return { + result, + } + } +} +``` + +## Browser Compatibility + +`@vue/composition-api` supports all modern browsers and IE11+. For lower versions IE you should install `WeakMap` polyfill (for example from `core-js` package). -Vue.use(plugin) +## Limitations + +> :white_check_mark: Support     :x: Not Supported + +### `Ref` Unwrap + +
+ +❌ Should NOT use ref in a plain object when working with Array + + +```js +const a = { + count: ref(0), +} +const b = reactive({ + list: [a], // `a.count` will not unwrap!! +}) + +// no unwrap for `count`, `.value` is required +b.list[0].count.value === 0 // true ``` -After installing the plugin you can use the new [function API](#API) to compose your component. +```js +const b = reactive({ + list: [ + { + count: ref(0), // no unwrap!! + }, + ], +}) + +// no unwrap for `count`, `.value` is required +b.list[0].count.value === 0 // true +``` + +
+ +
+ +✅ Should always use ref in a reactive when working with Array + + +```js +const a = reactive({ + list: [ + reactive({ + count: ref(0), + }), + ] +}) +// unwrapped +a.list[0].count === 0 // true + +a.list.push( + reactive({ + count: ref(1), + }) +) +// unwrapped +a.list[1].count === 1 // true +``` -# Example +
-## [Todo App Compare with Vue2 API](https://codesandbox.io/s/todo-example-6d7ep) +### Template Refs -## [CodePen Live Demo](https://codepen.io/liximomo/pen/dBOvgg) +
+ +✅ String ref && return it from setup() + -## Single-File Component -``` html +```html ``` -# API +
-## setup +
+ +✅ String ref && return it from setup() && Render Function / JSX + -▸ **setup**(props: *`Props`*, context: *[`Context`](#Context)*): `Object|undefined` - -A new component option, `setup()` is introduced. As the name suggests, this is the place where we use the function-based APIs to setup the logic of our component. `setup()` is called when an instance of the component is created, after props resolution. The function receives the resolved props as its first argument. +```jsx +export default { + setup() { + const root = ref(null) -The second argument provides a `context` object which exposes a number of properties that were previously exposed on this in 2.x APIs. + onMounted(() => { + // the DOM element will be assigned to the ref after initial render + console.log(root.value) //
+ }) -```js -const MyComponent = { - props: { - name: String + return { + root, + } + }, + render() { + // with JSX + return () =>
}, - setup(props, context) { - console.log(props.name); - // context.attrs - // context.slots - // context.refs - // context.emit - // context.parent - // context.root - } } ``` -## value +
-▸ **value**(value: *`any`*): [`Wrapper`][Wrapper] +
+ +❌ Function ref + -Calling `value()` returns a **value wrapper** object that contains a single reactive property: `.value`. +```html + -Example: + +``` -const MyComponent = { - setup(props) { - const msg = value('hello') - const appendName = () => { - msg.value = `hello ${props.name}` - } - return { - msg, - appendName - } +
+ +
+ +❌ Render Function / JSX in setup() + + +```jsx +export default { + setup() { + const root = ref(null) + + return () => + h('div', { + ref: root, + }) + + // with JSX + return () =>
}, - template: `
{{ msg }}
` } ``` -## state -▸ **state**(value: *`any`*) +
-Equivalent with [`Vue.observable`](https://vuejs.org/v2/api/index.html#Vue-observable). +
+ +⚠️ $refs accessing workaround + -Example: +
-```js -import { state } from 'vue-function-api' +> :warning: **Warning**: The `SetupContext.refs` won't exist in `Vue 3.0`. `@vue/composition-api` provide it as a workaround here. -const object = state({ - count: 0 -}) +If you really want to use template refs in this case, you can access `vm.$refs` via `SetupContext.refs` + +```jsx +export default { + setup(initProps, setupContext) { + const refs = setupContext.refs + onMounted(() => { + // the DOM element will be assigned to the ref after initial render + console.log(refs.root) //
+ }) + + return () => + h('div', { + ref: 'root', + }) -object.count++ + // with JSX + return () =>
+ }, +} ``` -## computed -▸ **computed**(getter: *`Function`*, setter?: *`Function`*): [`Wrapper`][Wrapper] +
-Equivalent with computed property from `vue 2.x`. +### Reactive -Example: +
+ +⚠️ reactive() mutates the original object + -```js -import { value, computed } from 'vue-function-api' +`reactive` uses `Vue.observable` underneath which will ***mutate*** the original object. -const count = value(0) -const countPlusOne = computed(() => count.value + 1) +> :bulb: In Vue 3, it will return a new proxy object. -console.log(countPlusOne.value) // 1 +
-count.value++ -console.log(countPlusOne.value) // 2 -``` +
+ +⚠️ set and del workaround for adding and deleting reactive properties + -## watch -▸ **watch**(source: *`Wrapper | () => any`*, callback: *`(newVal, oldVal)`*, options?: *[`WatchOption`](#WatchOption)*): `Function` +> ⚠️ Warning: `set` and `del` do NOT exist in Vue 3. We provide them as a workaround here, due to the limitation of [Vue 2.x reactivity system](https://vuejs.org/v2/guide/reactivity.html#For-Objects). +> +> In Vue 2, you will need to call `set` to track new keys on an `object`(similar to `Vue.set` but for `reactive objects` created by the Composition API). In Vue 3, you can just assign them like normal objects. +> +> Similarly, in Vue 2 you will need to call `del` to [ensure a key deletion triggers view updates](https://vuejs.org/v2/api/#Vue-delete) in reactive objects (similar to `Vue.delete` but for `reactive objects` created by the Composition API). In Vue 3 you can just delete them by calling `delete foo.bar`. -▸ **watch**(source: *`Array any>`*, callback: *`([newVal1, newVal2, ... newValN], [oldVal1, oldVal2, ... oldValN])`*, options?: *[`WatchOption`](#WatchOption)*): `Function` +```ts +import { reactive, set, del } from '@vue/composition-api' -The `watch` API provides a way to perform side effect based on reactive state changes. +const a = reactive({ + foo: 1 +}) -**Returns** a `Function` to stop the `watch`. +// add new reactive key +set(a, 'bar', 1) -> [effect-cleanup](https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/0000-function-api.md#effect-cleanup) is NOT supported currently. +// remove a key and trigger reactivity +del(a, 'bar') +``` -### WatchOption -| Name | Type | Default | Description | -| ------ | ------ | ------ | ------ | -| lazy | `boolean` | `false` | The opposite of 2.x's `immediate` option | -| deep | `boolean` | `false` | Same as 2.x | -| flush | `"pre"` \| `"post"` \| `"sync"` | `"post"` | `"post"`: fire after renderer flush; `"pre"`: fire before renderer flush; `"sync"`: fire synchronously | +
+### Watch -Example: +
+ +❌ onTrack and onTrigger are not available in WatchOptions + ```js -watch( - // getter - () => count.value + 1, - // callback - (value, oldValue) => { - console.log('count + 1 is: ', value) - } -) -// -> count + 1 is: 1 - -count.value++ -// -> count + 1 is: 2 +watch(() => { + /* ... */ +}, { + immediate: true, + onTrack() {}, // not available + onTrigger() {}, // not available +}) ``` -Example (Multiple Sources): +
-```js -watch( - [valueA, () => valueB.value], - ([a, b], [prevA, prevB]) => { - console.log(`a is: ${a}`) - console.log(`b is: ${b}`) - } -) +### `createApp` + +
+ +⚠️ createApp() is global + + +In Vue 3, `createApp()` is introduced to provide context(plugin, components, etc.) isolation between app instances. Due the design of Vue 2, in this plugin, we provide `createApp()` as a forward compatible API which is just an alias of the global. + +```ts +const app1 = createApp(RootComponent1) +app1.component('Foo', Foo) // equivalent to Vue.component('Foo', Foo) +app1.use(VueRouter) // equivalent to Vue.use(VueRouter) + +const app2 = createApp(RootComponent2) +app2.component('Bar', Bar) // equivalent to Vue.component('Bar', Bar) ``` -## lifecycle -▸ **onCreated**(cb: *`Function`*) +
-▸ **onBeforeMount**(cb: *`Function`*) +### `createElement` / `h` -▸ **onMounted**(cb: *`Function`*) +
+ +⚠️ createElement / h workaround + -▸ **onXXX**(cb: *`Function`*) +
-All current lifecycle hooks will have an equivalent `onXXX` function that can be used inside `setup()` +`createElement` / `h` in Vue 2 is only accessable in `render()` function. To use it outside of `render()`, you can explicitly bind a component instance to it. -Example: +> :warning: **Warning**: This ability is provided as a workaround Vue 2, it's not part of the Vue 3 API. -```js -import { onMounted, onUpdated, onUnmounted } from 'vue-function-api' +```jsx +import { h as _h } from '@vue/composition-api' -const MyComponent = { +export default { setup() { - onMounted(() => { - console.log('mounted!') - }) - onUpdated(() => { - console.log('updated!') - }) - onUnmounted(() => { - console.log('unmounted!') - }) - } + const vm = getCurrentInstance() + const h = _h.bind(vm) + + return () => + h('div', { + ref: 'root', + }) + }, } ``` -## provide, inject -▸ **provide**(value: *`Object`*) +
-▸ **inject**(key: *`string` | `symbol`*) -Equivalent with `provide` and `inject` from `2.x` +### `shallowReadonly` -Example: +
+ +⚠️ shallowReadonly() will create a new object and with the same root properties, new properties added will not be readonly or reactive. + -```js -import { provide, inject } from 'vue-function-api' +> :bulb: In Vue 3, it will return a new proxy object. -const CountSymbol = Symbol() +
-const Ancestor = { - setup() { - // providing a value can make it reactive - const count = value(0) - provide({ - [CountSymbol]: count - }) +### `readonly` + +
+ +⚠️ readonly() provides only type-level readonly check. + + +`readonly()` is provided as API alignment with Vue 3 on type-level only. Use isReadonly() on it or it's properties can not be guaranteed. + +
+ +### `props` + +
+ +⚠️ toRefs(props.foo) will incorrectly warn when accessing nested levels of props.
+    ⚠️ isReactive(props.foo) will return false. +
+ +```ts +defineComponent({ + setup(props) { + const { bar } = toRefs(props.foo) // it will `warn` + + // use this instead + const { foo } = toRefs(props) + const a = foo.value.bar } +}) +``` + +
+ +### `computed().effect` + +
+ +⚠️ computed() has a property effect set to true instead of a ReactiveEffect. + + +Due to the difference in implementation, there is no such concept as a `ReactiveEffect` in `@vue/composition-api`. Therefore, `effect` is merely `true` to enable differentiating computed from refs: + +```ts +function isComputed(o: ComputedRef | unknown): o is ComputedRef +function isComputed(o: any): o is ComputedRef { + return !!(isRef(o) && o.effect) } +``` -const Descendent = { - setup() { - const count = inject(CountSymbol) +
+ +### Missing APIs + +The following APIs introduced in Vue 3 are not available in this plugin. + +- `onRenderTracked` +- `onRenderTriggered` +- `isProxy` + +### Reactive APIs in `data()` + +
+ +❌ Passing ref, reactive or other reactive apis to data() would not work. + + +```jsx +export default { + data() { return { - count + // will result { a: { value: 1 } } in template + a: ref(1), } - } + }, } ``` -## Context -The `context` object exposes a number of properties that were previously exposed on this in 2.x APIs: - -```js -const MyComponent = { - setup(props, context) { - context.attrs - context.slots - context.refs - context.emit - context.parent - context.root +
+ +### `emits` Options + +
+ +❌ emits option is provided in type-level only, in order to align with Vue 3's type interface. Does NOT have actual effects on the code. + + +```ts +defineComponent({ + emits: { + // has no effects + submit: (eventOption) => { + if (...) { + return true + } else { + console.warn('Invalid submit event payload!') + return false + } + } } -} +}) ``` -Full properties list: - -* parent -* root -* refs -* slots -* attrs -* emit - -# Misc +
-- `vue-function-api` will keep updated with `Vue3.x` API. When `3.0` released, you can replace this library seamlessly. -- `vue-function-api` only relies on `Vue2.x` itself. Wheather `Vue3.x` is released or not, it's not affect you using this library. -- Due the the limitation of `Vue2.x`'s public API. `vue-function-api` inevitably introduce some extract workload. It doesn't concern you if you are now working on extreme environment. +### Performance Impact +Due the limitation of Vue2's public API. `@vue/composition-api` inevitably introduces some performance overhead. Note that in most scenarios, this shouldn't be the source of performance issues. -[wrapper]: https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/0000-function-api.md#why-do-we-need-value-wrappers +You can check the [benchmark results](https://antfu.github.io/vue-composition-api-benchmark-results/) for more details. diff --git a/README.zh-CN.md b/README.zh-CN.md index f5552f65..b48d1f53 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,410 +1,568 @@ -# Vue Function API +# @vue/composition-api -> [通过基于函数的 API 来复用组件逻辑](https://zhuanlan.zhihu.com/p/68477600) +用于提供 **组合式 API** 的 Vue 2 插件. -面向未来编程(Future-Oriented Programming),`vue-function-api` 提供 Vue3 中的组件逻辑复用机制帮助开发者开发下一代 vue 应用程序,允许开发者利用 Vue3 的响应性 API 建设未来 Vue 生态。 +[![npm](https://img.shields.io/npm/v/@vue/composition-api)](https://www.npmjs.com/package/@vue/composition-api) +[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/vuejs/composition-api/test.yml)](https://github.com/vuejs/composition-api/actions?query=workflow%3A%22Build+%26+Test%22) -[**English Version**](./README.md) +[English](./README.md) | 中文 ・ [**组合式 API 文档**](https://v3.cn.vuejs.org/guide/composition-api-introduction.html) ---- -# 导航 +> ⚠️ 随着 [Vue 2.7](https://blog.vuejs.org/posts/vue-2-7-naruto.html)的发布,它内置了Composition API,**你不再需要这个插件了**。因此,这个插件已经进入维护模式,将只支持Vue 2.6 或更早的版本。本项目将在 2022 年底达到生命终点(EOL)。 -- [Changelog](https://github.com/vuejs/vue-function-api/blob/master/CHANGELOG.md) -- [安装](#安装) -- [使用](#使用) -- [示例](#示例) - - [Todo App Compare with Vue2 API](https://codesandbox.io/s/todo-example-6d7ep) - - [CodePen Live Demo](https://codepen.io/liximomo/pen/dBOvgg) - - [Single-File Component](#single-file-Component) -- [API](#API) - - [setup](#setup) - - [value](#value) - - [state](#state) - - [computed](#computed) - - [watch](#watch) - - [lifecycle](#lifecycle) - - [provide, inject](#provide-inject) -- [其他](#其他) +## 安装 -# 安装 +### NPM -**npm** ```bash -npm install vue-function-api --save +npm install @vue/composition-api +# or +yarn add @vue/composition-api ``` -**yarn** -```bash -yarn add vue-function-api +在使用 `@vue/composition-api` 前,必须先通过 `Vue.use()` 进行安装。之后才可使用新的 [**组合式 API**](https://composition-api.vuejs.org/zh) 进行组件开发。 + +```js +import Vue from 'vue' +import VueCompositionAPI from '@vue/composition-api' + +Vue.use(VueCompositionAPI) ``` -**CDN** +```js +// 使用 API +import { ref, reactive } from '@vue/composition-api' +``` + +> :bulb: 当迁移到 Vue 3 时,只需简单的将 `@vue/composition-api` 替换成 `vue` 即可。你现有的代码几乎无需进行额外的改动。 + +### CDN +在 Vue 之后引入 `@vue/composition-api` ,插件将会自动完成安装。 + + ```html - + + ``` -通过全局变量 `window.vueFunctionApi` 来使用。 + + +`@vue/composition-api` 将会暴露在全局变量 `window.VueCompositionAPI` 中。 + +```ts +const { ref, reactive } = VueCompositionAPI +``` + +## TypeScript 支持 + +> 本插件要求使用 TypeScript **4.2** 或以上版本 + +为了让 TypeScript 在 Vue 组件选项中正确地进行类型推导,我们必须使用 `defineComponent` 来定义组件: + +```ts +import { defineComponent } from '@vue/composition-api' + +export default defineComponent({ + // 类型推断启用 +}) +``` + +### JSX/TSX -**CodePen** +JSX 现已在 [vuejs/jsx](https://github.com/vuejs/jsx) 中官方支持。你可以根据[这篇文档](https://github.com/vuejs/jsx/tree/dev/packages/babel-preset-jsx#usage)开启支持。你也可以使用由 [@luwanquan](https://github.com/luwanquan) 维护的社区版本 [babel-preset-vca-jsx](https://github.com/luwanquan/babel-preset-vca-jsx)。 -[在线示例](https://codepen.io/liximomo/pen/dBOvgg),fork 后进行测试或 bug 反馈。 +对于 TSX 支持,请在你的项目中创建如下声明文件: -# 使用 -您必须显式地通过 `Vue.use()` 来安装 `vue-function-api`: +```ts +// file: shim-tsx.d.ts +import Vue, { VNode } from 'vue'; +import { ComponentRenderProxy } from '@vue/composition-api'; + +declare global { + namespace JSX { + interface Element extends VNode {} + interface ElementClass extends ComponentRenderProxy {} + interface ElementAttributesProperty { + $props: any; // specify the property name to use + } + interface IntrinsicElements { + [elem: string]: any; + } + } +} +``` + +## SSR + +尽管 Vue 3 暂时没有给出确定的 SSR 的 API,这个插件实现了 `onServerPrefetch` 生命周期钩子函数。这个钩子允许你使用传统 API 中的 `serverPrefetch` 函数。 ```js -import Vue from 'vue' -import { plugin } from 'vue-function-api' +import { onServerPrefetch } from '@vue/composition-api' -Vue.use(plugin) +export default { + setup(props, { ssrContext }) { + const result = ref() + + onServerPrefetch(async () => { + result.value = await callApi(ssrContext.someId) + }) + + return { + result, + } + }, +} ``` -安装插件后,您就可以使用新的[函数式 API](#API)来书写组件了。 +## 浏览器兼容性 -# 示例 +`@vue/composition-api` 支持所有现代浏览器以及IE11+。对于更低版本的IE浏览器你需要安装`WeakMap` polyfill (例如使用 `core-js`库)。 -## [Todo App Compare with Vue2 API](https://codesandbox.io/s/todo-example-6d7ep) +## 限制 -## [CodePen Live Demo](https://codepen.io/liximomo/pen/dBOvgg) +> :white_check_mark: 支持     :x: 不支持 -## Single-File Component -``` html +### `Ref` 自动展开 (unwrap) + +
+ +❌ 不要 在数组中使用含有 ref 的普通对象 + + +```js +const a = { + count: ref(0), +} +const b = reactive({ + list: [a], // `a.count` 不会自动展开!! +}) + +// `count` 不会自动展开, 须使用 `.value` +b.list[0].count.value === 0 // true +``` + +```js +const b = reactive({ + list: [ + { + count: ref(0), // 不会自动展开!! + }, + ], +}) + +// `count` 不会自动展开, 须使用 `.value` +b.list[0].count.value === 0 // true +``` + +
+ +
+ +✅ 在数组中,应该 总是将 ref 存放到 reactive 对象中 + + +```js +const a = reactive({ + count: ref(0), +}) +const b = reactive({ + list: [a], +}) +// 自动展开 +b.list[0].count === 0 // true + +b.list.push( + reactive({ + count: ref(1), + }) +) +// 自动展开 +b.list[1].count === 1 // true +``` + +
+ +### 模板 Refs + +
+ +✅ 在`模板`中使用字符串 ref && 从 setup() 返回 ref + + +```html ``` -# API +
-## setup +
+ +✅ 在render()中使用字符串 ref && 从 setup() 返回 ref + -▸ **setup**(props: *`Props`*, context: *[`Context`](#Context)*): `Object|undefined` - -组件现在接受一个新的 `setup` 选项,在这里我们利用函数 api 进行组件逻辑设置。 +```jsx +export default { + setup() { + const root = ref(null) -`setup()` 中不可以使用 `this` 访问当前组件实例, 我们可以通过 `setup` 的第二个参数 `context` 来访问 vue2.x API 中实例上的属性。 + onMounted(() => { + // 在初次渲染后 DOM 元素会被赋值给 ref + console.log(root.value) //
+ }) -```js -const MyComponent = { - props: { - name: String + return { + root, + } + }, + render() { + // 使用 JSX + return () =>
}, - setup(props, context) { - console.log(props.name); - // context.attrs - // context.slots - // context.refs - // context.emit - // context.parent - // context.root - } } ``` -## value +
+ +
+ +❌ 函数式 ref + + +```html + + + +``` -`value` 函数为组件声明一个响应式属性,我们只需要在 `setup` 函数中返回 `value` 的返回值即可。 +
-Example: +
+ +❌ 在 setup() 中返回的`渲染函数 / JSX` + -```js -import { value } from 'vue-function-api' +```jsx +export default { + setup() { + const root = ref(null) -const MyComponent = { - setup(props) { - const msg = value('hello') - const appendName = () => { - msg.value = `hello ${props.name}` - } - return { - msg, - appendName - } + return () => + h('div', { + ref: root, + }) + + // 使用 JSX + return () =>
}, - template: `
{{ msg }}
` } ``` -## state -▸ **state**(value: *`any`*) +
+ +
+ +⚠️ $refs 访问的变通方案 + -与 [`Vue.observable`](https://vuejs.org/v2/api/index.html#Vue-observable) 等价。 +> :warning: **警告**: `SetupContext.refs` 并非 `Vue 3.0` 的一部分, `@vue/composition-api` 将其暴露在 `SetupContext` 中只是临时提供一种变通方案。 -Example: +如果你依然选择在 `setup()` 中写 `render` 函数,那么你可以使用 `SetupContext.refs` 来访问模板引用,它等价于 Vue 2.x 中的 `this.$refs`: ```js -import { state } from 'vue-function-api' +export default { + setup(initProps, setupContext) { + const refs = setupContext.refs + onMounted(() => { + // 在初次渲染后 DOM 元素会被赋值给 ref + console.log(refs.root) //
+ }) -const object = state({ - count: 0 -}) + return () => + h('div', { + ref: 'root', + }) -object.count++ + // 使用 JSX + return () =>
+ }, +} ``` -## computed -▸ **computed**(getter: *`Function`*, setter?: *`Function`*): [`Wrapper`](#Wrapper) +如果项目使用了 TypeScript,你还需要扩展 `SetupContext` 类型: -与 vue 2.x computed property 行为一致。 +```ts +import Vue from 'vue' -Example: +declare module '@vue/composition-api' { + interface SetupContext { + readonly refs: { [key: string]: Vue | Element | Vue[] | Element[] } + } +} +``` -```js -import { value, computed } from 'vue-function-api' +
-const count = value(0) -const countPlusOne = computed(() => count.value + 1) +### Reactive -console.log(countPlusOne.value) // 1 +
+ +⚠️ reactive() 会返回一个修改过的原始的对象 + -count.value++ -console.log(countPlusOne.value) // 2 -``` +此行为与 Vue 2 中的 `Vue.observable` 一致 -## watch -▸ **watch**(source: *`Wrapper | () => any`*, callback: *`(newVal, oldVal)`*, options?: *[`WatchOption`](#WatchOption)*): `Function` +> :bulb: 在 Vue 3 中,`reactive()` 会返回一个新的的代理对象 -▸ **watch**(source: *`Array any>`*, callback: *`([newVal1, newVal2, ... newValN], [oldVal1, oldVal2, ... oldValN])`*, options?: *[`WatchOption`](#WatchOption)*): `Function` +
-`watch` 允许我们在相应的状态发生改变时执行一个回调函数。 +
+ +⚠️ setdel 添加与刪除响应式属性变通方案 + -**返回值** `Function` -一个可调用的函数来停止 `watch`。 +> ⚠️ 警告: `set` 和 `del` 并非 Vue 3 的一部分。由于 [Vue 2.x 响应式系统的限制](https://vuejs.org/v2/guide/reactivity.html#For-Objects), 我们在这里提供它们作为一种变通方案。 +> 在 Vue 2中,你将需要调用`set` 去追踪`object`上新的属性 (与`Vue.set`类似,但用于由 Composition API 创建的`reactive objects`)。在 Vue 3 中,你只需要像对待普通对象一样直接为属性赋值即可。 +> +> 同样地, 在 Vue 2 中你将需要调用`del`去 [确保响应式对象中属性的删除将触发视图更新](https://vuejs.org/v2/api/#Vue-delete) (与`Vue.delete`类似,但用于由 Composition API 创建的`reactive objects`)。在Vue3中,你只需要通过调用 `delete foo.bar` 来删除它们。 -> [effect-cleanup](https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/0000-function-api.md#effect-cleanup) 当前并不支持。 +```ts +import { reactive, set, del } from '@vue/composition-api' -### WatchOption -| Name | Type | Default | Description | -| ------ | ------ | ------ | ------ | -| lazy | `boolean` | `false` | 与 2.x `watch` 中的 `immediate` 选项含义相反. | -| deep | `boolean` | `false` | 与 2.x 相同 | -| flush | `"pre"` \| `"post"` \| `"sync"` | `"post"` | `"post"`: render 后触发; `"pre"`: render 前触发, `"sync"`: 同步触发 | +const a = reactive({ + foo: 1 +}) +// 添加新的响应式属性 +set(a, 'bar', 1) -Example: +// 刪除属性并触发响应式更新 +del(a, 'bar') +``` -```js -watch( - // getter - () => count.value + 1, - // callback - (value, oldValue) => { - console.log('count + 1 is: ', value) - } -) -// -> count + 1 is: 1 +
-count.value++ -// -> count + 1 is: 2 -``` +### Watch -Example(Multiple Sources): +
+ +❌ 不支持 onTrackonTrigger 选项 + ```js watch( - [valueA, () => valueB.value], - ([a, b], [prevA, prevB]) => { - console.log(`a is: ${a}`) - console.log(`b is: ${b}`) + () => { + /* ... */ + }, + { + immediate: true, + onTrack() {}, // 不可用 + onTrigger() {}, // 不可用 } ) ``` -## lifecycle -▸ **onCreated**(cb: *`Function`*) +
-▸ **onBeforeMount**(cb: *`Function`*) +### `createApp` -▸ **onMounted**(cb: *`Function`*) +
+ +⚠️ createApp() 是全局的 + -▸ **onXXX**(cb: *`Function`*) +在 Vue3 中,引入了 `createApp()` 来隔离不同应用实例的上下文(plugin, components 等)。 由于 Vue2 的设计,在这个插件中,我们提供 `createApp()` 作为一个向前兼容的 API ,它只是全局的一个别名。 -支持 2.x 中除 `beforeCreated` 以外的所有生命周期函数,额外提供 `onUnmounted`。 +```ts +const app1 = createApp(RootComponent1) +app1.component('Foo', Foo) // 相当于 Vue.component('Foo', Foo) +app1.use(VueRouter) // 相当于 Vue.use(VueRouter) -Example: +const app2 = createApp(RootComponent2) +app2.component('Bar', Bar) // 相当于 Vue.component('Bar', Bar) +``` -```js -import { onMounted, onUpdated, onUnmounted } from 'vue-function-api' +
+ +### `createElement` / `h` + +
+ +⚠️ createElement / h 变通方案 + + +
+ +在 Vue2中 `createElement` / `h` 只能通过 `render()` 函数访问。要在 `render()`之外使用它, 你可以显式地给它绑定一个组件实例。 + +> :warning: **警告**: 此功能是作为 Vue 2 的变通方法提供的,它不是 Vue 3 API 的一部分。 -const MyComponent = { +```jsx +import { h as _h } from '@vue/composition-api' + +export default { setup() { - onMounted(() => { - console.log('mounted!') - }) - onUpdated(() => { - console.log('updated!') - }) - onUnmounted(() => { - console.log('unmounted!') - }) - } + const vm = getCurrentInstance() + const h = _h.bind(vm) + + return () => + h('div', { + ref: 'root', + }) + }, } ``` -## provide, inject -▸ **provide**(value: *`Object`*) +
-▸ **inject**(key: *`string` | `symbol`*) -与 2.x 中的 `provide` 和 `inject` 选项行为一致。 +### `shallowReadonly` -Example: +
+ +⚠️ shallowReadonly() 会返回一个新的浅拷贝对象,在此之后新加的字段将不会获得只读或响应式状态。 + -```js -import { provide, inject } from 'vue-function-api' +> :bulb: 在 Vue 3 中,`shallowReadonly()` 会返回一个新的的代理对象 -const CountSymbol = Symbol() +
-const Ancestor = { - setup() { - // providing a value can make it reactive - const count = value(0) - provide({ - [CountSymbol]: count - }) - } -} +### `readonly` -const Descendent = { - setup() { - const count = inject(CountSymbol) - return { - count - } +
+ +⚠️ readonly() 只提供类型层面的只读。 + + +`readonly()` 只在类型层面提供和 Vue 3 的对齐。在其返回值或其属性上使用 isReadonly() 检查的结果将无法保证。 + +
+ +### `props` + +
+ +⚠️ 当使用 toRefs 访问深层属性对象 (如 toRefs(props.foo) 时将会得到不正确的警告。
+     ⚠️ isReactive(props.foo) 将会返回 false。 +
+ +```ts +defineComponent({ + setup(props) { + const { bar } = toRefs(props.foo) // it will `warn` + + // use this instead + const { foo } = toRefs(props) + const a = foo.value.bar } -} +}) ``` -## Context -`Context` 对象中的属性是 2.x 中的 vue 实例属性的一个子集。完整的属性列表: -* parent -* root -* refs -* slots -* attrs -* emit +
-## Wrapper (包装对象) -> 以下内容引自 [尤雨溪知乎专栏](https://zhuanlan.zhihu.com/p/68477600) +### `computed().effect` -一个包装对象只有一个属性:`value` ,该属性指向内部被包装的值。 +
+ +⚠️ computed() 拥有一个被设置为 trueeffect 属性,用来代替 ReactiveEffect。 + +由于实现上的不同, 在 `@vue/composition-api` 中没有 `ReactiveEffect` 这种概念。 因此, `effect` 为 `true` 只是为了能够区分 computed 和 refs: -### 为什么需要包装对象? -我们知道在 JavaScript 中,原始值类型如 `string` 和 `number` 是只有值,没有引用的。如果在一个函数中返回一个字符串变量,接收到这个字符串的代码只会获得一个值,是无法追踪原始变量后续的变化的。 - -因此,包装对象的意义就在于提供一个让我们能够在函数之间以引用的方式传递任意类型值的容器。这有点像 React Hooks 中的 `useRef` —— 但不同的是 Vue 的包装对象同时还是响应式的数据源。有了这样的容器,我们就可以在封装了逻辑的组合函数中将状态以引用的方式传回给组件。组件负责展示(追踪依赖),组合函数负责管理状态(触发更新): -```js -setup() { - const valueA = useLogicA() // valueA 可能被 useLogicA() 内部的代码修改从而触发更新 - const valueB = useLogicB() - return { - valueA, - valueB - } +```ts +function isComputed(o: ComputedRef | unknown): o is ComputedRef +function isComputed(o: any): o is ComputedRef { + return !!(isRef(o) && o.effect) } ``` -包装对象也可以包装非原始值类型的数据,被包装的对象中嵌套的属性都会被响应式地追踪。用包装对象去包装对象或是数组并不是没有意义的:它让我们可以对整个对象的值进行替换 —— 比如用一个 filter 过的数组去替代原数组: -```js -const numbers = value([1, 2, 3]) -// 替代原数组,但引用不变 -numbers.value = numbers.value.filter(n => n > 1) -``` +
-如果你依然想创建一个没有包装的响应式对象,可以使用 `state` API(和 2.x 的 `Vue.observable()` 等同): -```js -import { state } from 'vue' +### 缺失的 API -const object = state({ - count: 0 -}) +以下在 Vue 3 新引入的 API ,在本插件中暂不适用: -object.count++ -``` +- `onRenderTracked` +- `onRenderTriggered` +- `isProxy` -### Value Unwrapping(包装对象的自动展开) +### 在 `data()` 中使用组合式 API -当包装对象被暴露给模版渲染上下文,或是被嵌套在另一个响应式对象中的时候,它会被自动展开 (unwrap) 为内部的值。 +
+ +❌ 在 data() 中使用 ref, reactive 或其他组合式 API 将不会生效 + -比如一个包装对象的绑定可以直接被模版中的内联函数修改: -```js -const MyComponent = { - setup() { +```jsx +export default { + data() { return { - count: value(0) + // 在模版中会成为 { a: { value: 1 } } + a: ref(1), } }, - template: `` } ``` -当一个包装对象被作为另一个响应式对象的属性引用的时候也会被自动展开: - -```js -const count = value(0) -const obj = state({ - count +
+ +### `emit` 选项 + +
+ +❌ emit 仅因在类型定义中对齐 Vue3 的选项而提供,不会有任何效果。 + + +```ts +defineComponent({ + emit: { + // 无效 + submit: (eventOption) => { + if (...) { + return true + } else { + console.warn('Invalid submit event payload!') + return false + } + } + } }) - -console.log(obj.count) // 0 - -obj.count++ -console.log(obj.count) // 1 -console.log(count.value) // 1 - -count.value++ -console.log(obj.count) // 2 -console.log(count.value) // 2 ``` -以上这些关于包装对象的细节可能会让你觉得有些复杂,但实际使用中你只需要记住一个基本的规则:只有当你直接以变量的形式引用一个包装对象的时候才会需要用 `.value` 去取它内部的值 —— 在模版中你甚至不需要知道它们的存在。 +
+### 性能影响 -# 其他 +由于 Vue 2 的公共 API 的限制,`@vue/composition-api` 不可避免地引入了额外的性能开销。除非在极端情况下,否则这并不会对你造成影响。 -- `vue-function-api` 会一直保持与 `Vue3.x` 的兼容性,当 `3.0` 发布时,您可以无缝替换掉本库。 -- `vue-function-api` 的实现只依赖 `Vue2.x` 本身,不论 `Vue3.x` 的发布与否,都不会影响您正常使用本库。 -- 由于 `Vue2.x` 的公共 API 限制,`vue-function-api` 无法避免的会产生一些额外的内存负载。如果您的应用并不工作在极端内存环境下,无需关心此项。 +你可以查看这个 [跑分结果](https://antfu.github.io/vue-composition-api-benchmark-results/) 了解更多信息。 diff --git a/index.js b/index.js new file mode 100644 index 00000000..bf4ed976 --- /dev/null +++ b/index.js @@ -0,0 +1,7 @@ +'use strict' + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./dist/vue-composition-api.common.prod.js') +} else { + module.exports = require('./dist/vue-composition-api.common.js') +} diff --git a/package.json b/package.json index 2b7d8ad7..c449b3d3 100644 --- a/package.json +++ b/package.json @@ -1,88 +1,97 @@ { - "name": "vue-function-api", - "version": "2.0.6", + "name": "@vue/composition-api", + "version": "1.7.2", + "packageManager": "pnpm@8.6.12", "description": "Provide logic composition capabilities for Vue.", "keywords": [ "vue", - "function-api" + "composition-api" ], - "main": "dist/vue-function-api.js", - "umd:main": "dist/vue-function-api.umd.js", - "module": "dist/vue-function-api.module.js", - "typings": "dist/index.d.ts", - "author": "liximomo", + "repository": { + "type": "git", + "url": "git+https://github.com/vuejs/composition-api.git" + }, + "main": "./index.js", + "module": "./dist/vue-composition-api.mjs", + "unpkg": "./dist/vue-composition-api.prod.js", + "jsdelivr": "./dist/vue-composition-api.prod.js", + "types": "./dist/vue-composition-api.d.ts", + "exports": { + ".": { + "import": "./dist/vue-composition-api.mjs", + "types": "./dist/vue-composition-api.d.ts", + "require": "./index.js" + }, + "./*": "./*" + }, + "author": { + "name": "liximomo", + "email": "liximomo@gmail.com" + }, "license": "MIT", "sideEffects": false, "files": [ - "dist/" + "dist", + "index.js" ], "scripts": { - "start": "cross-env TARGET=es rollup -c -w", - "build": "rollup -c", - "test": "cross-env NODE_ENV=test jest", - "prepub": "npm run test && npm run build", - "pub": "npm publish" + "start": "rollup -c -w", + "build": "rimraf dist && rollup -c", + "lint": "prettier --write --parser typescript \"{src,test,test-dts}/**/*.ts?(x)\" && prettier --write \"{src,test}/**/*.js\"", + "test": "vitest", + "test:all": "pnpm run test && pnpm run test:dts", + "test:dts": "tsc -p ./test-dts/tsconfig.json && tsc -p ./test-dts/tsconfig.vue3.json && pnpm run build && tsc -p ./test-dts/tsconfig.build.json", + "update-readme": "node ./scripts/update-readme.js", + "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s && pnpm run update-readme && git add CHANGELOG.md README.*", + "release": "bumpp -x \"npm run changelog\" --all && npm publish", + "release-gh": "conventional-github-releaser -p angular", + "prepublishOnly": "npm run build" }, "bugs": { - "url": "https://github.com/vuejs/vue-function-api/issues" - }, - "homepage": "https://github.com/vuejs/vue-function-api#readme", - "devDependencies": { - "@types/jest": "^24.0.13", - "@types/node": "^12.0.2", - "cross-env": "^5.2.0", - "husky": "^2.7.0", - "jest": "^24.8.0", - "lint-staged": "^8.2.1", - "prettier": "^1.18.2", - "rollup": "^1.12.0", - "rollup-plugin-node-resolve": "^5.0.0", - "rollup-plugin-replace": "^2.2.0", - "rollup-plugin-terser": "^4.0.4", - "rollup-plugin-typescript2": "^0.21.0", - "typescript": "^3.4.5", - "vue": "^2.5.22" + "url": "https://github.com/vuejs/composition-api/issues" }, + "homepage": "https://github.com/vuejs/composition-api#readme", "peerDependencies": { - "vue": "^2.5.22" + "vue": ">= 2.5 < 2.7" }, - "dependencies": { - "tslib": "^1.9.3" + "devDependencies": { + "@rollup/plugin-node-resolve": "^13.3.0", + "@rollup/plugin-replace": "^4.0.0", + "@types/node": "^17.0.31", + "bumpp": "^9.1.1", + "conventional-changelog-cli": "^2.2.2", + "conventional-github-releaser": "^3.1.5", + "jsdom": "^20.0.0", + "lint-staged": "^14.0.0", + "prettier": "^3.0.1", + "rimraf": "^5.0.1", + "rollup": "^2.72.0", + "rollup-plugin-dts": "^4.2.1", + "rollup-plugin-terser": "^7.0.2", + "rollup-plugin-typescript2": "^0.31.2", + "simple-git-hooks": "^2.9.0", + "tslib": "^2.4.0", + "typescript": "^4.6.4", + "vitest": "^0.34.1", + "vue": "^2.6.14", + "vue3": "npm:vue@3.2.21", + "vue-router": "^3.5.3", + "vue-server-renderer": "^2.6.14" }, - "husky": { - "hooks": { - "pre-commit": "lint-staged" - } + "simple-git-hooks": { + "pre-commit": "lint-staged" }, "lint-staged": { "*.js": [ - "prettier --write", - "git add" - ], - "*.ts": [ - "prettier --parser=typescript --write", - "git add" - ] - }, - "jest": { - "verbose": true, - "setupFilesAfterEnv": [ - "/test/helpers/wait-for-update.js" - ], - "moduleFileExtensions": [ - "ts", - "js" + "prettier --write" ], - "transform": { - "^.+\\.[jt]s$": "/test/tsTransform.js" - }, - "testMatch": [ - "/test/**/*.spec.js" + "*.ts?(x)": [ + "prettier --parser=typescript --write" ] }, "prettier": { - "printWidth": 100, + "semi": false, "singleQuote": true, - "trailingComma": "es5" + "printWidth": 80 } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 00000000..773d5d6b --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,4321 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +devDependencies: + '@rollup/plugin-node-resolve': + specifier: ^13.3.0 + version: 13.3.0(rollup@2.72.0) + '@rollup/plugin-replace': + specifier: ^4.0.0 + version: 4.0.0(rollup@2.72.0) + '@types/node': + specifier: ^17.0.31 + version: 17.0.31 + bumpp: + specifier: ^9.1.1 + version: 9.1.1 + conventional-changelog-cli: + specifier: ^2.2.2 + version: 2.2.2 + conventional-github-releaser: + specifier: ^3.1.5 + version: 3.1.5 + jsdom: + specifier: ^20.0.0 + version: 20.0.0 + lint-staged: + specifier: ^14.0.0 + version: 14.0.0 + prettier: + specifier: ^3.0.1 + version: 3.0.1 + rimraf: + specifier: ^5.0.1 + version: 5.0.1 + rollup: + specifier: ^2.72.0 + version: 2.72.0 + rollup-plugin-dts: + specifier: ^4.2.1 + version: 4.2.1(rollup@2.72.0)(typescript@4.6.4) + rollup-plugin-terser: + specifier: ^7.0.2 + version: 7.0.2(rollup@2.72.0) + rollup-plugin-typescript2: + specifier: ^0.31.2 + version: 0.31.2(@types/bluebird@3.5.38)(@types/node@17.0.31)(rollup@2.72.0)(ts-toolbelt@9.6.0)(typescript@4.6.4) + simple-git-hooks: + specifier: ^2.9.0 + version: 2.9.0 + tslib: + specifier: ^2.4.0 + version: 2.4.0 + typescript: + specifier: ^4.6.4 + version: 4.6.4 + vitest: + specifier: ^0.34.1 + version: 0.34.1(jsdom@20.0.0) + vue: + specifier: ^2.6.14 + version: 2.6.14 + vue-router: + specifier: ^3.5.3 + version: 3.5.3(vue@2.6.14) + vue-server-renderer: + specifier: ^2.6.14 + version: 2.6.14 + vue3: + specifier: npm:vue@3.2.21 + version: /vue@3.2.21 + +packages: + + /@babel/code-frame@7.16.7: + resolution: {integrity: sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.16.10 + dev: true + + /@babel/helper-string-parser@7.22.5: + resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier@7.16.7: + resolution: {integrity: sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier@7.22.5: + resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/highlight@7.16.10: + resolution: {integrity: sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.16.7 + chalk: 2.4.2 + js-tokens: 4.0.0 + dev: true + + /@babel/parser@7.16.12: + resolution: {integrity: sha512-VfaV15po8RiZssrkPweyvbGVSe4x2y+aciFCgn0n0/SJMR22cwofRV1mtnJQYcSB1wUTaA/X1LnA3es66MCO5A==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.22.10 + dev: true + + /@babel/types@7.22.10: + resolution: {integrity: sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.22.5 + '@babel/helper-validator-identifier': 7.22.5 + to-fast-properties: 2.0.0 + dev: true + + /@esbuild/linux-loong64@0.14.54: + resolution: {integrity: sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@hutson/parse-repository-url@3.0.2: + resolution: {integrity: sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==} + engines: {node: '>=6.9.0'} + dev: true + + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.0.1 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: true + + /@jest/schemas@29.6.0: + resolution: {integrity: sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jsdevtools/ez-spawn@3.0.4: + resolution: {integrity: sha512-f5DRIOZf7wxogefH03RjMPMdBF7ADTWUMoOs9kaJo06EfwF+aFhMZMDZxHg/Xe12hptN9xoZjGso2fdjapBRIA==} + engines: {node: '>=10'} + dependencies: + call-me-maybe: 1.0.1 + cross-spawn: 7.0.3 + string-argv: 0.3.1 + type-detect: 4.0.8 + dev: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.13.0 + dev: true + + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: true + optional: true + + /@rollup/plugin-node-resolve@13.3.0(rollup@2.72.0): + resolution: {integrity: sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw==} + engines: {node: '>= 10.0.0'} + peerDependencies: + rollup: ^2.42.0 + dependencies: + '@rollup/pluginutils': 3.1.0(rollup@2.72.0) + '@types/resolve': 1.17.1 + deepmerge: 4.2.2 + is-builtin-module: 3.1.0 + is-module: 1.0.0 + resolve: 1.22.0 + rollup: 2.72.0 + dev: true + + /@rollup/plugin-replace@4.0.0(rollup@2.72.0): + resolution: {integrity: sha512-+rumQFiaNac9y64OHtkHGmdjm7us9bo1PlbgQfdihQtuNxzjpaB064HbRnewUOggLQxVCCyINfStkgmBeQpv1g==} + peerDependencies: + rollup: ^1.20.0 || ^2.0.0 + dependencies: + '@rollup/pluginutils': 3.1.0(rollup@2.72.0) + magic-string: 0.25.7 + rollup: 2.72.0 + dev: true + + /@rollup/pluginutils@3.1.0(rollup@2.72.0): + resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} + engines: {node: '>= 8.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + dependencies: + '@types/estree': 0.0.39 + estree-walker: 1.0.1 + picomatch: 2.3.1 + rollup: 2.72.0 + dev: true + + /@rollup/pluginutils@4.1.2: + resolution: {integrity: sha512-ROn4qvkxP9SyPeHaf7uQC/GPFY6L/OWy9+bd9AwcjOAWQwxRscoEyAUD8qCY5o5iL4jqQwoLk2kaTKJPb/HwzQ==} + engines: {node: '>= 8.0.0'} + dependencies: + estree-walker: 2.0.2 + picomatch: 2.3.1 + dev: true + + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true + + /@sindresorhus/is@0.7.0: + resolution: {integrity: sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==} + engines: {node: '>=4'} + dev: true + + /@tootallnate/once@2.0.0: + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + dev: true + + /@ts-type/package-dts@1.0.58(@types/bluebird@3.5.38)(@types/node@17.0.31)(ts-toolbelt@9.6.0): + resolution: {integrity: sha512-Ry5RPZDAnSz/gyLtjd2a2yNC07CZ/PCOsuDzYj3phOolIgEH68HXRw6SbsDlavnVUEenDYj5GUM10gQ5iVEbVQ==} + peerDependencies: + '@types/bluebird': '*' + '@types/node': '*' + ts-toolbelt: '*' + dependencies: + '@types/bluebird': 3.5.38 + '@types/node': 17.0.31 + '@types/semver': 7.3.9 + ts-toolbelt: 9.6.0 + ts-type: 2.1.4(@types/bluebird@3.5.38)(@types/node@17.0.31)(ts-toolbelt@9.6.0) + dev: true + + /@types/bluebird@3.5.38: + resolution: {integrity: sha512-yR/Kxc0dd4FfwtEoLZMoqJbM/VE/W7hXn/MIjb+axcwag0iFmSPK7OBUZq1YWLynJUoWQkfUrI7T0HDqGApNSg==} + dev: true + + /@types/chai-subset@1.3.3: + resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} + dependencies: + '@types/chai': 4.3.5 + dev: true + + /@types/chai@4.3.5: + resolution: {integrity: sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==} + dev: true + + /@types/estree@0.0.39: + resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} + dev: true + + /@types/keyv@3.1.4: + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + dependencies: + '@types/node': 17.0.31 + dev: true + + /@types/minimist@1.2.2: + resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} + dev: true + + /@types/node@17.0.31: + resolution: {integrity: sha512-AR0x5HbXGqkEx9CadRH3EBYx/VkiUgZIhP4wvPn/+5KIsgpNoyFaRlVe0Zlx9gRtg8fA06a9tskE2MSN7TcG4Q==} + dev: true + + /@types/normalize-package-data@2.4.1: + resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} + dev: true + + /@types/resolve@1.17.1: + resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} + dependencies: + '@types/node': 17.0.31 + dev: true + + /@types/responselike@1.0.0: + resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} + dependencies: + '@types/node': 17.0.31 + dev: true + + /@types/semver@7.3.9: + resolution: {integrity: sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==} + dev: true + + /@vitest/expect@0.34.1: + resolution: {integrity: sha512-q2CD8+XIsQ+tHwypnoCk8Mnv5e6afLFvinVGCq3/BOT4kQdVQmY6rRfyKkwcg635lbliLPqbunXZr+L1ssUWiQ==} + dependencies: + '@vitest/spy': 0.34.1 + '@vitest/utils': 0.34.1 + chai: 4.3.7 + dev: true + + /@vitest/runner@0.34.1: + resolution: {integrity: sha512-YfQMpYzDsYB7yqgmlxZ06NI4LurHWfrH7Wy3Pvf/z/vwUSgq1zLAb1lWcItCzQG+NVox+VvzlKQrYEXb47645g==} + dependencies: + '@vitest/utils': 0.34.1 + p-limit: 4.0.0 + pathe: 1.1.1 + dev: true + + /@vitest/snapshot@0.34.1: + resolution: {integrity: sha512-0O9LfLU0114OqdF8lENlrLsnn024Tb1CsS9UwG0YMWY2oGTQfPtkW+B/7ieyv0X9R2Oijhi3caB1xgGgEgclSQ==} + dependencies: + magic-string: 0.30.2 + pathe: 1.1.1 + pretty-format: 29.6.2 + dev: true + + /@vitest/spy@0.34.1: + resolution: {integrity: sha512-UT4WcI3EAPUNO8n6y9QoEqynGGEPmmRxC+cLzneFFXpmacivjHZsNbiKD88KUScv5DCHVDgdBsLD7O7s1enFcQ==} + dependencies: + tinyspy: 2.1.1 + dev: true + + /@vitest/utils@0.34.1: + resolution: {integrity: sha512-/ql9dsFi4iuEbiNcjNHQWXBum7aL8pyhxvfnD9gNtbjR9fUKAjxhj4AA3yfLXg6gJpMGGecvtF8Au2G9y3q47Q==} + dependencies: + diff-sequences: 29.4.3 + loupe: 2.3.6 + pretty-format: 29.6.2 + dev: true + + /@vue/compiler-core@3.2.21: + resolution: {integrity: sha512-NhhiQZNG71KNq1h5pMW/fAXdTF7lJRaSI7LDm2edhHXVz1ROMICo8SreUmQnSf4Fet0UPBVqJ988eF4+936iDQ==} + dependencies: + '@babel/parser': 7.16.12 + '@vue/shared': 3.2.21 + estree-walker: 2.0.2 + source-map: 0.6.1 + dev: true + + /@vue/compiler-dom@3.2.21: + resolution: {integrity: sha512-gsJD3DpYZSYquiA7UIPsMDSlAooYWDvHPq9VRsqzJEk2PZtFvLvHPb4aaMD8Ufd62xzYn32cnnkzsEOJhyGilA==} + dependencies: + '@vue/compiler-core': 3.2.21 + '@vue/shared': 3.2.21 + dev: true + + /@vue/compiler-sfc@3.2.21: + resolution: {integrity: sha512-+yDlUSebKpz/ovxM2vLRRx7w/gVfY767pOfYTgbIhAs+ogvIV2BsIt4fpxlThnlCNChJ+yE0ERUNoROv2kEGEQ==} + dependencies: + '@babel/parser': 7.16.12 + '@vue/compiler-core': 3.2.21 + '@vue/compiler-dom': 3.2.21 + '@vue/compiler-ssr': 3.2.21 + '@vue/ref-transform': 3.2.21 + '@vue/shared': 3.2.21 + estree-walker: 2.0.2 + magic-string: 0.25.7 + postcss: 8.4.16 + source-map: 0.6.1 + dev: true + + /@vue/compiler-ssr@3.2.21: + resolution: {integrity: sha512-eU+A0iWYy+1zAo2CRIJ0zSVlv1iuGAIbNRCnllSJ31pV1lX3jypJYzGbJlSRAbB7VP6E+tYveVT1Oq8JKewa3g==} + dependencies: + '@vue/compiler-dom': 3.2.21 + '@vue/shared': 3.2.21 + dev: true + + /@vue/reactivity@3.2.21: + resolution: {integrity: sha512-7C57zFm/5E3SSTUhVuYj1InDwuJ+GIVQ/z+H43C9sST85gIThGXVhksl1yWTAadf8Yz4T5lSbqi5Ds8U/ueWcw==} + dependencies: + '@vue/shared': 3.2.21 + dev: true + + /@vue/ref-transform@3.2.21: + resolution: {integrity: sha512-uiEWWBsrGeun9O7dQExYWzXO3rHm/YdtFNXDVqCSoPypzOVxWxdiL+8hHeWzxMB58fVuV2sT80aUtIVyaBVZgQ==} + dependencies: + '@babel/parser': 7.16.12 + '@vue/compiler-core': 3.2.21 + '@vue/shared': 3.2.21 + estree-walker: 2.0.2 + magic-string: 0.25.7 + dev: true + + /@vue/runtime-core@3.2.21: + resolution: {integrity: sha512-7oOxKaU0D2IunOAMOOHZgJVrHg63xwng8BZx3fbgmakqEIMwHhQcp+5GV1sOg/sWW7R4UhaRDIUCukO2GRVK2Q==} + dependencies: + '@vue/reactivity': 3.2.21 + '@vue/shared': 3.2.21 + dev: true + + /@vue/runtime-dom@3.2.21: + resolution: {integrity: sha512-apBdriD6QsI4ywbllY8kjr9/0scGuStDuvLbJULPQkFPtHzntd51bP5PQTQVAEIc9kwnTozmj6x6ZdX/cwo7xA==} + dependencies: + '@vue/runtime-core': 3.2.21 + '@vue/shared': 3.2.21 + csstype: 2.6.19 + dev: true + + /@vue/server-renderer@3.2.21(vue@2.6.14): + resolution: {integrity: sha512-QBgYqVgI7XCSBCqGa4LduV9vpfQFdZBOodFmq5Txk5W/v1KrJ1LoOh2Q0RHiRgtoK/UR9uyvRVcYqOmwHkZNEg==} + peerDependencies: + vue: 3.2.21 + dependencies: + '@vue/compiler-ssr': 3.2.21 + '@vue/shared': 3.2.21 + vue: 2.6.14 + dev: true + + /@vue/shared@3.2.21: + resolution: {integrity: sha512-5EQmIPK6gw4UVYUbM959B0uPsJ58+xoMESCZs3N89XyvJ9e+fX4pqEPrOGV8OroIk3SbEvJcC+eYc8BH9JQrHA==} + dev: true + + /@yarn-tool/resolve-package@1.0.42(@types/bluebird@3.5.38)(@types/node@17.0.31)(ts-toolbelt@9.6.0): + resolution: {integrity: sha512-1BAsoiD6jGAaPc7mRH0UxIVXgRSTv7fnhwfKkaFUYpqsU4ZR7KIigZTMcb2bujtlzKQbNneMPQGjiqe3F8cmlw==} + peerDependencies: + '@types/node': '*' + dependencies: + '@ts-type/package-dts': 1.0.58(@types/bluebird@3.5.38)(@types/node@17.0.31)(ts-toolbelt@9.6.0) + '@types/node': 17.0.31 + pkg-dir: 5.0.0 + tslib: 2.4.0 + upath2: 3.1.12(@types/node@17.0.31) + transitivePeerDependencies: + - '@types/bluebird' + - ts-toolbelt + dev: true + + /JSONStream@1.3.5: + resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} + hasBin: true + dependencies: + jsonparse: 1.3.1 + through: 2.3.8 + dev: true + + /abab@2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + dev: true + + /acorn-globals@6.0.0: + resolution: {integrity: sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==} + dependencies: + acorn: 7.4.1 + acorn-walk: 7.2.0 + dev: true + + /acorn-walk@7.2.0: + resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn-walk@8.2.0: + resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /acorn@8.10.0: + resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /acorn@8.8.0: + resolution: {integrity: sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /add-stream@1.0.0: + resolution: {integrity: sha1-anmQQ3ynNtXhKI25K9MmbV9csqo=} + dev: true + + /agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /ansi-escapes@5.0.0: + resolution: {integrity: sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==} + engines: {node: '>=12'} + dependencies: + type-fest: 1.4.0 + dev: true + + /ansi-regex@2.1.1: + resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} + engines: {node: '>=0.10.0'} + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: true + + /ansi-styles@2.2.1: + resolution: {integrity: sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=} + engines: {node: '>=0.10.0'} + dev: true + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + dev: true + + /ansi-styles@6.1.0: + resolution: {integrity: sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ==} + engines: {node: '>=12'} + dev: true + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /array-find-index@1.0.2: + resolution: {integrity: sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=} + engines: {node: '>=0.10.0'} + dev: true + + /array-ify@1.0.0: + resolution: {integrity: sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=} + dev: true + + /arrify@1.0.1: + resolution: {integrity: sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=} + engines: {node: '>=0.10.0'} + dev: true + + /assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + dev: true + + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /browser-process-hrtime@1.0.0: + resolution: {integrity: sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==} + dev: true + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + + /builtin-modules@3.2.0: + resolution: {integrity: sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==} + engines: {node: '>=6'} + dev: true + + /bumpp@9.1.1: + resolution: {integrity: sha512-T7/2QmRNhHRkH2+HgDs/xk4keom3nlCjwQn6kHdz0I0dQMVrs+YMOH5HyuhV0R3tha/tTYP030RG9uQKpQ9CRg==} + engines: {node: '>=10'} + hasBin: true + dependencies: + '@jsdevtools/ez-spawn': 3.0.4 + c12: 1.4.2 + cac: 6.7.14 + fast-glob: 3.3.1 + prompts: 2.4.2 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + dev: true + + /c12@1.4.2: + resolution: {integrity: sha512-3IP/MuamSVRVw8W8+CHWAz9gKN4gd+voF2zm/Ln6D25C2RhytEZ1ABbC8MjKr4BR9rhoV1JQ7jJA158LDiTkLg==} + dependencies: + chokidar: 3.5.3 + defu: 6.1.2 + dotenv: 16.3.1 + giget: 1.1.2 + jiti: 1.19.1 + mlly: 1.4.0 + ohash: 1.1.3 + pathe: 1.1.1 + perfect-debounce: 1.0.0 + pkg-types: 1.0.3 + rc9: 2.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + dev: true + + /cacheable-request@2.1.4: + resolution: {integrity: sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0=} + dependencies: + clone-response: 1.0.2 + get-stream: 3.0.0 + http-cache-semantics: 3.8.1 + keyv: 3.0.0 + lowercase-keys: 1.0.0 + normalize-url: 2.0.1 + responselike: 1.0.2 + dev: true + + /call-me-maybe@1.0.1: + resolution: {integrity: sha512-wCyFsDQkKPwwF8BDwOiWNx/9K45L/hvggQiDbve+viMNMQnWhrlYIuBk09offfwCRtCO9P6XwUttufzU11WCVw==} + dev: true + + /camelcase-keys@2.1.0: + resolution: {integrity: sha1-MIvur/3ygRkFHvodkyITyRuPkuc=} + engines: {node: '>=0.10.0'} + dependencies: + camelcase: 2.1.1 + map-obj: 1.0.1 + dev: true + + /camelcase-keys@4.2.0: + resolution: {integrity: sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=} + engines: {node: '>=4'} + dependencies: + camelcase: 4.1.0 + map-obj: 2.0.0 + quick-lru: 1.1.0 + dev: true + + /camelcase-keys@6.2.2: + resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} + engines: {node: '>=8'} + dependencies: + camelcase: 5.3.1 + map-obj: 4.3.0 + quick-lru: 4.0.1 + dev: true + + /camelcase@2.1.1: + resolution: {integrity: sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=} + engines: {node: '>=0.10.0'} + dev: true + + /camelcase@4.1.0: + resolution: {integrity: sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=} + engines: {node: '>=4'} + dev: true + + /camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + dev: true + + /chai@4.3.7: + resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==} + engines: {node: '>=4'} + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.2 + deep-eql: 4.1.3 + get-func-name: 2.0.0 + loupe: 2.3.4 + pathval: 1.1.1 + type-detect: 4.0.8 + dev: true + + /chalk@1.1.3: + resolution: {integrity: sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=} + engines: {node: '>=0.10.0'} + dependencies: + ansi-styles: 2.2.1 + escape-string-regexp: 1.0.5 + has-ansi: 2.0.0 + strip-ansi: 3.0.1 + supports-color: 2.0.0 + dev: true + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: true + + /chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + dev: true + + /check-error@1.0.2: + resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} + dev: true + + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + dev: true + + /cli-cursor@4.0.0: + resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + restore-cursor: 4.0.0 + dev: true + + /cli-truncate@3.1.0: + resolution: {integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + slice-ansi: 5.0.0 + string-width: 5.1.2 + dev: true + + /cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true + + /clone-response@1.0.2: + resolution: {integrity: sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=} + dependencies: + mimic-response: 1.0.1 + dev: true + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + dev: true + + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: true + + /commander@11.0.0: + resolution: {integrity: sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==} + engines: {node: '>=16'} + dev: true + + /commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + dev: true + + /commondir@1.0.1: + resolution: {integrity: sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=} + dev: true + + /compare-func@1.3.4: + resolution: {integrity: sha512-sq2sWtrqKPkEXAC8tEJA1+BqAH9GbFkGBtUOqrUX57VSfwp8xyktctk+uLoRy5eccTdxzDcVIztlYDpKs3Jv1Q==} + dependencies: + array-ify: 1.0.0 + dot-prop: 3.0.0 + dev: true + + /compare-func@2.0.0: + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + dependencies: + array-ify: 1.0.0 + dot-prop: 5.3.0 + dev: true + + /conventional-changelog-angular@1.6.6: + resolution: {integrity: sha512-suQnFSqCxRwyBxY68pYTsFkG0taIdinHLNEAX5ivtw8bCRnIgnpvcHmlR/yjUyZIrNPYAoXlY1WiEKWgSE4BNg==} + dependencies: + compare-func: 1.3.4 + q: 1.5.1 + dev: true + + /conventional-changelog-angular@5.0.13: + resolution: {integrity: sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==} + engines: {node: '>=10'} + dependencies: + compare-func: 2.0.0 + q: 1.5.1 + dev: true + + /conventional-changelog-atom@2.0.8: + resolution: {integrity: sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw==} + engines: {node: '>=10'} + dependencies: + q: 1.5.1 + dev: true + + /conventional-changelog-cli@2.2.2: + resolution: {integrity: sha512-8grMV5Jo8S0kP3yoMeJxV2P5R6VJOqK72IiSV9t/4H5r/HiRqEBQ83bYGuz4Yzfdj4bjaAEhZN/FFbsFXr5bOA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + add-stream: 1.0.0 + conventional-changelog: 3.1.25 + lodash: 4.17.21 + meow: 8.1.2 + tempfile: 3.0.0 + dev: true + + /conventional-changelog-codemirror@2.0.8: + resolution: {integrity: sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw==} + engines: {node: '>=10'} + dependencies: + q: 1.5.1 + dev: true + + /conventional-changelog-conventionalcommits@4.6.3: + resolution: {integrity: sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g==} + engines: {node: '>=10'} + dependencies: + compare-func: 2.0.0 + lodash: 4.17.21 + q: 1.5.1 + dev: true + + /conventional-changelog-core@3.2.3: + resolution: {integrity: sha512-LMMX1JlxPIq/Ez5aYAYS5CpuwbOk6QFp8O4HLAcZxe3vxoCtABkhfjetk8IYdRB9CDQGwJFLR3Dr55Za6XKgUQ==} + engines: {node: '>=6.9.0'} + dependencies: + conventional-changelog-writer: 4.1.0 + conventional-commits-parser: 3.2.4 + dateformat: 3.0.3 + get-pkg-repo: 1.4.0 + git-raw-commits: 2.0.0 + git-remote-origin-url: 2.0.0 + git-semver-tags: 2.0.3 + lodash: 4.17.21 + normalize-package-data: 2.5.0 + q: 1.5.1 + read-pkg: 3.0.0 + read-pkg-up: 3.0.0 + through2: 3.0.2 + dev: true + + /conventional-changelog-core@4.2.4: + resolution: {integrity: sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg==} + engines: {node: '>=10'} + dependencies: + add-stream: 1.0.0 + conventional-changelog-writer: 5.0.1 + conventional-commits-parser: 3.2.4 + dateformat: 3.0.3 + get-pkg-repo: 4.2.1 + git-raw-commits: 2.0.11 + git-remote-origin-url: 2.0.0 + git-semver-tags: 4.1.1 + lodash: 4.17.21 + normalize-package-data: 3.0.3 + q: 1.5.1 + read-pkg: 3.0.0 + read-pkg-up: 3.0.0 + through2: 4.0.2 + dev: true + + /conventional-changelog-ember@2.0.9: + resolution: {integrity: sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A==} + engines: {node: '>=10'} + dependencies: + q: 1.5.1 + dev: true + + /conventional-changelog-eslint@3.0.9: + resolution: {integrity: sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA==} + engines: {node: '>=10'} + dependencies: + q: 1.5.1 + dev: true + + /conventional-changelog-express@2.0.6: + resolution: {integrity: sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ==} + engines: {node: '>=10'} + dependencies: + q: 1.5.1 + dev: true + + /conventional-changelog-jquery@0.1.0: + resolution: {integrity: sha1-Agg5cWLjhGmG5xJztsecW1+A9RA=} + dependencies: + q: 1.5.1 + dev: true + + /conventional-changelog-jquery@3.0.11: + resolution: {integrity: sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw==} + engines: {node: '>=10'} + dependencies: + q: 1.5.1 + dev: true + + /conventional-changelog-jscs@0.1.0: + resolution: {integrity: sha1-BHnrRDzH1yxYvwvPDvHURKkvDlw=} + dependencies: + q: 1.5.1 + dev: true + + /conventional-changelog-jshint@2.0.9: + resolution: {integrity: sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA==} + engines: {node: '>=10'} + dependencies: + compare-func: 2.0.0 + q: 1.5.1 + dev: true + + /conventional-changelog-preset-loader@2.3.4: + resolution: {integrity: sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g==} + engines: {node: '>=10'} + dev: true + + /conventional-changelog-writer@4.1.0: + resolution: {integrity: sha512-WwKcUp7WyXYGQmkLsX4QmU42AZ1lqlvRW9mqoyiQzdD+rJWbTepdWoKJuwXTS+yq79XKnQNa93/roViPQrAQgw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + compare-func: 2.0.0 + conventional-commits-filter: 2.0.7 + dateformat: 3.0.3 + handlebars: 4.7.7 + json-stringify-safe: 5.0.1 + lodash: 4.17.21 + meow: 8.1.2 + semver: 6.3.0 + split: 1.0.1 + through2: 4.0.2 + dev: true + + /conventional-changelog-writer@5.0.1: + resolution: {integrity: sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==} + engines: {node: '>=10'} + hasBin: true + dependencies: + conventional-commits-filter: 2.0.7 + dateformat: 3.0.3 + handlebars: 4.7.7 + json-stringify-safe: 5.0.1 + lodash: 4.17.21 + meow: 8.1.2 + semver: 6.3.0 + split: 1.0.1 + through2: 4.0.2 + dev: true + + /conventional-changelog@2.0.3: + resolution: {integrity: sha512-4bcII9cJHSKb2qi9e8qGF6aJHLf/AB0dokhyR+X6QILTMl77s4l163vK+reXhajvfOYbbHQvsrWybr5+PKZwNA==} + engines: {node: '>=6.9.0'} + dependencies: + conventional-changelog-angular: 1.6.6 + conventional-changelog-atom: 2.0.8 + conventional-changelog-codemirror: 2.0.8 + conventional-changelog-core: 3.2.3 + conventional-changelog-ember: 2.0.9 + conventional-changelog-eslint: 3.0.9 + conventional-changelog-express: 2.0.6 + conventional-changelog-jquery: 0.1.0 + conventional-changelog-jscs: 0.1.0 + conventional-changelog-jshint: 2.0.9 + conventional-changelog-preset-loader: 2.3.4 + dev: true + + /conventional-changelog@3.1.25: + resolution: {integrity: sha512-ryhi3fd1mKf3fSjbLXOfK2D06YwKNic1nC9mWqybBHdObPd8KJ2vjaXZfYj1U23t+V8T8n0d7gwnc9XbIdFbyQ==} + engines: {node: '>=10'} + dependencies: + conventional-changelog-angular: 5.0.13 + conventional-changelog-atom: 2.0.8 + conventional-changelog-codemirror: 2.0.8 + conventional-changelog-conventionalcommits: 4.6.3 + conventional-changelog-core: 4.2.4 + conventional-changelog-ember: 2.0.9 + conventional-changelog-eslint: 3.0.9 + conventional-changelog-express: 2.0.6 + conventional-changelog-jquery: 3.0.11 + conventional-changelog-jshint: 2.0.9 + conventional-changelog-preset-loader: 2.3.4 + dev: true + + /conventional-commits-filter@2.0.7: + resolution: {integrity: sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA==} + engines: {node: '>=10'} + dependencies: + lodash.ismatch: 4.4.0 + modify-values: 1.0.1 + dev: true + + /conventional-commits-parser@3.2.4: + resolution: {integrity: sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==} + engines: {node: '>=10'} + hasBin: true + dependencies: + JSONStream: 1.3.5 + is-text-path: 1.0.1 + lodash: 4.17.21 + meow: 8.1.2 + split2: 3.2.2 + through2: 4.0.2 + dev: true + + /conventional-github-releaser@3.1.5: + resolution: {integrity: sha512-VhPKbdN92b2ygnQLkuwHIfUaPAVrVfJVuQdxbmmVPkN927LDP98HthLWFVShh4pxqLK0nE66v78RERGJVeCzbg==} + engines: {node: '>=6.9.0'} + hasBin: true + dependencies: + conventional-changelog: 2.0.3 + dateformat: 3.0.3 + debug: 3.2.7 + gh-got: 7.1.0 + git-semver-tags: 2.0.3 + lodash.merge: 4.6.2 + meow: 7.1.1 + object-assign: 4.1.1 + q: 1.5.1 + semver: 5.7.1 + semver-regex: 2.0.0 + through2: 2.0.5 + transitivePeerDependencies: + - supports-color + dev: true + + /core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /cssom@0.3.8: + resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} + dev: true + + /cssom@0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + dev: true + + /cssstyle@2.3.0: + resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} + engines: {node: '>=8'} + dependencies: + cssom: 0.3.8 + dev: true + + /csstype@2.6.19: + resolution: {integrity: sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ==} + dev: true + + /currently-unhandled@0.4.1: + resolution: {integrity: sha1-mI3zP+qxke95mmE2nddsF635V+o=} + engines: {node: '>=0.10.0'} + dependencies: + array-find-index: 1.0.2 + dev: true + + /dargs@4.1.0: + resolution: {integrity: sha1-A6nbtLXC8Tm/FK5T8LiipqhvThc=} + engines: {node: '>=0.10.0'} + dependencies: + number-is-nan: 1.0.1 + dev: true + + /dargs@7.0.0: + resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==} + engines: {node: '>=8'} + dev: true + + /data-urls@3.0.2: + resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} + engines: {node: '>=12'} + dependencies: + abab: 2.0.6 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + dev: true + + /dateformat@3.0.3: + resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==} + dev: true + + /debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: true + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /decamelize-keys@1.1.0: + resolution: {integrity: sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=} + engines: {node: '>=0.10.0'} + dependencies: + decamelize: 1.2.0 + map-obj: 1.0.1 + dev: true + + /decamelize@1.2.0: + resolution: {integrity: sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=} + engines: {node: '>=0.10.0'} + dev: true + + /decimal.js@10.4.0: + resolution: {integrity: sha512-Nv6ENEzyPQ6AItkGwLE2PGKinZZ9g59vSh2BeH6NqPu0OTKZ5ruJsVqh/orbAnqXc9pBbgXAIrc2EyaCj8NpGg==} + dev: true + + /decode-uri-component@0.2.0: + resolution: {integrity: sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=} + engines: {node: '>=0.10'} + dev: true + + /decompress-response@3.3.0: + resolution: {integrity: sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=} + engines: {node: '>=4'} + dependencies: + mimic-response: 1.0.1 + dev: true + + /deep-eql@4.1.3: + resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} + engines: {node: '>=6'} + dependencies: + type-detect: 4.0.8 + dev: true + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /deepmerge@4.2.2: + resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} + engines: {node: '>=0.10.0'} + dev: true + + /defu@6.1.2: + resolution: {integrity: sha512-+uO4+qr7msjNNWKYPHqN/3+Dx3NFkmIzayk2L1MyZQlvgZb/J1A0fo410dpKrN2SnqFjt8n4JL8fDJE0wIgjFQ==} + dev: true + + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: true + + /destr@2.0.1: + resolution: {integrity: sha512-M1Ob1zPSIvlARiJUkKqvAZ3VAqQY6Jcuth/pBKQ2b1dX/Qx0OnJ8Vux6J2H5PTMQeRzWrrbTu70VxBfv/OPDJA==} + dev: true + + /diff-sequences@29.4.3: + resolution: {integrity: sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /domexception@4.0.0: + resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} + engines: {node: '>=12'} + dependencies: + webidl-conversions: 7.0.0 + dev: true + + /dot-prop@3.0.0: + resolution: {integrity: sha1-G3CK8JSknJoOfbyteQq6U52sEXc=} + engines: {node: '>=0.10.0'} + dependencies: + is-obj: 1.0.1 + dev: true + + /dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + dependencies: + is-obj: 2.0.0 + dev: true + + /dotenv@16.3.1: + resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} + engines: {node: '>=12'} + dev: true + + /duplexer3@0.1.4: + resolution: {integrity: sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=} + dev: true + + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true + + /entities@4.3.1: + resolution: {integrity: sha512-o4q/dYJlmyjP2zfnaWDUC6A3BQFmVTX+tZPezK7k0GLSU9QYCauscf5Y+qcEPzKL+EixVouYDgLQK5H9GrLpkg==} + engines: {node: '>=0.12'} + dev: true + + /error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + dependencies: + is-arrayish: 0.2.1 + dev: true + + /esbuild-android-64@0.14.54: + resolution: {integrity: sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-android-arm64@0.14.54: + resolution: {integrity: sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-64@0.14.54: + resolution: {integrity: sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-arm64@0.14.54: + resolution: {integrity: sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-64@0.14.54: + resolution: {integrity: sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-arm64@0.14.54: + resolution: {integrity: sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-32@0.14.54: + resolution: {integrity: sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-64@0.14.54: + resolution: {integrity: sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm64@0.14.54: + resolution: {integrity: sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm@0.14.54: + resolution: {integrity: sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-mips64le@0.14.54: + resolution: {integrity: sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-ppc64le@0.14.54: + resolution: {integrity: sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-riscv64@0.14.54: + resolution: {integrity: sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-s390x@0.14.54: + resolution: {integrity: sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-netbsd-64@0.14.54: + resolution: {integrity: sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-openbsd-64@0.14.54: + resolution: {integrity: sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-sunos-64@0.14.54: + resolution: {integrity: sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-32@0.14.54: + resolution: {integrity: sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-64@0.14.54: + resolution: {integrity: sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-arm64@0.14.54: + resolution: {integrity: sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild@0.14.54: + resolution: {integrity: sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/linux-loong64': 0.14.54 + esbuild-android-64: 0.14.54 + esbuild-android-arm64: 0.14.54 + esbuild-darwin-64: 0.14.54 + esbuild-darwin-arm64: 0.14.54 + esbuild-freebsd-64: 0.14.54 + esbuild-freebsd-arm64: 0.14.54 + esbuild-linux-32: 0.14.54 + esbuild-linux-64: 0.14.54 + esbuild-linux-arm: 0.14.54 + esbuild-linux-arm64: 0.14.54 + esbuild-linux-mips64le: 0.14.54 + esbuild-linux-ppc64le: 0.14.54 + esbuild-linux-riscv64: 0.14.54 + esbuild-linux-s390x: 0.14.54 + esbuild-netbsd-64: 0.14.54 + esbuild-openbsd-64: 0.14.54 + esbuild-sunos-64: 0.14.54 + esbuild-windows-32: 0.14.54 + esbuild-windows-64: 0.14.54 + esbuild-windows-arm64: 0.14.54 + dev: true + + /escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + dev: true + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=} + engines: {node: '>=0.8.0'} + dev: true + + /escodegen@2.0.0: + resolution: {integrity: sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==} + engines: {node: '>=6.0'} + hasBin: true + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionator: 0.8.3 + optionalDependencies: + source-map: 0.6.1 + dev: true + + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /estree-walker@1.0.1: + resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} + dev: true + + /estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: true + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + dev: true + + /execa@7.2.0: + resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==} + engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 4.3.1 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.1.0 + onetime: 6.0.0 + signal-exit: 3.0.7 + strip-final-newline: 3.0.0 + dev: true + + /fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.4 + dev: true + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fastq@1.13.0: + resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} + dependencies: + reusify: 1.0.4 + dev: true + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /find-cache-dir@3.3.2: + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} + dependencies: + commondir: 1.0.1 + make-dir: 3.1.0 + pkg-dir: 4.2.0 + dev: true + + /find-up@1.1.2: + resolution: {integrity: sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=} + engines: {node: '>=0.10.0'} + dependencies: + path-exists: 2.1.0 + pinkie-promise: 2.0.1 + dev: true + + /find-up@2.1.0: + resolution: {integrity: sha1-RdG35QbHF93UgndaK3eSCjwMV6c=} + engines: {node: '>=4'} + dependencies: + locate-path: 2.0.0 + dev: true + + /find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + dev: true + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + dev: true + + /foreground-child@3.1.1: + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + dev: true + + /form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: true + + /from2@2.3.0: + resolution: {integrity: sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=} + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.7 + dev: true + + /fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + dependencies: + graceful-fs: 4.2.9 + jsonfile: 6.1.0 + universalify: 2.0.0 + dev: true + + /fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.6 + dev: true + + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind@1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: true + + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: true + + /get-func-name@2.0.0: + resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} + dev: true + + /get-pkg-repo@1.4.0: + resolution: {integrity: sha1-xztInAbYDMVTbCyFP54FIyBWly0=} + hasBin: true + dependencies: + hosted-git-info: 2.8.9 + meow: 3.7.0 + normalize-package-data: 2.5.0 + parse-github-repo-url: 1.4.1 + through2: 2.0.5 + dev: true + + /get-pkg-repo@4.2.1: + resolution: {integrity: sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA==} + engines: {node: '>=6.9.0'} + hasBin: true + dependencies: + '@hutson/parse-repository-url': 3.0.2 + hosted-git-info: 4.1.0 + through2: 2.0.5 + yargs: 16.2.0 + dev: true + + /get-stdin@4.0.1: + resolution: {integrity: sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=} + engines: {node: '>=0.10.0'} + dev: true + + /get-stream@3.0.0: + resolution: {integrity: sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=} + engines: {node: '>=4'} + dev: true + + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + dev: true + + /gh-got@7.1.0: + resolution: {integrity: sha512-KeWkkhresa7sbpzQLYzITMgez5rMigUsijhmSAHcLDORIMUbdlkdoZyaN1wQvIjmUZnyb/wkAPaXb4MQKX0mdQ==} + engines: {node: '>=4'} + dependencies: + got: 8.3.2 + is-plain-obj: 1.1.0 + dev: true + + /giget@1.1.2: + resolution: {integrity: sha512-HsLoS07HiQ5oqvObOI+Qb2tyZH4Gj5nYGfF9qQcZNrPw+uEFhdXtgJr01aO2pWadGHucajYDLxxbtQkm97ON2A==} + hasBin: true + dependencies: + colorette: 2.0.20 + defu: 6.1.2 + https-proxy-agent: 5.0.1 + mri: 1.2.0 + node-fetch-native: 1.2.0 + pathe: 1.1.1 + tar: 6.1.15 + transitivePeerDependencies: + - supports-color + dev: true + + /git-raw-commits@2.0.0: + resolution: {integrity: sha512-w4jFEJFgKXMQJ0H0ikBk2S+4KP2VEjhCvLCNqbNRQC8BgGWgLKNCO7a9K9LI+TVT7Gfoloje502sEnctibffgg==} + engines: {node: '>=6.9.0'} + hasBin: true + dependencies: + dargs: 4.1.0 + lodash.template: 4.5.0 + meow: 4.0.1 + split2: 2.2.0 + through2: 2.0.5 + dev: true + + /git-raw-commits@2.0.11: + resolution: {integrity: sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==} + engines: {node: '>=10'} + hasBin: true + dependencies: + dargs: 7.0.0 + lodash: 4.17.21 + meow: 8.1.2 + split2: 3.2.2 + through2: 4.0.2 + dev: true + + /git-remote-origin-url@2.0.0: + resolution: {integrity: sha1-UoJlna4hBxRaERJhEq0yFuxfpl8=} + engines: {node: '>=4'} + dependencies: + gitconfiglocal: 1.0.0 + pify: 2.3.0 + dev: true + + /git-semver-tags@2.0.3: + resolution: {integrity: sha512-tj4FD4ww2RX2ae//jSrXZzrocla9db5h0V7ikPl1P/WwoZar9epdUhwR7XHXSgc+ZkNq72BEEerqQuicoEQfzA==} + engines: {node: '>=6.9.0'} + hasBin: true + dependencies: + meow: 4.0.1 + semver: 6.3.0 + dev: true + + /git-semver-tags@4.1.1: + resolution: {integrity: sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + meow: 8.1.2 + semver: 6.3.0 + dev: true + + /gitconfiglocal@1.0.0: + resolution: {integrity: sha1-QdBF84UaXqiPA/JMocYXgRRGS5s=} + dependencies: + ini: 1.3.8 + dev: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob@10.3.3: + resolution: {integrity: sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + foreground-child: 3.1.1 + jackspeak: 2.2.3 + minimatch: 9.0.3 + minipass: 7.0.3 + path-scurry: 1.10.1 + dev: true + + /got@8.3.2: + resolution: {integrity: sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw==} + engines: {node: '>=4'} + dependencies: + '@sindresorhus/is': 0.7.0 + '@types/keyv': 3.1.4 + '@types/responselike': 1.0.0 + cacheable-request: 2.1.4 + decompress-response: 3.3.0 + duplexer3: 0.1.4 + get-stream: 3.0.0 + into-stream: 3.1.0 + is-retry-allowed: 1.2.0 + isurl: 1.0.0 + lowercase-keys: 1.0.1 + mimic-response: 1.0.1 + p-cancelable: 0.4.1 + p-timeout: 2.0.1 + pify: 3.0.0 + safe-buffer: 5.2.1 + timed-out: 4.0.1 + url-parse-lax: 3.0.0 + url-to-options: 1.0.1 + dev: true + + /graceful-fs@4.2.9: + resolution: {integrity: sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==} + dev: true + + /handlebars@4.7.7: + resolution: {integrity: sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==} + engines: {node: '>=0.4.7'} + hasBin: true + dependencies: + minimist: 1.2.5 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.15.0 + dev: true + + /hard-rejection@2.1.0: + resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} + engines: {node: '>=6'} + dev: true + + /has-ansi@2.0.0: + resolution: {integrity: sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=} + engines: {node: '>=0.10.0'} + dependencies: + ansi-regex: 2.1.1 + dev: true + + /has-flag@3.0.0: + resolution: {integrity: sha1-tdRU3CGZriJWmfNGfloH87lVuv0=} + engines: {node: '>=4'} + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /has-symbol-support-x@1.4.2: + resolution: {integrity: sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==} + dev: true + + /has-to-string-tag-x@1.4.1: + resolution: {integrity: sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==} + dependencies: + has-symbol-support-x: 1.4.2 + dev: true + + /has@1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + dev: true + + /hash-sum@1.0.2: + resolution: {integrity: sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=} + dev: true + + /he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + dev: true + + /hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + dev: true + + /hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + dependencies: + lru-cache: 6.0.0 + dev: true + + /html-encoding-sniffer@3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + dependencies: + whatwg-encoding: 2.0.0 + dev: true + + /http-cache-semantics@3.8.1: + resolution: {integrity: sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==} + dev: true + + /http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /human-signals@4.3.1: + resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} + engines: {node: '>=14.18.0'} + dev: true + + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: true + + /indent-string@2.1.0: + resolution: {integrity: sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=} + engines: {node: '>=0.10.0'} + dependencies: + repeating: 2.0.1 + dev: true + + /indent-string@3.2.0: + resolution: {integrity: sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=} + engines: {node: '>=4'} + dev: true + + /indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + dev: true + + /into-stream@3.1.0: + resolution: {integrity: sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY=} + engines: {node: '>=4'} + dependencies: + from2: 2.3.0 + p-is-promise: 1.1.0 + dev: true + + /is-arrayish@0.2.1: + resolution: {integrity: sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=} + dev: true + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-builtin-module@3.1.0: + resolution: {integrity: sha512-OV7JjAgOTfAFJmHZLvpSTb4qi0nIILDV1gWPYDnDJUTNFM5aGlRAhk4QcT8i7TuAleeEV5Fdkqn3t4mS+Q11fg==} + engines: {node: '>=6'} + dependencies: + builtin-modules: 3.2.0 + dev: true + + /is-core-module@2.10.0: + resolution: {integrity: sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==} + dependencies: + has: 1.0.3 + dev: true + + /is-core-module@2.8.1: + resolution: {integrity: sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==} + dependencies: + has: 1.0.3 + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-finite@1.1.0: + resolution: {integrity: sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==} + engines: {node: '>=0.10.0'} + dev: true + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + + /is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-module@1.0.0: + resolution: {integrity: sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=} + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-obj@1.0.1: + resolution: {integrity: sha1-PkcprB9f3gJc19g6iW2rn09n2w8=} + engines: {node: '>=0.10.0'} + dev: true + + /is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + dev: true + + /is-object@1.0.2: + resolution: {integrity: sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==} + dev: true + + /is-plain-obj@1.1.0: + resolution: {integrity: sha1-caUMhCnfync8kqOQpKA7OfzVHT4=} + engines: {node: '>=0.10.0'} + dev: true + + /is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + dev: true + + /is-retry-allowed@1.2.0: + resolution: {integrity: sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==} + engines: {node: '>=0.10.0'} + dev: true + + /is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /is-text-path@1.0.1: + resolution: {integrity: sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4=} + engines: {node: '>=0.10.0'} + dependencies: + text-extensions: 1.9.0 + dev: true + + /is-utf8@0.2.1: + resolution: {integrity: sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=} + dev: true + + /isarray@1.0.0: + resolution: {integrity: sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /isurl@1.0.0: + resolution: {integrity: sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==} + engines: {node: '>= 4'} + dependencies: + has-to-string-tag-x: 1.4.1 + is-object: 1.0.2 + dev: true + + /jackspeak@2.2.3: + resolution: {integrity: sha512-pF0kfjmg8DJLxDrizHoCZGUFz4P4czQ3HyfW4BU0ffebYkzAVlBywp5zaxW/TM+r0sGbmrQdi8EQQVTJFxnGsQ==} + engines: {node: '>=14'} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: true + + /jest-worker@26.6.2: + resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/node': 17.0.31 + merge-stream: 2.0.0 + supports-color: 7.2.0 + dev: true + + /jiti@1.19.1: + resolution: {integrity: sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg==} + hasBin: true + dev: true + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: true + + /jsdom@20.0.0: + resolution: {integrity: sha512-x4a6CKCgx00uCmP+QakBDFXwjAJ69IkkIWHmtmjd3wvXPcdOS44hfX2vqkOQrVrq8l9DhNNADZRXaCEWvgXtVA==} + engines: {node: '>=14'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + abab: 2.0.6 + acorn: 8.8.0 + acorn-globals: 6.0.0 + cssom: 0.5.0 + cssstyle: 2.3.0 + data-urls: 3.0.2 + decimal.js: 10.4.0 + domexception: 4.0.0 + escodegen: 2.0.0 + form-data: 4.0.0 + html-encoding-sniffer: 3.0.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.1 + parse5: 7.0.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.0.0 + w3c-hr-time: 1.0.2 + w3c-xmlserializer: 3.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + ws: 8.8.1 + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /json-buffer@3.0.0: + resolution: {integrity: sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=} + dev: true + + /json-parse-better-errors@1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + dev: true + + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: true + + /json-stringify-safe@5.0.1: + resolution: {integrity: sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=} + dev: true + + /jsonc-parser@3.2.0: + resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + dev: true + + /jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.0 + optionalDependencies: + graceful-fs: 4.2.9 + dev: true + + /jsonparse@1.3.1: + resolution: {integrity: sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=} + engines: {'0': node >= 0.2.0} + dev: true + + /keyv@3.0.0: + resolution: {integrity: sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==} + dependencies: + json-buffer: 3.0.0 + dev: true + + /kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + dev: true + + /kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + dev: true + + /levn@0.3.0: + resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.1.2 + type-check: 0.3.2 + dev: true + + /lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + dev: true + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: true + + /lint-staged@14.0.0: + resolution: {integrity: sha512-0tLf0pqZYkar/wu3nTctk4rVIG+d7PanDYv4/IQR4qwdqfQkTDziLRFnqMcLuLBTuUqmcLwsHPD2EjQ18d/oaA==} + engines: {node: ^16.14.0 || >=18.0.0} + hasBin: true + dependencies: + chalk: 5.3.0 + commander: 11.0.0 + debug: 4.3.4 + execa: 7.2.0 + lilconfig: 2.1.0 + listr2: 6.6.1 + micromatch: 4.0.5 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.3.1 + transitivePeerDependencies: + - enquirer + - supports-color + dev: true + + /listr2@6.6.1: + resolution: {integrity: sha512-+rAXGHh0fkEWdXBmX+L6mmfmXmXvDGEKzkjxO+8mP3+nI/r/CWznVBvsibXdxda9Zz0OW2e2ikphN3OwCT/jSg==} + engines: {node: '>=16.0.0'} + peerDependencies: + enquirer: '>= 2.3.0 < 3' + peerDependenciesMeta: + enquirer: + optional: true + dependencies: + cli-truncate: 3.1.0 + colorette: 2.0.20 + eventemitter3: 5.0.1 + log-update: 5.0.1 + rfdc: 1.3.0 + wrap-ansi: 8.1.0 + dev: true + + /load-json-file@1.1.0: + resolution: {integrity: sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=} + engines: {node: '>=0.10.0'} + dependencies: + graceful-fs: 4.2.9 + parse-json: 2.2.0 + pify: 2.3.0 + pinkie-promise: 2.0.1 + strip-bom: 2.0.0 + dev: true + + /load-json-file@4.0.0: + resolution: {integrity: sha1-L19Fq5HjMhYjT9U62rZo607AmTs=} + engines: {node: '>=4'} + dependencies: + graceful-fs: 4.2.9 + parse-json: 4.0.0 + pify: 3.0.0 + strip-bom: 3.0.0 + dev: true + + /local-pkg@0.4.3: + resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} + engines: {node: '>=14'} + dev: true + + /locate-path@2.0.0: + resolution: {integrity: sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=} + engines: {node: '>=4'} + dependencies: + p-locate: 2.0.0 + path-exists: 3.0.0 + dev: true + + /locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + dependencies: + p-locate: 4.1.0 + dev: true + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /lodash._reinterpolate@3.0.0: + resolution: {integrity: sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=} + dev: true + + /lodash.ismatch@4.4.0: + resolution: {integrity: sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=} + dev: true + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /lodash.template@4.5.0: + resolution: {integrity: sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==} + dependencies: + lodash._reinterpolate: 3.0.0 + lodash.templatesettings: 4.2.0 + dev: true + + /lodash.templatesettings@4.2.0: + resolution: {integrity: sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==} + dependencies: + lodash._reinterpolate: 3.0.0 + dev: true + + /lodash.uniq@4.5.0: + resolution: {integrity: sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=} + dev: true + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: true + + /log-update@5.0.1: + resolution: {integrity: sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + ansi-escapes: 5.0.0 + cli-cursor: 4.0.0 + slice-ansi: 5.0.0 + strip-ansi: 7.0.1 + wrap-ansi: 8.1.0 + dev: true + + /loud-rejection@1.6.0: + resolution: {integrity: sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=} + engines: {node: '>=0.10.0'} + dependencies: + currently-unhandled: 0.4.1 + signal-exit: 3.0.7 + dev: true + + /loupe@2.3.4: + resolution: {integrity: sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==} + dependencies: + get-func-name: 2.0.0 + dev: true + + /loupe@2.3.6: + resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} + dependencies: + get-func-name: 2.0.0 + dev: true + + /lowercase-keys@1.0.0: + resolution: {integrity: sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=} + engines: {node: '>=0.10.0'} + dev: true + + /lowercase-keys@1.0.1: + resolution: {integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==} + engines: {node: '>=0.10.0'} + dev: true + + /lru-cache@10.0.1: + resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==} + engines: {node: 14 || >=16.14} + dev: true + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: true + + /magic-string@0.25.7: + resolution: {integrity: sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==} + dependencies: + sourcemap-codec: 1.4.8 + dev: true + + /magic-string@0.26.1: + resolution: {integrity: sha512-ndThHmvgtieXe8J/VGPjG+Apu7v7ItcD5mhEIvOscWjPF/ccOiLxHaSuCAS2G+3x4GKsAbT8u7zdyamupui8Tg==} + engines: {node: '>=12'} + dependencies: + sourcemap-codec: 1.4.8 + dev: true + + /magic-string@0.30.2: + resolution: {integrity: sha512-lNZdu7pewtq/ZvWUp9Wpf/x7WzMTsR26TWV03BRZrXFsv+BI6dy8RAiKgm1uM/kyR0rCfUcqvOlXKG66KhIGug==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + dependencies: + semver: 6.3.0 + dev: true + + /map-obj@1.0.1: + resolution: {integrity: sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=} + engines: {node: '>=0.10.0'} + dev: true + + /map-obj@2.0.0: + resolution: {integrity: sha1-plzSkIepJZi4eRJXpSPgISIqwfk=} + engines: {node: '>=4'} + dev: true + + /map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + dev: true + + /meow@3.7.0: + resolution: {integrity: sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=} + engines: {node: '>=0.10.0'} + dependencies: + camelcase-keys: 2.1.0 + decamelize: 1.2.0 + loud-rejection: 1.6.0 + map-obj: 1.0.1 + minimist: 1.2.5 + normalize-package-data: 2.5.0 + object-assign: 4.1.1 + read-pkg-up: 1.0.1 + redent: 1.0.0 + trim-newlines: 1.0.0 + dev: true + + /meow@4.0.1: + resolution: {integrity: sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==} + engines: {node: '>=4'} + dependencies: + camelcase-keys: 4.2.0 + decamelize-keys: 1.1.0 + loud-rejection: 1.6.0 + minimist: 1.2.5 + minimist-options: 3.0.2 + normalize-package-data: 2.5.0 + read-pkg-up: 3.0.0 + redent: 2.0.0 + trim-newlines: 2.0.0 + dev: true + + /meow@7.1.1: + resolution: {integrity: sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA==} + engines: {node: '>=10'} + dependencies: + '@types/minimist': 1.2.2 + camelcase-keys: 6.2.2 + decamelize-keys: 1.1.0 + hard-rejection: 2.1.0 + minimist-options: 4.1.0 + normalize-package-data: 2.5.0 + read-pkg-up: 7.0.1 + redent: 3.0.0 + trim-newlines: 3.0.1 + type-fest: 0.13.1 + yargs-parser: 18.1.3 + dev: true + + /meow@8.1.2: + resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==} + engines: {node: '>=10'} + dependencies: + '@types/minimist': 1.2.2 + camelcase-keys: 6.2.2 + decamelize-keys: 1.1.0 + hard-rejection: 2.1.0 + minimist-options: 4.1.0 + normalize-package-data: 3.0.3 + read-pkg-up: 7.0.1 + redent: 3.0.0 + trim-newlines: 3.0.1 + type-fest: 0.18.1 + yargs-parser: 20.2.9 + dev: true + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch@4.0.4: + resolution: {integrity: sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: true + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: true + + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + dev: true + + /mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + dev: true + + /mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + dev: true + + /min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + dev: true + + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimist-options@3.0.2: + resolution: {integrity: sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==} + engines: {node: '>= 4'} + dependencies: + arrify: 1.0.1 + is-plain-obj: 1.1.0 + dev: true + + /minimist-options@4.1.0: + resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} + engines: {node: '>= 6'} + dependencies: + arrify: 1.0.1 + is-plain-obj: 1.1.0 + kind-of: 6.0.3 + dev: true + + /minimist@1.2.5: + resolution: {integrity: sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==} + dev: true + + /minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + dependencies: + yallist: 4.0.0 + dev: true + + /minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + dev: true + + /minipass@7.0.3: + resolution: {integrity: sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==} + engines: {node: '>=16 || 14 >=14.17'} + dev: true + + /minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + dev: true + + /mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + dev: true + + /mlly@1.4.0: + resolution: {integrity: sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==} + dependencies: + acorn: 8.10.0 + pathe: 1.1.1 + pkg-types: 1.0.3 + ufo: 1.2.0 + dev: true + + /modify-values@1.0.1: + resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==} + engines: {node: '>=0.10.0'} + dev: true + + /mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + dev: true + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: true + + /nanoid@3.3.4: + resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + dev: true + + /node-fetch-native@1.2.0: + resolution: {integrity: sha512-5IAMBTl9p6PaAjYCnMv5FmqIF6GcZnawAVnzaCG0rX2aYZJ4CxEkZNtVPuTRug7fL7wyM5BQYTlAzcyMPi6oTQ==} + dev: true + + /normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + dependencies: + hosted-git-info: 2.8.9 + resolve: 1.22.0 + semver: 5.7.1 + validate-npm-package-license: 3.0.4 + dev: true + + /normalize-package-data@3.0.3: + resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} + engines: {node: '>=10'} + dependencies: + hosted-git-info: 4.1.0 + is-core-module: 2.8.1 + semver: 7.3.5 + validate-npm-package-license: 3.0.4 + dev: true + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /normalize-url@2.0.1: + resolution: {integrity: sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==} + engines: {node: '>=4'} + dependencies: + prepend-http: 2.0.0 + query-string: 5.1.1 + sort-keys: 2.0.0 + dev: true + + /npm-run-path@5.1.0: + resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + path-key: 4.0.0 + dev: true + + /number-is-nan@1.0.1: + resolution: {integrity: sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=} + engines: {node: '>=0.10.0'} + dev: true + + /nwsapi@2.2.1: + resolution: {integrity: sha512-JYOWTeFoS0Z93587vRJgASD5Ut11fYl5NyihP3KrYBvMe1FRRs6RN7m20SA/16GM4P6hTnZjT+UmDOt38UeXNg==} + dev: true + + /object-assign@4.1.1: + resolution: {integrity: sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=} + engines: {node: '>=0.10.0'} + dev: true + + /ohash@1.1.3: + resolution: {integrity: sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==} + dev: true + + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + dev: true + + /onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + dependencies: + mimic-fn: 4.0.0 + dev: true + + /optionator@0.8.3: + resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} + engines: {node: '>= 0.8.0'} + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.3.0 + prelude-ls: 1.1.2 + type-check: 0.3.2 + word-wrap: 1.2.3 + dev: true + + /p-cancelable@0.4.1: + resolution: {integrity: sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==} + engines: {node: '>=4'} + dev: true + + /p-finally@1.0.0: + resolution: {integrity: sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=} + engines: {node: '>=4'} + dev: true + + /p-is-promise@1.1.0: + resolution: {integrity: sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=} + engines: {node: '>=4'} + dev: true + + /p-limit@1.3.0: + resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} + engines: {node: '>=4'} + dependencies: + p-try: 1.0.0 + dev: true + + /p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + dev: true + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + yocto-queue: 1.0.0 + dev: true + + /p-locate@2.0.0: + resolution: {integrity: sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=} + engines: {node: '>=4'} + dependencies: + p-limit: 1.3.0 + dev: true + + /p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + dependencies: + p-limit: 2.3.0 + dev: true + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /p-timeout@2.0.1: + resolution: {integrity: sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==} + engines: {node: '>=4'} + dependencies: + p-finally: 1.0.0 + dev: true + + /p-try@1.0.0: + resolution: {integrity: sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=} + engines: {node: '>=4'} + dev: true + + /p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + dev: true + + /parse-github-repo-url@1.4.1: + resolution: {integrity: sha1-nn2LslKmy2ukJZUGC3v23z28H1A=} + dev: true + + /parse-json@2.2.0: + resolution: {integrity: sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=} + engines: {node: '>=0.10.0'} + dependencies: + error-ex: 1.3.2 + dev: true + + /parse-json@4.0.0: + resolution: {integrity: sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=} + engines: {node: '>=4'} + dependencies: + error-ex: 1.3.2 + json-parse-better-errors: 1.0.2 + dev: true + + /parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.16.7 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + dev: true + + /parse5@7.0.0: + resolution: {integrity: sha512-y/t8IXSPWTuRZqXc0ajH/UwDj4mnqLEbSttNbThcFhGrZuOyoyvNBO85PBp2jQa55wY9d07PBNjsK8ZP3K5U6g==} + dependencies: + entities: 4.3.1 + dev: true + + /path-exists@2.1.0: + resolution: {integrity: sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=} + engines: {node: '>=0.10.0'} + dependencies: + pinkie-promise: 2.0.1 + dev: true + + /path-exists@3.0.0: + resolution: {integrity: sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=} + engines: {node: '>=4'} + dev: true + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-is-network-drive@1.0.13: + resolution: {integrity: sha512-Hg74mRN6mmXV+gTm3INjFK40ncAmC/Lo4qoQaSZ+GT3hZzlKdWQSqAjqyPeW0SvObP2W073WyYEBWY9d3wOm3A==} + dependencies: + tslib: 2.4.0 + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + dev: true + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /path-scurry@1.10.1: + resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + lru-cache: 10.0.1 + minipass: 7.0.3 + dev: true + + /path-strip-sep@1.0.10: + resolution: {integrity: sha512-JpCy+8LAJQQTO1bQsb/84s1g+/Stm3h39aOpPRBQ/paMUGVPPZChLTOTKHoaCkc/6sKuF7yVsnq5Pe1S6xQGcA==} + dependencies: + tslib: 2.4.0 + dev: true + + /path-type@1.1.0: + resolution: {integrity: sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=} + engines: {node: '>=0.10.0'} + dependencies: + graceful-fs: 4.2.9 + pify: 2.3.0 + pinkie-promise: 2.0.1 + dev: true + + /path-type@3.0.0: + resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} + engines: {node: '>=4'} + dependencies: + pify: 3.0.0 + dev: true + + /pathe@1.1.1: + resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==} + dev: true + + /pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + dev: true + + /perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + dev: true + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + dev: true + + /pify@2.3.0: + resolution: {integrity: sha1-7RQaasBDqEnqWISY59yosVMw6Qw=} + engines: {node: '>=0.10.0'} + dev: true + + /pify@3.0.0: + resolution: {integrity: sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=} + engines: {node: '>=4'} + dev: true + + /pinkie-promise@2.0.1: + resolution: {integrity: sha1-ITXW36ejWMBprJsXh3YogihFD/o=} + engines: {node: '>=0.10.0'} + dependencies: + pinkie: 2.0.4 + dev: true + + /pinkie@2.0.4: + resolution: {integrity: sha1-clVrgM+g1IqXToDnckjoDtT3+HA=} + engines: {node: '>=0.10.0'} + dev: true + + /pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + dependencies: + find-up: 4.1.0 + dev: true + + /pkg-dir@5.0.0: + resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==} + engines: {node: '>=10'} + dependencies: + find-up: 5.0.0 + dev: true + + /pkg-types@1.0.3: + resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} + dependencies: + jsonc-parser: 3.2.0 + mlly: 1.4.0 + pathe: 1.1.1 + dev: true + + /postcss@8.4.16: + resolution: {integrity: sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.4 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + + /prelude-ls@1.1.2: + resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} + engines: {node: '>= 0.8.0'} + dev: true + + /prepend-http@2.0.0: + resolution: {integrity: sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=} + engines: {node: '>=4'} + dev: true + + /prettier@3.0.1: + resolution: {integrity: sha512-fcOWSnnpCrovBsmFZIGIy9UqK2FaI7Hqax+DIO0A9UxeVoY4iweyaFjS5TavZN97Hfehph0nhsZnjlVKzEQSrQ==} + engines: {node: '>=14'} + hasBin: true + dev: true + + /pretty-format@29.6.2: + resolution: {integrity: sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.0 + ansi-styles: 5.2.0 + react-is: 18.2.0 + dev: true + + /process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + dev: true + + /prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + dev: true + + /psl@1.9.0: + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + dev: true + + /punycode@2.1.1: + resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} + engines: {node: '>=6'} + dev: true + + /q@1.5.1: + resolution: {integrity: sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=} + engines: {node: '>=0.6.0', teleport: '>=0.2.0'} + dev: true + + /query-string@5.1.1: + resolution: {integrity: sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==} + engines: {node: '>=0.10.0'} + dependencies: + decode-uri-component: 0.2.0 + object-assign: 4.1.1 + strict-uri-encode: 1.1.0 + dev: true + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /quick-lru@1.1.0: + resolution: {integrity: sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=} + engines: {node: '>=4'} + dev: true + + /quick-lru@4.0.1: + resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} + engines: {node: '>=8'} + dev: true + + /randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /rc9@2.1.1: + resolution: {integrity: sha512-lNeOl38Ws0eNxpO3+wD1I9rkHGQyj1NU1jlzv4go2CtEnEQEUfqnIvZG7W+bC/aXdJ27n5x/yUjb6RoT9tko+Q==} + dependencies: + defu: 6.1.2 + destr: 2.0.1 + flat: 5.0.2 + dev: true + + /react-is@18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + dev: true + + /read-pkg-up@1.0.1: + resolution: {integrity: sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=} + engines: {node: '>=0.10.0'} + dependencies: + find-up: 1.1.2 + read-pkg: 1.1.0 + dev: true + + /read-pkg-up@3.0.0: + resolution: {integrity: sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=} + engines: {node: '>=4'} + dependencies: + find-up: 2.1.0 + read-pkg: 3.0.0 + dev: true + + /read-pkg-up@7.0.1: + resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} + engines: {node: '>=8'} + dependencies: + find-up: 4.1.0 + read-pkg: 5.2.0 + type-fest: 0.8.1 + dev: true + + /read-pkg@1.1.0: + resolution: {integrity: sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=} + engines: {node: '>=0.10.0'} + dependencies: + load-json-file: 1.1.0 + normalize-package-data: 2.5.0 + path-type: 1.1.0 + dev: true + + /read-pkg@3.0.0: + resolution: {integrity: sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=} + engines: {node: '>=4'} + dependencies: + load-json-file: 4.0.0 + normalize-package-data: 2.5.0 + path-type: 3.0.0 + dev: true + + /read-pkg@5.2.0: + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} + engines: {node: '>=8'} + dependencies: + '@types/normalize-package-data': 2.4.1 + normalize-package-data: 2.5.0 + parse-json: 5.2.0 + type-fest: 0.6.0 + dev: true + + /readable-stream@2.3.7: + resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==} + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + dev: true + + /readable-stream@3.6.0: + resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: true + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /redent@1.0.0: + resolution: {integrity: sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=} + engines: {node: '>=0.10.0'} + dependencies: + indent-string: 2.1.0 + strip-indent: 1.0.1 + dev: true + + /redent@2.0.0: + resolution: {integrity: sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=} + engines: {node: '>=4'} + dependencies: + indent-string: 3.2.0 + strip-indent: 2.0.0 + dev: true + + /redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + dev: true + + /repeating@2.0.1: + resolution: {integrity: sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=} + engines: {node: '>=0.10.0'} + dependencies: + is-finite: 1.1.0 + dev: true + + /require-directory@2.1.1: + resolution: {integrity: sha1-jGStX9MNqxyXbiNE/+f3kqam30I=} + engines: {node: '>=0.10.0'} + dev: true + + /resolve@1.22.0: + resolution: {integrity: sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==} + hasBin: true + dependencies: + is-core-module: 2.8.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /resolve@1.22.1: + resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} + hasBin: true + dependencies: + is-core-module: 2.10.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /responselike@1.0.2: + resolution: {integrity: sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=} + dependencies: + lowercase-keys: 1.0.1 + dev: true + + /restore-cursor@4.0.0: + resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rfdc@1.3.0: + resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} + dev: true + + /rimraf@5.0.1: + resolution: {integrity: sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg==} + engines: {node: '>=14'} + hasBin: true + dependencies: + glob: 10.3.3 + dev: true + + /rollup-plugin-dts@4.2.1(rollup@2.72.0)(typescript@4.6.4): + resolution: {integrity: sha512-eaxQZNUJ5iQcxNGlpJ1CUgG4OSVqWjDZ3nNSWBIoGrpcote2aNphSe1RJOaSYkb8dwn3o+rYm1vvld/5z3EGSQ==} + engines: {node: '>=v12.22.11'} + peerDependencies: + rollup: ^2.70 + typescript: ^4.6 + dependencies: + magic-string: 0.26.1 + rollup: 2.72.0 + typescript: 4.6.4 + optionalDependencies: + '@babel/code-frame': 7.16.7 + dev: true + + /rollup-plugin-terser@7.0.2(rollup@2.72.0): + resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==} + peerDependencies: + rollup: ^2.0.0 + dependencies: + '@babel/code-frame': 7.16.7 + jest-worker: 26.6.2 + rollup: 2.72.0 + serialize-javascript: 4.0.0 + terser: 5.10.0 + transitivePeerDependencies: + - acorn + dev: true + + /rollup-plugin-typescript2@0.31.2(@types/bluebird@3.5.38)(@types/node@17.0.31)(rollup@2.72.0)(ts-toolbelt@9.6.0)(typescript@4.6.4): + resolution: {integrity: sha512-hRwEYR1C8xDGVVMFJQdEVnNAeWRvpaY97g5mp3IeLnzhNXzSVq78Ye/BJ9PAaUfN4DXa/uDnqerifMOaMFY54Q==} + peerDependencies: + rollup: '>=1.26.3' + typescript: '>=2.4.0' + dependencies: + '@rollup/pluginutils': 4.1.2 + '@yarn-tool/resolve-package': 1.0.42(@types/bluebird@3.5.38)(@types/node@17.0.31)(ts-toolbelt@9.6.0) + find-cache-dir: 3.3.2 + fs-extra: 10.1.0 + resolve: 1.22.0 + rollup: 2.72.0 + tslib: 2.4.0 + typescript: 4.6.4 + transitivePeerDependencies: + - '@types/bluebird' + - '@types/node' + - ts-toolbelt + dev: true + + /rollup@2.72.0: + resolution: {integrity: sha512-KqtR2YcO35/KKijg4nx4STO3569aqCUeGRkKWnJ6r+AvBBrVY9L4pmf4NHVrQr4mTOq6msbohflxr2kpihhaOA==} + engines: {node: '>=10.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /rollup@2.77.3: + resolution: {integrity: sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==} + engines: {node: '>=10.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + dev: true + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: true + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: true + + /saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + dependencies: + xmlchars: 2.2.0 + dev: true + + /semver-regex@2.0.0: + resolution: {integrity: sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==} + engines: {node: '>=6'} + dev: true + + /semver@5.7.1: + resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} + hasBin: true + dev: true + + /semver@6.3.0: + resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} + hasBin: true + dev: true + + /semver@7.3.5: + resolution: {integrity: sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + + /serialize-javascript@3.1.0: + resolution: {integrity: sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==} + dependencies: + randombytes: 2.1.0 + dev: true + + /serialize-javascript@4.0.0: + resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==} + dependencies: + randombytes: 2.1.0 + dev: true + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + dev: true + + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: true + + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + + /simple-git-hooks@2.9.0: + resolution: {integrity: sha512-waSQ5paUQtyGC0ZxlHmcMmD9I1rRXauikBwX31bX58l5vTOhCEcBC5Bi+ZDkPXTjDnZAF8TbCqKBY+9+sVPScw==} + hasBin: true + requiresBuild: true + dev: true + + /sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + dev: true + + /slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.1.0 + is-fullwidth-code-point: 4.0.0 + dev: true + + /sort-keys@2.0.0: + resolution: {integrity: sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=} + engines: {node: '>=4'} + dependencies: + is-plain-obj: 1.1.0 + dev: true + + /source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map@0.5.6: + resolution: {integrity: sha1-dc449SvwczxafwwRjYEzSiu19BI=} + engines: {node: '>=0.10.0'} + dev: true + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map@0.7.3: + resolution: {integrity: sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==} + engines: {node: '>= 8'} + dev: true + + /sourcemap-codec@1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + dev: true + + /spdx-correct@3.1.1: + resolution: {integrity: sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==} + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.11 + dev: true + + /spdx-exceptions@2.3.0: + resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} + dev: true + + /spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + dependencies: + spdx-exceptions: 2.3.0 + spdx-license-ids: 3.0.11 + dev: true + + /spdx-license-ids@3.0.11: + resolution: {integrity: sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==} + dev: true + + /split2@2.2.0: + resolution: {integrity: sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==} + dependencies: + through2: 2.0.5 + dev: true + + /split2@3.2.2: + resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} + dependencies: + readable-stream: 3.6.0 + dev: true + + /split@1.0.1: + resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} + dependencies: + through: 2.3.8 + dev: true + + /stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + dev: true + + /std-env@3.3.3: + resolution: {integrity: sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==} + dev: true + + /strict-uri-encode@1.1.0: + resolution: {integrity: sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=} + engines: {node: '>=0.10.0'} + dev: true + + /string-argv@0.3.1: + resolution: {integrity: sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==} + engines: {node: '>=0.6.19'} + dev: true + + /string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + dev: true + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.0.1 + dev: true + + /string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + dependencies: + safe-buffer: 5.1.2 + dev: true + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /strip-ansi@3.0.1: + resolution: {integrity: sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=} + engines: {node: '>=0.10.0'} + dependencies: + ansi-regex: 2.1.1 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-ansi@7.0.1: + resolution: {integrity: sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: true + + /strip-bom@2.0.0: + resolution: {integrity: sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=} + engines: {node: '>=0.10.0'} + dependencies: + is-utf8: 0.2.1 + dev: true + + /strip-bom@3.0.0: + resolution: {integrity: sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=} + engines: {node: '>=4'} + dev: true + + /strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + dev: true + + /strip-indent@1.0.1: + resolution: {integrity: sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=} + engines: {node: '>=0.10.0'} + hasBin: true + dependencies: + get-stdin: 4.0.1 + dev: true + + /strip-indent@2.0.0: + resolution: {integrity: sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=} + engines: {node: '>=4'} + dev: true + + /strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + dependencies: + min-indent: 1.0.1 + dev: true + + /strip-literal@1.3.0: + resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==} + dependencies: + acorn: 8.10.0 + dev: true + + /supports-color@2.0.0: + resolution: {integrity: sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=} + engines: {node: '>=0.8.0'} + dev: true + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + dev: true + + /tar@6.1.15: + resolution: {integrity: sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==} + engines: {node: '>=10'} + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + dev: true + + /temp-dir@2.0.0: + resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} + engines: {node: '>=8'} + dev: true + + /tempfile@3.0.0: + resolution: {integrity: sha512-uNFCg478XovRi85iD42egu+eSFUmmka750Jy7L5tfHI5hQKKtbPnxaSaXAbBqCDYrw3wx4tXjKwci4/QmsZJxw==} + engines: {node: '>=8'} + dependencies: + temp-dir: 2.0.0 + uuid: 3.4.0 + dev: true + + /terser@5.10.0: + resolution: {integrity: sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==} + engines: {node: '>=10'} + hasBin: true + peerDependencies: + acorn: ^8.5.0 + peerDependenciesMeta: + acorn: + optional: true + dependencies: + commander: 2.20.3 + source-map: 0.7.3 + source-map-support: 0.5.21 + dev: true + + /text-extensions@1.9.0: + resolution: {integrity: sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==} + engines: {node: '>=0.10'} + dev: true + + /through2@2.0.5: + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + dependencies: + readable-stream: 2.3.7 + xtend: 4.0.2 + dev: true + + /through2@3.0.2: + resolution: {integrity: sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==} + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.0 + dev: true + + /through2@4.0.2: + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + dependencies: + readable-stream: 3.6.0 + dev: true + + /through@2.3.8: + resolution: {integrity: sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=} + dev: true + + /timed-out@4.0.1: + resolution: {integrity: sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=} + engines: {node: '>=0.10.0'} + dev: true + + /tinybench@2.5.0: + resolution: {integrity: sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==} + dev: true + + /tinypool@0.7.0: + resolution: {integrity: sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==} + engines: {node: '>=14.0.0'} + dev: true + + /tinyspy@2.1.1: + resolution: {integrity: sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==} + engines: {node: '>=14.0.0'} + dev: true + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /tough-cookie@4.0.0: + resolution: {integrity: sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==} + engines: {node: '>=6'} + dependencies: + psl: 1.9.0 + punycode: 2.1.1 + universalify: 0.1.2 + dev: true + + /tr46@3.0.0: + resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} + engines: {node: '>=12'} + dependencies: + punycode: 2.1.1 + dev: true + + /trim-newlines@1.0.0: + resolution: {integrity: sha1-WIeWa7WCpFA6QetST301ARgVphM=} + engines: {node: '>=0.10.0'} + dev: true + + /trim-newlines@2.0.0: + resolution: {integrity: sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=} + engines: {node: '>=4'} + dev: true + + /trim-newlines@3.0.1: + resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} + engines: {node: '>=8'} + dev: true + + /ts-toolbelt@9.6.0: + resolution: {integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==} + dev: true + + /ts-type@2.1.4(@types/bluebird@3.5.38)(@types/node@17.0.31)(ts-toolbelt@9.6.0): + resolution: {integrity: sha512-wnajiiIMhn/RHJ1oPld95siKmMJrOgaT6+rMmC8vO1LORgDFEzKP2nBmEFM5b4XVe7Q0J5KcU9oRJFzju7UzrA==} + peerDependencies: + '@types/bluebird': '*' + '@types/node': '*' + ts-toolbelt: ^9.6.0 + dependencies: + '@types/bluebird': 3.5.38 + '@types/node': 17.0.31 + ts-toolbelt: 9.6.0 + tslib: 2.4.0 + typedarray-dts: 1.0.0 + dev: true + + /tslib@2.4.0: + resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} + dev: true + + /type-check@0.3.2: + resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.1.2 + dev: true + + /type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + dev: true + + /type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + dev: true + + /type-fest@0.18.1: + resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} + engines: {node: '>=10'} + dev: true + + /type-fest@0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} + dev: true + + /type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + dev: true + + /type-fest@1.4.0: + resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} + engines: {node: '>=10'} + dev: true + + /typedarray-dts@1.0.0: + resolution: {integrity: sha512-Ka0DBegjuV9IPYFT1h0Qqk5U4pccebNIJCGl8C5uU7xtOs+jpJvKGAY4fHGK25hTmXZOEUl9Cnsg5cS6K/b5DA==} + dev: true + + /typescript@4.6.4: + resolution: {integrity: sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + + /ufo@1.2.0: + resolution: {integrity: sha512-RsPyTbqORDNDxqAdQPQBpgqhWle1VcTSou/FraClYlHf6TZnQcGslpLcAphNR+sQW4q5lLWLbOsRlh9j24baQg==} + dev: true + + /uglify-js@3.15.0: + resolution: {integrity: sha512-x+xdeDWq7FiORDvyIJ0q/waWd4PhjBNOm5dQUOq2AKC0IEjxOS66Ha9tctiVDGcRQuh69K7fgU5oRuTK4cysSg==} + engines: {node: '>=0.8.0'} + hasBin: true + requiresBuild: true + dev: true + optional: true + + /universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + dev: true + + /universalify@2.0.0: + resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} + engines: {node: '>= 10.0.0'} + dev: true + + /upath2@3.1.12(@types/node@17.0.31): + resolution: {integrity: sha512-yC3eZeCyCXFWjy7Nu4pgjLhXNYjuzuUmJiRgSSw6TJp8Emc+E4951HGPJf+bldFC5SL7oBLeNbtm1fGzXn2gxw==} + peerDependencies: + '@types/node': '*' + dependencies: + '@types/node': 17.0.31 + path-is-network-drive: 1.0.13 + path-strip-sep: 1.0.10 + tslib: 2.4.0 + dev: true + + /url-parse-lax@3.0.0: + resolution: {integrity: sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=} + engines: {node: '>=4'} + dependencies: + prepend-http: 2.0.0 + dev: true + + /url-to-options@1.0.1: + resolution: {integrity: sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=} + engines: {node: '>= 4'} + dev: true + + /util-deprecate@1.0.2: + resolution: {integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=} + dev: true + + /uuid@3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + hasBin: true + dev: true + + /validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + dependencies: + spdx-correct: 3.1.1 + spdx-expression-parse: 3.0.1 + dev: true + + /vite-node@0.34.1: + resolution: {integrity: sha512-odAZAL9xFMuAg8aWd7nSPT+hU8u2r9gU3LRm9QKjxBEF2rRdWpMuqkrkjvyVQEdNFiBctqr2Gg4uJYizm5Le6w==} + engines: {node: '>=v14.18.0'} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4 + mlly: 1.4.0 + pathe: 1.1.1 + picocolors: 1.0.0 + vite: 3.0.8 + transitivePeerDependencies: + - less + - sass + - stylus + - supports-color + - terser + dev: true + + /vite@3.0.8: + resolution: {integrity: sha512-AOZ4eN7mrkJiOLuw8IA7piS4IdOQyQCA81GxGsAQvAZzMRi9ZwGB3TOaYsj4uLAWK46T5L4AfQ6InNGlxX30IQ==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + less: '*' + sass: '*' + stylus: '*' + terser: ^5.4.0 + peerDependenciesMeta: + less: + optional: true + sass: + optional: true + stylus: + optional: true + terser: + optional: true + dependencies: + esbuild: 0.14.54 + postcss: 8.4.16 + resolve: 1.22.1 + rollup: 2.77.3 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /vitest@0.34.1(jsdom@20.0.0): + resolution: {integrity: sha512-G1PzuBEq9A75XSU88yO5G4vPT20UovbC/2osB2KEuV/FisSIIsw7m5y2xMdB7RsAGHAfg2lPmp2qKr3KWliVlQ==} + engines: {node: '>=v14.18.0'} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@vitest/browser': '*' + '@vitest/ui': '*' + happy-dom: '*' + jsdom: '*' + playwright: '*' + safaridriver: '*' + webdriverio: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + playwright: + optional: true + safaridriver: + optional: true + webdriverio: + optional: true + dependencies: + '@types/chai': 4.3.5 + '@types/chai-subset': 1.3.3 + '@types/node': 17.0.31 + '@vitest/expect': 0.34.1 + '@vitest/runner': 0.34.1 + '@vitest/snapshot': 0.34.1 + '@vitest/spy': 0.34.1 + '@vitest/utils': 0.34.1 + acorn: 8.10.0 + acorn-walk: 8.2.0 + cac: 6.7.14 + chai: 4.3.7 + debug: 4.3.4 + jsdom: 20.0.0 + local-pkg: 0.4.3 + magic-string: 0.30.2 + pathe: 1.1.1 + picocolors: 1.0.0 + std-env: 3.3.3 + strip-literal: 1.3.0 + tinybench: 2.5.0 + tinypool: 0.7.0 + vite: 3.0.8 + vite-node: 0.34.1 + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - sass + - stylus + - supports-color + - terser + dev: true + + /vue-router@3.5.3(vue@2.6.14): + resolution: {integrity: sha512-FUlILrW3DGitS2h+Xaw8aRNvGTwtuaxrRkNSHWTizOfLUie7wuYwezeZ50iflRn8YPV5kxmU2LQuu3nM/b3Zsg==} + peerDependencies: + vue: ^2 + dependencies: + vue: 2.6.14 + dev: true + + /vue-server-renderer@2.6.14: + resolution: {integrity: sha512-HifYRa/LW7cKywg9gd4ZtvtRuBlstQBao5ZCWlg40fyB4OPoGfEXAzxb0emSLv4pBDOHYx0UjpqvxpiQFEuoLA==} + dependencies: + chalk: 1.1.3 + hash-sum: 1.0.2 + he: 1.2.0 + lodash.template: 4.5.0 + lodash.uniq: 4.5.0 + resolve: 1.22.0 + serialize-javascript: 3.1.0 + source-map: 0.5.6 + dev: true + + /vue@2.6.14: + resolution: {integrity: sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==} + dev: true + + /vue@3.2.21: + resolution: {integrity: sha512-jpy7ckXdyclfRzqLjL4mtq81AkzQleE54KjZsJg/9OorNVurAxdlU5XpD49GpjKdnftuffKUvx2C5jDOrgc/zg==} + dependencies: + '@vue/compiler-dom': 3.2.21 + '@vue/compiler-sfc': 3.2.21 + '@vue/runtime-dom': 3.2.21 + '@vue/server-renderer': 3.2.21(vue@2.6.14) + '@vue/shared': 3.2.21 + dev: true + + /w3c-hr-time@1.0.2: + resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} + dependencies: + browser-process-hrtime: 1.0.0 + dev: true + + /w3c-xmlserializer@3.0.0: + resolution: {integrity: sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==} + engines: {node: '>=12'} + dependencies: + xml-name-validator: 4.0.0 + dev: true + + /webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + dev: true + + /whatwg-encoding@2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + dependencies: + iconv-lite: 0.6.3 + dev: true + + /whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + dev: true + + /whatwg-url@11.0.0: + resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} + engines: {node: '>=12'} + dependencies: + tr46: 3.0.0 + webidl-conversions: 7.0.0 + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /why-is-node-running@2.2.2: + resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + dev: true + + /word-wrap@1.2.3: + resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} + engines: {node: '>=0.10.0'} + dev: true + + /wordwrap@1.0.0: + resolution: {integrity: sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=} + dev: true + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.1.0 + string-width: 5.1.2 + strip-ansi: 7.0.1 + dev: true + + /ws@8.8.1: + resolution: {integrity: sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: true + + /xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + dev: true + + /xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + dev: true + + /xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + dev: true + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: true + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: true + + /yaml@2.3.1: + resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} + engines: {node: '>= 14'} + dev: true + + /yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + dev: true + + /yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + dev: true + + /yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + dependencies: + cliui: 7.0.4 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + dev: true + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true + + /yocto-queue@1.0.0: + resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + engines: {node: '>=12.20'} + dev: true diff --git a/rollup.config.js b/rollup.config.js index 1164001d..4e8f4e3e 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,44 +1,56 @@ -import * as path from 'path'; -// import filesize from 'rollup-plugin-filesize'; -import typescript from 'rollup-plugin-typescript2'; -import resolve from 'rollup-plugin-node-resolve'; -import { terser } from 'rollup-plugin-terser'; -import replace from 'rollup-plugin-replace'; +import * as path from 'path' +import typescript from 'rollup-plugin-typescript2' +import resolve from '@rollup/plugin-node-resolve' +import { terser } from 'rollup-plugin-terser' +import replace from '@rollup/plugin-replace' +import dts from 'rollup-plugin-dts' +import { version } from './package.json' const builds = { 'cjs-dev': { - outFile: 'vue-function-api.js', + outFile: 'vue-composition-api.common.js', format: 'cjs', mode: 'development', }, 'cjs-prod': { - outFile: 'vue-function-api.min.js', + outFile: 'vue-composition-api.common.prod.js', format: 'cjs', mode: 'production', }, 'umd-dev': { - outFile: 'vue-function-api.umd.js', + outFile: 'vue-composition-api.js', format: 'umd', mode: 'development', }, 'umd-prod': { - outFile: 'vue-function-api.umd.min.js', + outFile: 'vue-composition-api.prod.js', format: 'umd', mode: 'production', }, - es: { - outFile: 'vue-function-api.module.js', + esm: { + outFile: 'vue-composition-api.esm.js', format: 'es', mode: 'development', }, -}; + mjs: { + outFile: 'vue-composition-api.mjs', + format: 'es', + mode: 'development', + }, +} + +function onwarn(msg, warn) { + if (!/Circular/.test(msg)) { + warn(msg) + } +} function getAllBuilds() { - return Object.keys(builds).map(key => genConfig(builds[key])); + return Object.keys(builds).map((key) => genConfig(builds[key])) } function genConfig({ outFile, format, mode }) { - const isProd = mode === 'production'; + const isProd = mode === 'production' return { input: './src/index.ts', output: { @@ -47,26 +59,59 @@ function genConfig({ outFile, format, mode }) { globals: { vue: 'Vue', }, - name: format === 'umd' ? 'vueFunctionApi' : undefined, + exports: 'named', + name: format === 'umd' ? 'VueCompositionAPI' : undefined, }, external: ['vue'], + onwarn, plugins: [ typescript({ - typescript: require('typescript'), + tsconfigOverride: { + declaration: false, + declarationDir: null, + emitDeclarationOnly: false, + }, + useTsconfigDeclarationDir: true, }), resolve(), - replace({ 'process.env.NODE_ENV': JSON.stringify(isProd ? 'production' : 'development') }), + replace({ + preventAssignment: true, + 'process.env.NODE_ENV': + format === 'es' + ? // preserve to be handled by bundlers + 'process.env.NODE_ENV' + : // hard coded dev/prod builds + JSON.stringify(isProd ? 'production' : 'development'), + __DEV__: + format === 'es' + ? // preserve to be handled by bundlers + `(process.env.NODE_ENV !== 'production')` + : // hard coded dev/prod builds + !isProd, + __VERSION__: JSON.stringify(version), + }), isProd && terser(), ].filter(Boolean), - }; + } } -let buildConfig; +let buildConfig if (process.env.TARGET) { - buildConfig = genConfig(builds[process.env.TARGET]); + buildConfig = [genConfig(builds[process.env.TARGET])] } else { - buildConfig = getAllBuilds(); + buildConfig = getAllBuilds() } -export default buildConfig; +// bundle typings +buildConfig.push({ + input: 'src/index.ts', + output: { + file: 'dist/vue-composition-api.d.ts', + format: 'es', + }, + onwarn, + plugins: [dts()], +}) + +export default buildConfig diff --git a/scripts/update-readme.js b/scripts/update-readme.js new file mode 100644 index 00000000..364b5a9c --- /dev/null +++ b/scripts/update-readme.js @@ -0,0 +1,29 @@ +const { promises: fs } = require('fs') +const path = require('path') +const { version } = require('../package.json') + +const files = ['../README.md', '../README.zh-CN.md'] + +const MakeLinks = (version, vueVersion = '2.6') => + ` +\`\`\`html + + +\`\`\` +` + +;(async () => { + const links = MakeLinks(version) + + for (const file of files) { + const filepath = path.resolve(__dirname, file) + const raw = await fs.readFile(filepath, 'utf-8') + + const updated = raw.replace( + /([\s\S]*)/g, + `${links}` + ) + + await fs.writeFile(filepath, updated, 'utf-8') + } +})() diff --git a/src/apis/computed.ts b/src/apis/computed.ts new file mode 100644 index 00000000..fc85a5ee --- /dev/null +++ b/src/apis/computed.ts @@ -0,0 +1,102 @@ +import { getVueConstructor } from '../runtimeContext' +import { createRef, ComputedRef, WritableComputedRef } from '../reactivity' +import { + warn, + noopFn, + defineComponentInstance, + getVueInternalClasses, + isFunction, +} from '../utils' +import { getCurrentScopeVM } from './effectScope' + +export type ComputedGetter = (ctx?: any) => T +export type ComputedSetter = (v: T) => void + +export interface WritableComputedOptions { + get: ComputedGetter + set: ComputedSetter +} + +// read-only +export function computed(getter: ComputedGetter): ComputedRef +// writable +export function computed( + options: WritableComputedOptions +): WritableComputedRef +// implement +export function computed( + getterOrOptions: ComputedGetter | WritableComputedOptions +): ComputedRef | WritableComputedRef { + const vm = getCurrentScopeVM() + let getter: ComputedGetter + let setter: ComputedSetter | undefined + + if (isFunction(getterOrOptions)) { + getter = getterOrOptions + } else { + getter = getterOrOptions.get + setter = getterOrOptions.set + } + + let computedSetter + let computedGetter + + if (vm && !vm.$isServer) { + const { Watcher, Dep } = getVueInternalClasses() + let watcher: any + computedGetter = () => { + if (!watcher) { + watcher = new Watcher(vm, getter, noopFn, { lazy: true }) + } + if (watcher.dirty) { + watcher.evaluate() + } + if (Dep.target) { + watcher.depend() + } + return watcher.value + } + + computedSetter = (v: T) => { + if (__DEV__ && !setter) { + warn('Write operation failed: computed value is readonly.', vm!) + return + } + + if (setter) { + setter(v) + } + } + } else { + // fallback + const computedHost = defineComponentInstance(getVueConstructor(), { + computed: { + $$state: { + get: getter, + set: setter, + }, + }, + }) + + vm && vm.$on('hook:destroyed', () => computedHost.$destroy()) + + computedGetter = () => (computedHost as any).$$state + computedSetter = (v: T) => { + if (__DEV__ && !setter) { + warn('Write operation failed: computed value is readonly.', vm!) + return + } + + ;(computedHost as any).$$state = v + } + } + + return createRef( + { + get: computedGetter, + set: computedSetter, + }, + !setter, + true + ) as WritableComputedRef | ComputedRef +} diff --git a/src/apis/createApp.ts b/src/apis/createApp.ts new file mode 100644 index 00000000..8f3aa208 --- /dev/null +++ b/src/apis/createApp.ts @@ -0,0 +1,76 @@ +import type Vue from 'vue' +import { VueConstructor } from 'vue' +import { Directive } from '../component/directives' +import { getVueConstructor } from '../runtimeContext' +import { warn } from '../utils' +import { InjectionKey } from './inject' + +// Has a generic to match Vue 3 API and be type compatible +export interface App { + config: VueConstructor['config'] + use: VueConstructor['use'] + mixin: VueConstructor['mixin'] + component: VueConstructor['component'] + directive(name: string): Directive | undefined + directive(name: string, directive: Directive): this + provide(key: InjectionKey | symbol | string, value: T): this + mount: Vue['$mount'] + unmount: Vue['$destroy'] +} + +export function createApp(rootComponent: any, rootProps: any = undefined): App { + const V = getVueConstructor()! + + let mountedVM: Vue | undefined = undefined + + let provide: Record = {} + + const app: App = { + config: V.config, + use: V.use.bind(V), + mixin: V.mixin.bind(V), + component: V.component.bind(V), + provide(key: InjectionKey | symbol | string, value: T) { + provide[key as any] = value + return this + }, + directive(name: string, dir?: Directive | undefined): any { + if (dir) { + V.directive(name, dir as any) + return app + } else { + return V.directive(name) + } + }, + mount: (el, hydrating) => { + if (!mountedVM) { + mountedVM = new V({ + propsData: rootProps, + ...rootComponent, + provide: { ...provide, ...rootComponent.provide }, + }) + mountedVM.$mount(el, hydrating) + return mountedVM + } else { + if (__DEV__) { + warn( + `App has already been mounted.\n` + + `If you want to remount the same app, move your app creation logic ` + + `into a factory function and create fresh app instances for each ` + + `mount - e.g. \`const createMyApp = () => createApp(App)\`` + ) + } + return mountedVM + } + }, + unmount: () => { + if (mountedVM) { + mountedVM.$destroy() + mountedVM = undefined + } else if (__DEV__) { + warn(`Cannot unmount an app that is not mounted.`) + } + }, + } + return app +} diff --git a/src/apis/createElement.ts b/src/apis/createElement.ts new file mode 100644 index 00000000..7f8bc2d6 --- /dev/null +++ b/src/apis/createElement.ts @@ -0,0 +1,51 @@ +import type { CreateElement } from 'vue' +import { + getVueConstructor, + getCurrentInstance, + ComponentInternalInstance, +} from '../runtimeContext' +import { defineComponentInstance } from '../utils/helper' +import { warn } from '../utils' +import { AsyncComponent, Component } from 'vue/types/options' +import { VNode, VNodeChildren, VNodeData } from 'vue/types/vnode' + +export interface H extends CreateElement { + ( + this: ComponentInternalInstance | null | undefined, + tag?: + | string + | Component + | AsyncComponent + | (() => Component), + children?: VNodeChildren + ): VNode + ( + this: ComponentInternalInstance | null | undefined, + tag?: + | string + | Component + | AsyncComponent + | (() => Component), + data?: VNodeData, + children?: VNodeChildren + ): VNode +} + +let fallbackCreateElement: CreateElement + +export const createElement: H = function createElement(this, ...args: any) { + const instance = this?.proxy || getCurrentInstance()?.proxy + if (!instance) { + __DEV__ && + warn('`createElement()` has been called outside of render function.') + if (!fallbackCreateElement) { + fallbackCreateElement = defineComponentInstance( + getVueConstructor() + ).$createElement + } + + return fallbackCreateElement.apply(fallbackCreateElement, args) + } + + return instance.$createElement.apply(instance, args) +} diff --git a/src/apis/effectScope.ts b/src/apis/effectScope.ts new file mode 100644 index 00000000..89521ed1 --- /dev/null +++ b/src/apis/effectScope.ts @@ -0,0 +1,130 @@ +import { + ComponentInternalInstance, + getCurrentInstance, + getVueConstructor, + withCurrentInstanceTrackingDisabled, +} from '../runtimeContext' +import { defineComponentInstance } from '../utils' +import { warn } from './warn' + +let activeEffectScope: EffectScope | undefined +const effectScopeStack: EffectScope[] = [] + +class EffectScopeImpl { + active = true + effects: EffectScope[] = [] + cleanups: (() => void)[] = [] + + /** + * @internal + **/ + vm: Vue + + constructor(vm: Vue) { + this.vm = vm + } + + run(fn: () => T): T | undefined { + if (this.active) { + try { + this.on() + return fn() + } finally { + this.off() + } + } else if (__DEV__) { + warn(`cannot run an inactive effect scope.`) + } + return + } + + on() { + if (this.active) { + effectScopeStack.push(this) + activeEffectScope = this + } + } + + off() { + if (this.active) { + effectScopeStack.pop() + activeEffectScope = effectScopeStack[effectScopeStack.length - 1] + } + } + + stop() { + if (this.active) { + this.vm.$destroy() + this.effects.forEach((e) => e.stop()) + this.cleanups.forEach((cleanup) => cleanup()) + this.active = false + } + } +} + +export class EffectScope extends EffectScopeImpl { + constructor(detached = false) { + let vm: Vue = undefined! + withCurrentInstanceTrackingDisabled(() => { + vm = defineComponentInstance(getVueConstructor()) + }) + super(vm) + if (!detached) { + recordEffectScope(this) + } + } +} + +export function recordEffectScope( + effect: EffectScope, + scope?: EffectScope | null +) { + scope = scope || activeEffectScope + if (scope && scope.active) { + scope.effects.push(effect) + return + } + // destroy on parent component unmounted + const vm = getCurrentInstance()?.proxy + vm && vm.$on('hook:destroyed', () => effect.stop()) +} + +export function effectScope(detached?: boolean) { + return new EffectScope(detached) +} + +export function getCurrentScope() { + return activeEffectScope +} + +export function onScopeDispose(fn: () => void) { + if (activeEffectScope) { + activeEffectScope.cleanups.push(fn) + } else if (__DEV__) { + warn( + `onScopeDispose() is called when there is no active effect scope` + + ` to be associated with.` + ) + } +} + +/** + * @internal + **/ +export function getCurrentScopeVM() { + return getCurrentScope()?.vm || getCurrentInstance()?.proxy +} + +/** + * @internal + **/ +export function bindCurrentScopeToVM( + vm: ComponentInternalInstance +): EffectScope { + if (!vm.scope) { + const scope = new EffectScopeImpl(vm.proxy) as EffectScope + vm.scope = scope + vm.proxy.$on('hook:destroyed', () => scope.stop()) + } + return vm.scope +} diff --git a/src/apis/index.ts b/src/apis/index.ts new file mode 100644 index 00000000..515837da --- /dev/null +++ b/src/apis/index.ts @@ -0,0 +1,28 @@ +export * from '../reactivity' +export { + onBeforeMount, + onMounted, + onBeforeUpdate, + onUpdated, + onBeforeUnmount, + onUnmounted, + onErrorCaptured, + onActivated, + onDeactivated, + onServerPrefetch, +} from './lifecycle' +export * from './watch' +export * from './computed' +export * from './inject' +export { useCssModule, useCSSModule } from './useCssModule' +export { App, createApp } from './createApp' +export { nextTick } from './nextTick' +export { createElement as h } from './createElement' +export { warn } from './warn' +export { + effectScope, + EffectScope, + getCurrentScope, + onScopeDispose, +} from './effectScope' +export { useAttrs, useSlots } from './setupHelpers' diff --git a/src/apis/inject.ts b/src/apis/inject.ts new file mode 100644 index 00000000..693b1920 --- /dev/null +++ b/src/apis/inject.ts @@ -0,0 +1,82 @@ +import { ComponentInstance } from '../component' +import { + hasOwn, + warn, + getCurrentInstanceForFn, + isFunction, + proxy, +} from '../utils' +import { getCurrentInstance } from '../runtimeContext' + +const NOT_FOUND = {} +export interface InjectionKey extends Symbol {} + +function resolveInject( + provideKey: InjectionKey | string, + vm: ComponentInstance +): any { + let source = vm + while (source) { + if (source._provided && hasOwn(source._provided, provideKey as PropertyKey)) { + return source._provided[provideKey as PropertyKey] + } + source = source.$parent + } + + return NOT_FOUND +} + +export function provide(key: InjectionKey | string, value: T): void { + const vm = getCurrentInstanceForFn('provide')?.proxy + if (!vm) return + + if (!vm._provided) { + const provideCache = {} + proxy(vm, '_provided', { + get: () => provideCache, + set: (v: any) => Object.assign(provideCache, v), + }) + } + + vm._provided[key as string] = value +} + +export function inject(key: InjectionKey | string): T | undefined +export function inject( + key: InjectionKey | string, + defaultValue: T, + treatDefaultAsFactory?: false +): T +export function inject( + key: InjectionKey | string, + defaultValue: T | (() => T), + treatDefaultAsFactory?: true +): T +export function inject( + key: InjectionKey | string, + defaultValue?: unknown, + treatDefaultAsFactory = false +) { + const vm = getCurrentInstance()?.proxy + if (!vm) { + __DEV__ && + warn(`inject() can only be used inside setup() or functional components.`) + return + } + + if (!key) { + __DEV__ && warn(`injection "${String(key)}" not found.`, vm) + return defaultValue + } + + const val = resolveInject(key, vm) + if (val !== NOT_FOUND) { + return val + } else if (arguments.length > 1) { + return treatDefaultAsFactory && isFunction(defaultValue) + ? defaultValue() + : defaultValue + } else if (__DEV__) { + warn(`Injection "${String(key)}" not found.`, vm) + } +} diff --git a/src/apis/lifecycle.ts b/src/apis/lifecycle.ts new file mode 100644 index 00000000..3bb49e3a --- /dev/null +++ b/src/apis/lifecycle.ts @@ -0,0 +1,58 @@ +import { VueConstructor } from 'vue' +import { + getVueConstructor, + setCurrentInstance, + getCurrentInstance, + ComponentInternalInstance, +} from '../runtimeContext' +import { getCurrentInstanceForFn } from '../utils/helper' + +const genName = (name: string) => `on${name[0].toUpperCase() + name.slice(1)}` +function createLifeCycle(lifeCyclehook: string) { + return (callback: Function, target?: ComponentInternalInstance | null) => { + const instance = getCurrentInstanceForFn(genName(lifeCyclehook), target) + return ( + instance && + injectHookOption(getVueConstructor(), instance, lifeCyclehook, callback) + ) + } +} + +function injectHookOption( + Vue: VueConstructor, + instance: ComponentInternalInstance, + hook: string, + val: Function +) { + const options = instance.proxy.$options as Record + const mergeFn = Vue.config.optionMergeStrategies[hook] + const wrappedHook = wrapHookCall(instance, val) + options[hook] = mergeFn(options[hook], wrappedHook) + return wrappedHook +} + +function wrapHookCall( + instance: ComponentInternalInstance, + fn: Function +): Function { + return (...args: any) => { + let prev = getCurrentInstance() + setCurrentInstance(instance) + try { + return fn(...args) + } finally { + setCurrentInstance(prev) + } + } +} + +export const onBeforeMount = createLifeCycle('beforeMount') +export const onMounted = createLifeCycle('mounted') +export const onBeforeUpdate = createLifeCycle('beforeUpdate') +export const onUpdated = createLifeCycle('updated') +export const onBeforeUnmount = createLifeCycle('beforeDestroy') +export const onUnmounted = createLifeCycle('destroyed') +export const onErrorCaptured = createLifeCycle('errorCaptured') +export const onActivated = createLifeCycle('activated') +export const onDeactivated = createLifeCycle('deactivated') +export const onServerPrefetch = createLifeCycle('serverPrefetch') diff --git a/src/apis/nextTick.ts b/src/apis/nextTick.ts new file mode 100644 index 00000000..9c736ef9 --- /dev/null +++ b/src/apis/nextTick.ts @@ -0,0 +1,11 @@ +import Vue from 'vue' +import { getVueConstructor } from '../runtimeContext' + +type NextTick = Vue['$nextTick'] + +export const nextTick: NextTick = function nextTick( + this: ThisType, + ...args: Parameters +) { + return getVueConstructor()?.nextTick.apply(this, args) +} diff --git a/src/apis/setupHelpers.ts b/src/apis/setupHelpers.ts new file mode 100644 index 00000000..4b958221 --- /dev/null +++ b/src/apis/setupHelpers.ts @@ -0,0 +1,18 @@ +import { getCurrentInstance, SetupContext } from '../runtimeContext' +import { warn } from '../utils' + +export function useSlots(): SetupContext['slots'] { + return getContext().slots +} + +export function useAttrs(): SetupContext['attrs'] { + return getContext().attrs +} + +function getContext(): SetupContext { + const i = getCurrentInstance()! + if (__DEV__ && !i) { + warn(`useContext() called without active instance.`) + } + return i.setupContext! +} diff --git a/src/apis/useCssModule.ts b/src/apis/useCssModule.ts new file mode 100644 index 00000000..772cadd5 --- /dev/null +++ b/src/apis/useCssModule.ts @@ -0,0 +1,28 @@ +import { getCurrentInstance } from '../runtimeContext' +import { warn } from '../utils' + +const EMPTY_OBJ: { readonly [key: string]: string } = __DEV__ + ? Object.freeze({}) + : {} + +export const useCssModule = (name = '$style'): Record => { + const instance = getCurrentInstance() + if (!instance) { + __DEV__ && warn(`useCssModule must be called inside setup()`) + return EMPTY_OBJ + } + + const mod = (instance.proxy as any)?.[name] + if (!mod) { + __DEV__ && + warn(`Current instance does not have CSS module named "${name}".`) + return EMPTY_OBJ + } + + return mod as Record +} + +/** + * @deprecated use `useCssModule` instead. + */ +export const useCSSModule = useCssModule diff --git a/src/apis/warn.ts b/src/apis/warn.ts new file mode 100644 index 00000000..242704ec --- /dev/null +++ b/src/apis/warn.ts @@ -0,0 +1,12 @@ +import { getCurrentInstance } from '../runtimeContext' +import { warn as vueWarn } from '../utils' + +/** + * Displays a warning message (using console.error) with a stack trace if the + * function is called inside of active component. + * + * @param message warning message to be displayed + */ +export function warn(message: string) { + vueWarn(message, getCurrentInstance()?.proxy) +} diff --git a/src/apis/watch.ts b/src/apis/watch.ts new file mode 100644 index 00000000..9987ec5a --- /dev/null +++ b/src/apis/watch.ts @@ -0,0 +1,517 @@ +import { ComponentInstance } from '../component' +import { Ref, isRef, isReactive, ComputedRef } from '../reactivity' +import { + assert, + logError, + noopFn, + warn, + isFunction, + isObject, + isArray, + isPlainObject, + isSet, + isMap, + isSame, +} from '../utils' +import { defineComponentInstance } from '../utils/helper' +import { getVueConstructor } from '../runtimeContext' +import { + WatcherPreFlushQueueKey, + WatcherPostFlushQueueKey, +} from '../utils/symbols' +import { getCurrentScopeVM } from './effectScope' +import { rawSet } from '../utils/sets' + +export type WatchEffect = (onInvalidate: InvalidateCbRegistrator) => void + +export type WatchSource = Ref | ComputedRef | (() => T) + +export type WatchCallback = ( + value: V, + oldValue: OV, + onInvalidate: InvalidateCbRegistrator +) => any + +declare type MapSources = { + [K in keyof T]: T[K] extends WatchSource + ? Immediate extends true + ? V | undefined + : V + : never +} + +type MultiWatchSources = (WatchSource | object)[] + +export interface WatchOptionsBase { + flush?: FlushMode + // onTrack?: ReactiveEffectOptions['onTrack']; + // onTrigger?: ReactiveEffectOptions['onTrigger']; +} + +type InvalidateCbRegistrator = (cb: () => void) => void + +export type FlushMode = 'pre' | 'post' | 'sync' + +export interface WatchOptions extends WatchOptionsBase { + immediate?: Immediate + deep?: boolean +} + +export interface VueWatcher { + lazy: boolean + get(): any + teardown(): void + run(): void + + value: any +} + +export type WatchStopHandle = () => void + +let fallbackVM: ComponentInstance + +function flushPreQueue(this: any) { + flushQueue(this, WatcherPreFlushQueueKey) +} + +function flushPostQueue(this: any) { + flushQueue(this, WatcherPostFlushQueueKey) +} + +function hasWatchEnv(vm: any) { + return vm[WatcherPreFlushQueueKey] !== undefined +} + +function installWatchEnv(vm: any) { + vm[WatcherPreFlushQueueKey] = [] + vm[WatcherPostFlushQueueKey] = [] + vm.$on('hook:beforeUpdate', flushPreQueue) + vm.$on('hook:updated', flushPostQueue) +} + +function getWatcherOption(options?: Partial): WatchOptions { + return { + ...{ + immediate: false, + deep: false, + flush: 'pre', + }, + ...options, + } +} + +function getWatchEffectOption(options?: Partial): WatchOptions { + return { + ...{ + flush: 'pre', + }, + ...options, + } +} + +function getWatcherVM() { + let vm = getCurrentScopeVM() + if (!vm) { + if (!fallbackVM) { + fallbackVM = defineComponentInstance(getVueConstructor()) + } + vm = fallbackVM + } else if (!hasWatchEnv(vm)) { + installWatchEnv(vm) + } + return vm +} + +function flushQueue(vm: any, key: any) { + const queue = vm[key] + for (let index = 0; index < queue.length; index++) { + queue[index]() + } + queue.length = 0 +} + +function queueFlushJob( + vm: any, + fn: () => void, + mode: Exclude +) { + // flush all when beforeUpdate and updated are not fired + const fallbackFlush = () => { + vm.$nextTick(() => { + if (vm[WatcherPreFlushQueueKey].length) { + flushQueue(vm, WatcherPreFlushQueueKey) + } + if (vm[WatcherPostFlushQueueKey].length) { + flushQueue(vm, WatcherPostFlushQueueKey) + } + }) + } + + switch (mode) { + case 'pre': + fallbackFlush() + vm[WatcherPreFlushQueueKey].push(fn) + break + case 'post': + fallbackFlush() + vm[WatcherPostFlushQueueKey].push(fn) + break + default: + assert( + false, + `flush must be one of ["post", "pre", "sync"], but got ${mode}` + ) + break + } +} + +function createVueWatcher( + vm: ComponentInstance, + getter: () => any, + callback: (n: any, o: any) => any, + options: { + deep: boolean + sync: boolean + immediateInvokeCallback?: boolean + noRun?: boolean + before?: () => void + } +): VueWatcher { + const index = vm._watchers.length + // @ts-ignore: use undocumented options + vm.$watch(getter, callback, { + immediate: options.immediateInvokeCallback, + deep: options.deep, + lazy: options.noRun, + sync: options.sync, + before: options.before, + }) + + return vm._watchers[index] +} + +// We have to monkeypatch the teardown function so Vue will run +// runCleanup() when it tears down the watcher on unmounted. +function patchWatcherTeardown(watcher: VueWatcher, runCleanup: () => void) { + const _teardown = watcher.teardown + watcher.teardown = function (...args) { + _teardown.apply(watcher, args) + runCleanup() + } +} + +function createWatcher( + vm: ComponentInstance, + source: WatchSource | WatchSource[] | WatchEffect, + cb: WatchCallback | null, + options: WatchOptions +): () => void { + if (__DEV__ && !cb) { + if (options.immediate !== undefined) { + warn( + `watch() "immediate" option is only respected when using the ` + + `watch(source, callback, options?) signature.` + ) + } + if (options.deep !== undefined) { + warn( + `watch() "deep" option is only respected when using the ` + + `watch(source, callback, options?) signature.` + ) + } + } + + const flushMode = options.flush + const isSync = flushMode === 'sync' + let cleanup: (() => void) | null + const registerCleanup: InvalidateCbRegistrator = (fn: () => void) => { + cleanup = () => { + try { + fn() + } catch ( + // FIXME: remove any + error: any + ) { + logError(error, vm, 'onCleanup()') + } + } + } + // cleanup before running getter again + const runCleanup = () => { + if (cleanup) { + cleanup() + cleanup = null + } + } + const createScheduler = (fn: T): T => { + if ( + isSync || + /* without a current active instance, ignore pre|post mode */ vm === + fallbackVM + ) { + return fn + } + return ((...args: any[]) => + queueFlushJob( + vm, + () => { + fn(...args) + }, + flushMode as 'pre' | 'post' + )) as unknown as T + } + + // effect watch + if (cb === null) { + let running = false + const getter = () => { + // preventing the watch callback being call in the same execution + if (running) { + return + } + try { + running = true + ;(source as WatchEffect)(registerCleanup) + } finally { + running = false + } + } + const watcher = createVueWatcher(vm, getter, noopFn, { + deep: options.deep || false, + sync: isSync, + before: runCleanup, + }) + + patchWatcherTeardown(watcher, runCleanup) + + // enable the watcher update + watcher.lazy = false + const originGet = watcher.get.bind(watcher) + + // always run watchEffect + watcher.get = createScheduler(originGet) + + return () => { + watcher.teardown() + } + } + + let deep = options.deep + let isMultiSource = false + + let getter: () => any + if (isRef(source)) { + getter = () => source.value + } else if (isReactive(source)) { + getter = () => source + deep = true + } else if (isArray(source)) { + isMultiSource = true + getter = () => + source.map((s) => { + if (isRef(s)) { + return s.value + } else if (isReactive(s)) { + return traverse(s) + } else if (isFunction(s)) { + return s() + } else { + __DEV__ && + warn( + `Invalid watch source: ${JSON.stringify(s)}. + A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types.`, + vm + ) + return noopFn + } + }) + } else if (isFunction(source)) { + getter = source as () => any + } else { + getter = noopFn + __DEV__ && + warn( + `Invalid watch source: ${JSON.stringify(source)}. + A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types.`, + vm + ) + } + + if (deep) { + const baseGetter = getter + getter = () => traverse(baseGetter()) + } + + const applyCb = (n: any, o: any) => { + if ( + !deep && + isMultiSource && + n.every((v: any, i: number) => isSame(v, o[i])) + ) + return + // cleanup before running cb again + runCleanup() + return cb(n, o, registerCleanup) + } + let callback = createScheduler(applyCb) + if (options.immediate) { + const originalCallback = callback + // `shiftCallback` is used to handle the first sync effect run. + // The subsequent callbacks will redirect to `callback`. + let shiftCallback = (n: any, o: any) => { + shiftCallback = originalCallback + // o is undefined on the first call + return applyCb(n, isArray(n) ? [] : o) + } + callback = (n: any, o: any) => { + return shiftCallback(n, o) + } + } + + // @ts-ignore: use undocumented option "sync" + const stop = vm.$watch(getter, callback, { + immediate: options.immediate, + deep: deep, + sync: isSync, + }) + + // Once again, we have to hack the watcher for proper teardown + const watcher = vm._watchers[vm._watchers.length - 1] + + // if the return value is reactive and deep:true + // watch for changes, this might happen when new key is added + if (isReactive(watcher.value) && watcher.value.__ob__?.dep && deep) { + watcher.value.__ob__.dep.addSub({ + update() { + // this will force the source to be reevaluated and the callback + // executed if needed + watcher.run() + }, + }) + } + + patchWatcherTeardown(watcher, runCleanup) + + return () => { + stop() + } +} + +export function watchEffect( + effect: WatchEffect, + options?: WatchOptionsBase +): WatchStopHandle { + const opts = getWatchEffectOption(options) + const vm = getWatcherVM() + return createWatcher(vm, effect, null, opts) +} + +export function watchPostEffect(effect: WatchEffect) { + return watchEffect(effect, { flush: 'post' }) +} + +export function watchSyncEffect(effect: WatchEffect) { + return watchEffect(effect, { flush: 'sync' }) +} + +// overload #1 + #2 + #3: array of multiple sources + cb + +// overload #1: In readonly case the first overload must be spread tuple type. +// In otherwise members in tuple can not get the correct types. +export function watch< + T extends Readonly, + Immediate extends Readonly = false +>( + sources: [...T], + cb: WatchCallback, MapSources>, + options?: WatchOptions +): WatchStopHandle + +// overload #2: for not spread readonly tuple +export function watch< + T extends Readonly, + Immediate extends Readonly = false +>( + sources: T, + cb: WatchCallback, MapSources>, + options?: WatchOptions +): WatchStopHandle + +// overload #3: for not readonly multiSources +export function watch< + T extends MultiWatchSources, + Immediate extends Readonly = false +>( + sources: [...T], + cb: WatchCallback, MapSources>, + options?: WatchOptions +): WatchStopHandle + +// overload #4: single source + cb +export function watch = false>( + source: WatchSource, + cb: WatchCallback, + options?: WatchOptions +): WatchStopHandle + +// overload #5: watching reactive object w/ cb +export function watch< + T extends object, + Immediate extends Readonly = false +>( + source: T, + cb: WatchCallback, + options?: WatchOptions +): WatchStopHandle + +// implementation +export function watch( + source: WatchSource | WatchSource[], + cb: WatchCallback, + options?: WatchOptions +): WatchStopHandle { + let callback: WatchCallback | null = null + if (isFunction(cb)) { + // source watch + callback = cb as WatchCallback + } else { + // effect watch + if (__DEV__) { + warn( + `\`watch(fn, options?)\` signature has been moved to a separate API. ` + + `Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` + + `supports \`watch(source, cb, options?) signature.` + ) + } + options = cb as Partial + callback = null + } + + const opts = getWatcherOption(options) + const vm = getWatcherVM() + + return createWatcher(vm, source, callback, opts) +} + +function traverse(value: unknown, seen: Set = new Set()) { + if (!isObject(value) || seen.has(value) || rawSet.has(value)) { + return value + } + seen.add(value) + if (isRef(value)) { + traverse(value.value, seen) + } else if (isArray(value)) { + for (let i = 0; i < value.length; i++) { + traverse(value[i], seen) + } + } else if (isSet(value) || isMap(value)) { + value.forEach((v: any) => { + traverse(v, seen) + }) + } else if (isPlainObject(value)) { + for (const key in value) { + traverse(value[key], seen) + } + } + return value +} diff --git a/src/component/common.ts b/src/component/common.ts new file mode 100644 index 00000000..014e0cd4 --- /dev/null +++ b/src/component/common.ts @@ -0,0 +1 @@ +export type Data = { [key: string]: unknown } diff --git a/src/component/componentOptions.ts b/src/component/componentOptions.ts new file mode 100644 index 00000000..a2eb4eaf --- /dev/null +++ b/src/component/componentOptions.ts @@ -0,0 +1,118 @@ +import Vue, { VNode, ComponentOptions as Vue2ComponentOptions } from 'vue' +import { EmitsOptions, SetupContext } from '../runtimeContext' +import { Data } from './common' +import { ComponentPropsOptions, ExtractPropTypes } from './componentProps' +import { ComponentRenderProxy } from './componentProxy' +export { ComponentPropsOptions } from './componentProps' + +export type ComputedGetter = (ctx?: any) => T +export type ComputedSetter = (v: T) => void + +export interface WritableComputedOptions { + get: ComputedGetter + set: ComputedSetter +} + +export type ComputedOptions = Record< + string, + ComputedGetter | WritableComputedOptions +> + +export interface MethodOptions { + [key: string]: Function +} + +export type SetupFunction< + Props, + RawBindings = {}, + Emits extends EmitsOptions = {} +> = ( + this: void, + props: Readonly, + ctx: SetupContext +) => RawBindings | (() => VNode | null) | void + +interface ComponentOptionsBase< + Props, + D = Data, + C extends ComputedOptions = {}, + M extends MethodOptions = {}, + Mixin = {}, + Extends = {}, + Emits extends EmitsOptions = {} +> extends Omit< + Vue2ComponentOptions, + 'data' | 'computed' | 'method' | 'setup' | 'props' + > { + // allow any custom options + [key: string]: any + + // rewrite options api types + data?: (this: Props & Vue, vm: Props) => D + computed?: C + methods?: M +} + +export type ExtractComputedReturns = { + [key in keyof T]: T[key] extends { get: (...args: any[]) => infer TReturn } + ? TReturn + : T[key] extends (...args: any[]) => infer TReturn + ? TReturn + : never +} + +export type ComponentOptionsWithProps< + PropsOptions = ComponentPropsOptions, + RawBindings = Data, + D = Data, + C extends ComputedOptions = {}, + M extends MethodOptions = {}, + Mixin = {}, + Extends = {}, + Emits extends EmitsOptions = {}, + Props = ExtractPropTypes +> = ComponentOptionsBase & { + props?: PropsOptions + emits?: Emits & ThisType + setup?: SetupFunction +} & ThisType< + ComponentRenderProxy + > + +export type ComponentOptionsWithArrayProps< + PropNames extends string = string, + RawBindings = Data, + D = Data, + C extends ComputedOptions = {}, + M extends MethodOptions = {}, + Mixin = {}, + Extends = {}, + Emits extends EmitsOptions = {}, + Props = Readonly<{ [key in PropNames]?: any }> +> = ComponentOptionsBase & { + props?: PropNames[] + emits?: Emits & ThisType + setup?: SetupFunction +} & ThisType< + ComponentRenderProxy + > + +export type ComponentOptionsWithoutProps< + Props = {}, + RawBindings = Data, + D = Data, + C extends ComputedOptions = {}, + M extends MethodOptions = {}, + Mixin = {}, + Extends = {}, + Emits extends EmitsOptions = {} +> = ComponentOptionsBase & { + props?: undefined + emits?: Emits & ThisType + setup?: SetupFunction +} & ThisType< + ComponentRenderProxy + > + +export type WithLegacyAPI = T & + Omit, keyof T> diff --git a/src/component/componentProps.ts b/src/component/componentProps.ts new file mode 100644 index 00000000..0cac1bc5 --- /dev/null +++ b/src/component/componentProps.ts @@ -0,0 +1,100 @@ +import { Data } from './common' + +export type ComponentPropsOptions

= + | ComponentObjectPropsOptions

+ | string[] + +export type ComponentObjectPropsOptions

= { + [K in keyof P]: Prop | null +} + +export type Prop = PropOptions | PropType + +type DefaultFactory = () => T | null | undefined + +export interface PropOptions { + type?: PropType | true | null + required?: boolean + default?: D | DefaultFactory | null | undefined | object + validator?(value: unknown): boolean +} + +export type PropType = PropConstructor | PropConstructor[] + +type PropConstructor = + | { new (...args: any[]): T & object } + | { (): T } + | { new (...args: string[]): Function } + +type RequiredKeys = { + [K in keyof T]: T[K] extends + | { required: true } + | { default: any } + | BooleanConstructor + | { type: BooleanConstructor } + ? K + : never +}[keyof T] + +type OptionalKeys = Exclude> + +type ExtractFunctionPropType< + T extends Function, + TArgs extends Array = any[], + TResult = any +> = T extends (...args: TArgs) => TResult ? T : never + +type ExtractCorrectPropType = T extends Function + ? ExtractFunctionPropType + : Exclude + +// prettier-ignore +type InferPropType = T extends null + ? any // null & true would fail to infer + : T extends { type: null | true } + ? any // As TS issue https://github.com/Microsoft/TypeScript/issues/14829 // somehow `ObjectConstructor` when inferred from { (): T } becomes `any` // `BooleanConstructor` when inferred from PropConstructor(with PropMethod) becomes `Boolean` + : T extends ObjectConstructor | { type: ObjectConstructor } + ? Record + : T extends BooleanConstructor | { type: BooleanConstructor } + ? boolean + : T extends DateConstructor | { type: DateConstructor} + ? Date + : T extends FunctionConstructor | { type: FunctionConstructor } + ? Function + : T extends Prop + ? unknown extends V + ? D extends null | undefined + ? V + : D + : ExtractCorrectPropType + : T + +export type ExtractPropTypes = { + // use `keyof Pick>` instead of `RequiredKeys` to support IDE features + [K in keyof Pick>]: InferPropType +} & { + // use `keyof Pick>` instead of `OptionalKeys` to support IDE features + [K in keyof Pick>]?: InferPropType +} + +type DefaultKeys = { + [K in keyof T]: T[K] extends + | { + default: any + } + | BooleanConstructor + | { type: BooleanConstructor } + ? T[K] extends { + type: BooleanConstructor + required: true + } + ? never + : K + : never +}[keyof T] + +// extract props which defined with default from prop options +export type ExtractDefaultPropTypes = O extends object + ? // use `keyof Pick>` instead of `DefaultKeys` to support IDE features + { [K in keyof Pick>]: InferPropType } + : {} diff --git a/src/component/componentProxy.ts b/src/component/componentProxy.ts new file mode 100644 index 00000000..5199e2c6 --- /dev/null +++ b/src/component/componentProxy.ts @@ -0,0 +1,193 @@ +import { ExtractDefaultPropTypes, ExtractPropTypes } from './componentProps' +import { + nextTick, + ShallowUnwrapRef, + UnwrapNestedRefs, + WatchOptions, + WatchStopHandle, +} from '..' +import { Data } from './common' + +import Vue, { + VueConstructor, + ComponentOptions as Vue2ComponentOptions, +} from 'vue' +import { + ComputedOptions, + MethodOptions, + ExtractComputedReturns, +} from './componentOptions' +import { + ComponentInternalInstance, + ComponentRenderEmitFn, + EmitFn, + EmitsOptions, + ObjectEmitsOptions, + Slots, +} from '../runtimeContext' + +type EmitsToProps = T extends string[] + ? { + [K in string & `on${Capitalize}`]?: (...args: any[]) => any + } + : T extends ObjectEmitsOptions + ? { + [K in string & + `on${Capitalize}`]?: K extends `on${infer C}` + ? T[Uncapitalize] extends null + ? (...args: any[]) => any + : ( + ...args: T[Uncapitalize] extends (...args: infer P) => any + ? P + : never + ) => any + : never + } + : {} + +export type ComponentInstance = InstanceType + +// public properties exposed on the proxy, which is used as the render context +// in templates (as `this` in the render option) +export type ComponentRenderProxy< + P = {}, // props type extracted from props option + B = {}, // raw bindings returned from setup() + D = {}, // return from data() + C extends ComputedOptions = {}, + M extends MethodOptions = {}, + Mixin = {}, + Extends = {}, + Emits extends EmitsOptions = {}, + PublicProps = P, + Defaults = {}, + MakeDefaultsOptional extends boolean = false +> = { + $data: D + $props: Readonly< + MakeDefaultsOptional extends true + ? Partial & Omit

+ : P & PublicProps + > + $attrs: Record + $emit: ComponentRenderEmitFn< + Emits, + keyof Emits, + ComponentRenderProxy< + P, + B, + D, + C, + M, + Mixin, + Extends, + Emits, + PublicProps, + Defaults, + MakeDefaultsOptional + > + > +} & Readonly

& + ShallowUnwrapRef & + D & + M & + ExtractComputedReturns & + Omit + +// for Vetur and TSX support +type VueConstructorProxy< + PropsOptions, + RawBindings, + Data, + Computed extends ComputedOptions, + Methods extends MethodOptions, + Mixin = {}, + Extends = {}, + Emits extends EmitsOptions = {}, + Props = ExtractPropTypes & + ({} extends Emits ? {} : EmitsToProps) +> = Omit & { + new (...args: any[]): ComponentRenderProxy< + Props, + ShallowUnwrapRef, + Data, + Computed, + Methods, + Mixin, + Extends, + Emits, + Props, + ExtractDefaultPropTypes, + true + > +} + +type DefaultData = object | ((this: V) => object) +type DefaultMethods = { [key: string]: (this: V, ...args: any[]) => any } +type DefaultComputed = { [key: string]: any } + +export type VueProxy< + PropsOptions, + RawBindings, + Data = DefaultData, + Computed extends ComputedOptions = DefaultComputed, + Methods extends MethodOptions = DefaultMethods, + Mixin = {}, + Extends = {}, + Emits extends EmitsOptions = {} +> = Vue2ComponentOptions< + Vue, + ShallowUnwrapRef & Data, + Methods, + Computed, + PropsOptions, + ExtractPropTypes +> & + VueConstructorProxy< + PropsOptions, + RawBindings, + Data, + Computed, + Methods, + Mixin, + Extends, + Emits + > + +// public properties exposed on the proxy, which is used as the render context +// in templates (as `this` in the render option) +export type ComponentPublicInstance< + P = {}, // props type extracted from props option + B = {}, // raw bindings returned from setup() + D = {}, // return from data() + C extends ComputedOptions = {}, + M extends MethodOptions = {}, + E extends EmitsOptions = {}, + PublicProps = P, + Defaults = {}, + MakeDefaultsOptional extends boolean = false +> = { + $: ComponentInternalInstance + $data: D + $props: MakeDefaultsOptional extends true + ? Partial & Omit

+ : P & PublicProps + $attrs: Data + $refs: Data + $slots: Slots + $root: ComponentPublicInstance | null + $parent: ComponentPublicInstance | null + $emit: EmitFn + $el: any + // $options: Options & MergedComponentOptionsOverride + $forceUpdate: () => void + $nextTick: typeof nextTick + $watch( + source: string | Function, + cb: Function, + options?: WatchOptions + ): WatchStopHandle +} & P & + ShallowUnwrapRef & + UnwrapNestedRefs & + ExtractComputedReturns & + M diff --git a/src/component/defineAsyncComponent.ts b/src/component/defineAsyncComponent.ts new file mode 100644 index 00000000..a5c40894 --- /dev/null +++ b/src/component/defineAsyncComponent.ts @@ -0,0 +1,128 @@ +import { isFunction, isObject, warn } from '../utils' +import { VueProxy } from './componentProxy' +import { AsyncComponent } from 'vue' + +import { + ComponentOptionsWithoutProps, + ComponentOptionsWithArrayProps, + ComponentOptionsWithProps, +} from './componentOptions' + +type Component = VueProxy + +type ComponentOrComponentOptions = + // Component + | Component + // ComponentOptions + | ComponentOptionsWithoutProps + | ComponentOptionsWithArrayProps + | ComponentOptionsWithProps + +export type AsyncComponentResolveResult = + | T + | { default: T } // es modules + +export type AsyncComponentLoader = () => Promise + +export interface AsyncComponentOptions { + loader: AsyncComponentLoader + loadingComponent?: ComponentOrComponentOptions + errorComponent?: ComponentOrComponentOptions + delay?: number + timeout?: number + suspensible?: boolean + onError?: ( + error: Error, + retry: () => void, + fail: () => void, + attempts: number + ) => any +} + +export function defineAsyncComponent( + source: AsyncComponentLoader | AsyncComponentOptions +): AsyncComponent { + if (isFunction(source)) { + source = { loader: source } + } + + const { + loader, + loadingComponent, + errorComponent, + delay = 200, + timeout, // undefined = never times out + suspensible = false, // in Vue 3 default is true + onError: userOnError, + } = source + + if (__DEV__ && suspensible) { + warn( + `The suspensiblbe option for async components is not supported in Vue2. It is ignored.` + ) + } + + let pendingRequest: Promise | null = null + + let retries = 0 + const retry = () => { + retries++ + pendingRequest = null + return load() + } + + const load = (): Promise => { + let thisRequest: Promise + return ( + pendingRequest || + (thisRequest = pendingRequest = + loader() + .catch((err) => { + err = err instanceof Error ? err : new Error(String(err)) + if (userOnError) { + return new Promise((resolve, reject) => { + const userRetry = () => resolve(retry()) + const userFail = () => reject(err) + userOnError(err, userRetry, userFail, retries + 1) + }) + } else { + throw err + } + }) + .then((comp: any) => { + if (thisRequest !== pendingRequest && pendingRequest) { + return pendingRequest + } + if (__DEV__ && !comp) { + warn( + `Async component loader resolved to undefined. ` + + `If you are using retry(), make sure to return its return value.` + ) + } + // interop module default + if ( + comp && + (comp.__esModule || comp[Symbol.toStringTag] === 'Module') + ) { + comp = comp.default + } + if (__DEV__ && comp && !isObject(comp) && !isFunction(comp)) { + throw new Error(`Invalid async component load result: ${comp}`) + } + return comp + })) + ) + } + + return () => { + const component = load() + + return { + component, + delay, + timeout, + error: errorComponent, + loading: loadingComponent, + } + } +} diff --git a/src/component/defineComponent.ts b/src/component/defineComponent.ts new file mode 100644 index 00000000..a6a035f7 --- /dev/null +++ b/src/component/defineComponent.ts @@ -0,0 +1,118 @@ +import { ComponentPropsOptions } from './componentProps' +import { + MethodOptions, + ComputedOptions, + ComponentOptionsWithoutProps, + ComponentOptionsWithArrayProps, + ComponentOptionsWithProps, +} from './componentOptions' +import { VueProxy } from './componentProxy' +import { Data } from './common' +import { HasDefined } from '../types/basic' +import { EmitsOptions } from '../runtimeContext' + +/** + * overload 1: object format with no props + */ +export function defineComponent< + RawBindings, + D = Data, + C extends ComputedOptions = {}, + M extends MethodOptions = {}, + Mixin = {}, + Extends = {}, + Emits extends EmitsOptions = {} +>( + options: ComponentOptionsWithoutProps< + {}, + RawBindings, + D, + C, + M, + Mixin, + Extends, + Emits + > +): VueProxy<{}, RawBindings, D, C, M, Mixin, Extends, Emits> +/** + * overload 2: object format with array props declaration + * props inferred as `{ [key in PropNames]?: any }` + * + * return type is for Vetur and TSX support + */ +export function defineComponent< + PropNames extends string, + RawBindings = Data, + D = Data, + C extends ComputedOptions = {}, + M extends MethodOptions = {}, + Mixin = {}, + Extends = {}, + Emits extends EmitsOptions = {}, + PropsOptions extends ComponentPropsOptions = ComponentPropsOptions +>( + options: ComponentOptionsWithArrayProps< + PropNames, + RawBindings, + D, + C, + M, + Mixin, + Extends, + Emits + > +): VueProxy< + Readonly<{ [key in PropNames]?: any }>, + RawBindings, + D, + C, + M, + Mixin, + Extends, + Emits +> + +/** + * overload 3: object format with object props declaration + * + * see `ExtractPropTypes` in './componentProps.ts' + */ +export function defineComponent< + Props, + RawBindings = Data, + D = Data, + C extends ComputedOptions = {}, + M extends MethodOptions = {}, + Mixin = {}, + Extends = {}, + Emits extends EmitsOptions = {}, + PropsOptions extends ComponentPropsOptions = ComponentPropsOptions +>( + options: HasDefined extends true + ? ComponentOptionsWithProps< + PropsOptions, + RawBindings, + D, + C, + M, + Mixin, + Extends, + Emits, + Props + > + : ComponentOptionsWithProps< + PropsOptions, + RawBindings, + D, + C, + M, + Mixin, + Extends, + Emits + > +): VueProxy + +// implementation, close to no-op +export function defineComponent(options: any) { + return options as any +} diff --git a/src/component/directives.ts b/src/component/directives.ts new file mode 100644 index 00000000..44d91da4 --- /dev/null +++ b/src/component/directives.ts @@ -0,0 +1,29 @@ +import type { VNodeDirective, VNode } from 'vue' + +export type DirectiveModifiers = Record + +export interface DirectiveBinding extends Readonly { + readonly modifiers: DirectiveModifiers + readonly value: V + readonly oldValue: V | null +} + +export type DirectiveHook = ( + el: T, + binding: DirectiveBinding, + vnode: VNode, + prevVNode: Prev +) => void + +export interface ObjectDirective { + bind?: DirectiveHook + inserted?: DirectiveHook + update?: DirectiveHook + componentUpdated?: DirectiveHook + unbind?: DirectiveHook +} +export type FunctionDirective = DirectiveHook + +export type Directive = + | ObjectDirective + | FunctionDirective diff --git a/src/component/index.ts b/src/component/index.ts new file mode 100644 index 00000000..99a9f45e --- /dev/null +++ b/src/component/index.ts @@ -0,0 +1,29 @@ +export { defineComponent } from './defineComponent' +export { defineAsyncComponent } from './defineAsyncComponent' +export { + SetupFunction, + ComputedOptions, + MethodOptions, + ComponentPropsOptions, +} from './componentOptions' +export { + ComponentInstance, + ComponentPublicInstance, + ComponentRenderProxy, +} from './componentProxy' +export { Data } from './common' +export { + PropType, + PropOptions, + ExtractPropTypes, + ExtractDefaultPropTypes, +} from './componentProps' + +export { + DirectiveModifiers, + DirectiveBinding, + DirectiveHook, + ObjectDirective, + FunctionDirective, + Directive, +} from './directives' diff --git a/src/env.d.ts b/src/env.d.ts index 2be58b81..0433fb7a 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -1,27 +1,33 @@ -import { VueConstructor } from 'vue'; +import { VueConstructor } from 'vue' +import { VfaState } from './utils/vmStateManager' +import { VueWatcher } from './apis/watch' declare global { interface Window { - Vue: VueConstructor; + Vue: VueConstructor } } declare module 'vue/types/vue' { interface Vue { - readonly _data: Record; + readonly _uid: number + readonly _data: Record + _watchers: VueWatcher[] + _provided: Record + __composition_api_state__?: VfaState } interface VueConstructor { - observable(x: any): T; + observable(x: any): T util: { - warn(msg: string, vm?: Vue); + warn(msg: string, vm?: Vue | null) defineReactive( obj: Object, key: string, val: any, customSetter?: Function, shallow?: boolean - ); - }; + ) + } } } diff --git a/src/functions/computed.ts b/src/functions/computed.ts deleted file mode 100644 index 6cd0c6cd..00000000 --- a/src/functions/computed.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { compoundComputed } from '../helper'; -import { Wrapper, ComputedWrapper } from '../wrappers'; - -export function computed(getter: () => T, setter?: (x: T) => void): Wrapper { - const computedHost = compoundComputed({ - $$state: { - get: getter, - set: setter, - }, - }); - - return new ComputedWrapper({ - read: () => computedHost.$$state, - ...(setter && { - write: (v: T) => { - computedHost.$$state = v; - }, - }), - }); -} diff --git a/src/functions/inject.ts b/src/functions/inject.ts deleted file mode 100644 index 2b26e2b7..00000000 --- a/src/functions/inject.ts +++ /dev/null @@ -1,54 +0,0 @@ -import Vue from 'vue'; -import { getCurrentVue } from '../runtimeContext'; -import { state } from '../functions/state'; -import { isWrapper, Wrapper, ComputedWrapper } from '../wrappers'; -import { ensureCurrentVMInFn } from '../helper'; -import { hasOwn } from '../utils'; - -const UNRESOLVED_INJECT = {}; -export interface Key extends Symbol {} - -function resolveInject(provideKey: Key, vm: Vue): any { - let source = vm; - while (source) { - // @ts-ignore - if (source._provided && hasOwn(source._provided, provideKey)) { - //@ts-ignore - return source._provided[provideKey]; - } - source = source.$parent; - } - - return UNRESOLVED_INJECT; -} - -export function provide(key: Key, value: T | Wrapper) { - const vm: any = ensureCurrentVMInFn('provide'); - if (!vm._provided) { - vm._provided = {}; - } - vm._provided[key as any] = value; -} - -export function inject(key: Key): Wrapper | void { - if (!key) { - return; - } - - const vm = ensureCurrentVMInFn('inject'); - const val = resolveInject(key, vm); - if (val !== UNRESOLVED_INJECT) { - if (isWrapper(val)) { - return val; - } - const reactiveVal = state(val); - return new ComputedWrapper({ - read: () => reactiveVal, - write() { - getCurrentVue().util.warn(`The injectd value can't be re-assigned`, vm); - }, - }); - } else if (process.env.NODE_ENV !== 'production') { - getCurrentVue().util.warn(`Injection "${String(key)}" not found`, vm); - } -} diff --git a/src/functions/lifecycle.ts b/src/functions/lifecycle.ts deleted file mode 100644 index d0e1b913..00000000 --- a/src/functions/lifecycle.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ensureCurrentVMInFn } from '../helper'; - -const genName = (name: string) => `on${name[0].toUpperCase() + name.slice(1)}`; -function createLifeCycle(lifeCyclehook: string) { - return (callback: Function) => { - const vm = ensureCurrentVMInFn(genName(lifeCyclehook)); - vm.$on(`hook:${lifeCyclehook}`, callback); - }; -} - -function createLifeCycles(lifeCyclehooks: string[], name: string) { - return (callback: Function) => { - const vm = ensureCurrentVMInFn(name); - lifeCyclehooks.forEach(lifeCyclehook => vm.$on(`hook:${lifeCyclehook}`, callback)); - }; -} - -export const onCreated = createLifeCycle('created'); -export const onBeforeMount = createLifeCycle('beforeMount'); -export const onMounted = createLifeCycle('mounted'); -export const onBeforeUpdate = createLifeCycle('beforeUpdate'); -export const onUpdated = createLifeCycle('updated'); -export const onActivated = createLifeCycle('activated'); -export const onDeactivated = createLifeCycle('deactivated'); -export const onBeforeDestroy = createLifeCycle('beforeDestroy'); -export const onDestroyed = createLifeCycle('destroyed'); -export const onErrorCaptured = createLifeCycle('errorCaptured'); - -// only one event will be fired between destroyed and deactivated when an unmount occurs -export const onUnmounted = createLifeCycles(['destroyed', 'deactivated'], genName('unmounted')); diff --git a/src/functions/state.ts b/src/functions/state.ts deleted file mode 100644 index 89786a23..00000000 --- a/src/functions/state.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Wrapper, ValueWrapper } from '../wrappers'; -import { observable } from '../reactivity'; - -export function state(value: T): T { - return observable(value); -} - -export function value(value: T): Wrapper { - return new ValueWrapper(state({ $$state: value })); -} diff --git a/src/functions/watch.ts b/src/functions/watch.ts deleted file mode 100644 index 7ff766a6..00000000 --- a/src/functions/watch.ts +++ /dev/null @@ -1,264 +0,0 @@ -import Vue, { VueConstructor } from 'vue'; -import { Wrapper } from '../wrappers'; -import { isArray, assert } from '../utils'; -import { isWrapper } from '../wrappers'; -import { getCurrentVM, getCurrentVue } from '../runtimeContext'; -import { WatcherPreFlushQueueKey, WatcherPostFlushQueueKey } from '../symbols'; - -const initValue = {}; -type InitValue = typeof initValue; -type watcherCallBack = (newVal: T, oldVal: T) => void; -type watchedValue = Wrapper | (() => T); -type FlushMode = 'pre' | 'post' | 'sync'; -interface WatcherOption { - lazy: boolean; - deep: boolean; - flush: FlushMode; -} -interface WatcherContext { - getter: () => T; - value: T | InitValue; - oldValue: T | InitValue; - watcherStopHandle: Function; -} - -let fallbackVM: Vue; - -function hasWatchEnv(vm: any) { - return vm[WatcherPreFlushQueueKey] !== undefined; -} - -function installWatchEnv(vm: any) { - vm[WatcherPreFlushQueueKey] = []; - vm[WatcherPostFlushQueueKey] = []; - vm.$on('hook:beforeUpdate', createFlusher(WatcherPreFlushQueueKey)); - vm.$on('hook:updated', createFlusher(WatcherPostFlushQueueKey)); -} - -function createFlusher(key: any) { - return function(this: any) { - flushQueue(this, key); - }; -} - -function flushQueue(vm: any, key: any) { - const queue = vm[key]; - for (let index = 0; index < queue.length; index++) { - queue[index](); - } - queue.length = 0; -} - -function flushWatcherCallback(vm: any, fn: Function, mode: FlushMode) { - // flush all when beforeUpdate and updated are not fired - function fallbackFlush() { - vm.$nextTick(() => { - if (vm[WatcherPreFlushQueueKey].length) { - flushQueue(vm, WatcherPreFlushQueueKey); - } - if (vm[WatcherPostFlushQueueKey].length) { - flushQueue(vm, WatcherPostFlushQueueKey); - } - }); - } - - switch (mode) { - case 'pre': - fallbackFlush(); - vm[WatcherPreFlushQueueKey].push(fn); - break; - case 'post': - fallbackFlush(); - vm[WatcherPostFlushQueueKey].push(fn); - break; - case 'sync': - fn(); - break; - default: - assert(false, `flush must be one of ["post", "pre", "sync"], but got ${mode}`); - break; - } -} - -function createSingleSourceWatcher( - vm: InstanceType, - source: watchedValue, - cb: watcherCallBack, - options: WatcherOption -): () => void { - let getter: () => T; - if (isWrapper(source)) { - getter = () => source.value; - } else { - getter = source as () => T; - } - - let callbackRef = (n: T, o: T) => { - callbackRef = flush; - - if (!options.lazy) { - cb(n, o); - } else { - flush(n, o); - } - }; - - const flush = (n: T, o: T) => { - flushWatcherCallback( - vm, - () => { - cb(n, o); - }, - options.flush - ); - }; - - return vm.$watch( - getter, - (n: T, o: T) => { - callbackRef(n, o); - }, - { - immediate: !options.lazy, - deep: options.deep, - // @ts-ignore - sync: options.flush === 'sync', - } - ); -} - -function createMuiltSourceWatcher( - vm: InstanceType, - sources: Array>, - cb: watcherCallBack, - options: WatcherOption -): () => void { - let execCallbackAfterNumRun: false | number = options.lazy ? false : sources.length; - let pendingCallback = false; - const watcherContext: Array> = []; - - function execCallback() { - cb.apply( - vm, - watcherContext.reduce<[T[], T[]]>( - (acc, ctx) => { - acc[0].push((ctx.value === initValue ? ctx.getter() : ctx.value) as T); - acc[1].push((ctx.oldValue === initValue ? undefined : ctx.oldValue) as T); - return acc; - }, - [[], []] - ) - ); - } - function stop() { - watcherContext.forEach(ctx => ctx.watcherStopHandle()); - } - - let callbackRef = () => { - if (execCallbackAfterNumRun !== false) { - if (--execCallbackAfterNumRun === 0) { - execCallbackAfterNumRun = false; - callbackRef = flush; - execCallback(); - } - } else { - callbackRef = flush; - flush(); - } - }; - - const flush = () => { - if (!pendingCallback) { - pendingCallback = true; - vm.$nextTick(() => { - flushWatcherCallback( - vm, - () => { - pendingCallback = false; - execCallback(); - }, - options.flush - ); - }); - } - }; - - sources.forEach(source => { - let getter: () => T; - if (isWrapper(source)) { - getter = () => source.value; - } else { - getter = source as () => T; - } - const watcherCtx = { - getter, - value: initValue, - oldValue: initValue, - } as WatcherContext; - // must push watcherCtx before create watcherStopHandle - watcherContext.push(watcherCtx); - - watcherCtx.watcherStopHandle = vm.$watch( - getter, - (n: T, o: T) => { - watcherCtx.value = n; - watcherCtx.oldValue = o; - - callbackRef(); - }, - { - immediate: !options.lazy, - deep: options.deep, - // @ts-ignore - // always set to true, so we can fully control the schedule - sync: true, - } - ); - }); - - return stop; -} - -export function watch( - source: watchedValue, - cb: watcherCallBack, - options?: Partial -): () => void; -export function watch( - source: Array>, - cb: watcherCallBack, - options?: Partial -): () => void; -export function watch( - source: watchedValue | Array>, - cb: watcherCallBack | watcherCallBack, - options: Partial = {} -): () => void { - const opts: WatcherOption = { - ...{ - lazy: false, - deep: false, - flush: 'post', - }, - ...options, - }; - let vm = getCurrentVM(); - if (!vm) { - if (!fallbackVM) { - const Vue = getCurrentVue(); - const silent = Vue.config.silent; - Vue.config.silent = true; - fallbackVM = new Vue(); - Vue.config.silent = silent; - } - vm = fallbackVM; - opts.flush = 'sync'; - } - - if (!hasWatchEnv(vm)) installWatchEnv(vm); - - if (isArray(source)) { - return createMuiltSourceWatcher(vm, source, cb as watcherCallBack, opts); - } - return createSingleSourceWatcher(vm, source, cb as watcherCallBack, opts); -} diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 00000000..9bc2c044 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,3 @@ +// Global compile-time constants +declare const __DEV__: boolean +declare const __VERSION__: string diff --git a/src/helper.ts b/src/helper.ts deleted file mode 100644 index 9753f8dc..00000000 --- a/src/helper.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { VueConstructor } from 'vue'; -import { getCurrentVue, getCurrentVM } from './runtimeContext'; -import { assert } from './utils'; - -export function ensureCurrentVMInFn(hook: string): InstanceType { - const vm = getCurrentVM(); - if (process.env.NODE_ENV !== 'production') { - assert(vm, `"${hook}" get called outside of "setup()"`); - } - return vm!; -} - -export function compoundComputed(computed: { - [key: string]: - | (() => any) - | { - get?: () => any; - set?: (v: any) => void; - }; -}) { - const Vue = getCurrentVue(); - const silent = Vue.config.silent; - Vue.config.silent = true; - const reactive = new Vue({ - computed, - }); - Vue.config.silent = silent; - return reactive; -} diff --git a/src/index.ts b/src/index.ts index 2db00fab..3b1632d0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,35 +1,26 @@ -import Vue, { VueConstructor } from 'vue'; -import { SetupContext } from './types/vue'; -import { currentVue } from './runtimeContext'; -import { Wrapper } from './wrappers'; -import { install } from './install'; -import { mixin } from './setup'; +import type Vue from 'vue' +import { Data, SetupFunction } from './component' +import { Plugin } from './install' + +export const version = __VERSION__ + +export * from './apis' +export * from './component' +export { + getCurrentInstance, + ComponentInternalInstance, + SetupContext, +} from './runtimeContext' + +export default Plugin declare module 'vue/types/options' { interface ComponentOptions { - setup?: ( - this: void, - props: { [x: string]: any }, - context: SetupContext - ) => object | null | undefined | void; + setup?: SetupFunction } } -const _install = (Vue: VueConstructor) => install(Vue, mixin); -const plugin = { - install: _install, -}; -// Auto install if it is not done yet and `window` has `Vue`. -// To allow users to avoid auto-installation in some cases, -if (currentVue && typeof window !== 'undefined' && window.Vue) { - _install(window.Vue); +// auto install when using CDN +if (typeof window !== 'undefined' && window.Vue) { + window.Vue.use(Plugin) } - -export { plugin, Wrapper }; -export { set } from './reactivity'; -export * from './ts-api'; -export * from './functions/state'; -export * from './functions/lifecycle'; -export * from './functions/watch'; -export * from './functions/computed'; -export * from './functions/inject'; diff --git a/src/install.ts b/src/install.ts index ebbd41f2..fd8a56e6 100644 --- a/src/install.ts +++ b/src/install.ts @@ -1,56 +1,82 @@ -import { AnyObject } from './types/basic'; -import { hasSymbol, hasOwn, isPlainObject, assert } from './utils'; -import { isWrapper } from './wrappers'; -import { setCurrentVue, currentVue } from './runtimeContext'; -import { VueConstructor } from 'vue'; +import type { VueConstructor } from 'vue' +import { AnyObject } from './types/basic' +import { isFunction, hasSymbol, hasOwn, isPlainObject, warn } from './utils' +import { isRef } from './reactivity' +import { setVueConstructor, isVueRegistered } from './runtimeContext' +import { mixin } from './mixin' /** * Helper that recursively merges two data objects together. */ -function mergeData(to: AnyObject, from?: AnyObject): Object { - if (!from) return to; - let key: any; - let toVal: any; - let fromVal: any; +function mergeData(from: AnyObject, to: AnyObject): Object { + if (!from) return to + if (!to) return from - const keys = hasSymbol ? Reflect.ownKeys(from) : Object.keys(from); + let key: any + let toVal: any + let fromVal: any + + const keys = hasSymbol ? Reflect.ownKeys(from) : Object.keys(from) for (let i = 0; i < keys.length; i++) { - key = keys[i]; + key = keys[i] // in case the object is already observed... - if (key === '__ob__') continue; - toVal = to[key]; - fromVal = from[key]; + if (key === '__ob__') continue + toVal = to[key] + fromVal = from[key] if (!hasOwn(to, key)) { - to[key] = fromVal; + to[key] = fromVal } else if ( toVal !== fromVal && - (isPlainObject(toVal) && !isWrapper(toVal)) && - (isPlainObject(fromVal) && !isWrapper(toVal)) + isPlainObject(toVal) && + !isRef(toVal) && + isPlainObject(fromVal) && + !isRef(fromVal) ) { - mergeData(toVal, fromVal); + mergeData(fromVal, toVal) } } - return to; + return to } -export function install(Vue: VueConstructor, _install: (Vue: VueConstructor) => void) { - if (currentVue && currentVue === Vue) { - if (process.env.NODE_ENV !== 'production') { - assert(false, 'already installed. Vue.use(plugin) should be called only once'); +export function install(Vue: VueConstructor) { + if (isVueRegistered(Vue)) { + if (__DEV__) { + warn( + '[vue-composition-api] already installed. Vue.use(VueCompositionAPI) should be called only once.' + ) } - return; + return } - Vue.config.optionMergeStrategies.setup = function(parent: Function, child: Function) { + if (__DEV__) { + if (Vue.version) { + if (Vue.version[0] !== '2' || Vue.version[1] !== '.') { + warn( + `[vue-composition-api] only works with Vue 2, v${Vue.version} found.` + ) + } + } else { + warn('[vue-composition-api] no Vue version found') + } + } + + Vue.config.optionMergeStrategies.setup = function ( + parent: Function, + child: Function + ) { return function mergedSetupFn(props: any, context: any) { return mergeData( - typeof child === 'function' ? child(props, context) || {} : {}, - typeof parent === 'function' ? parent(props, context) || {} : {} - ); - }; - }; - - setCurrentVue(Vue); - _install(Vue); + isFunction(parent) ? parent(props, context) || {} : undefined, + isFunction(child) ? child(props, context) || {} : undefined + ) + } + } + + setVueConstructor(Vue) + mixin(Vue) +} + +export const Plugin = { + install: (Vue: VueConstructor) => install(Vue), } diff --git a/src/mixin.ts b/src/mixin.ts new file mode 100644 index 00000000..dc100fda --- /dev/null +++ b/src/mixin.ts @@ -0,0 +1,250 @@ +import type { VueConstructor } from 'vue' +import { ComponentInstance, SetupFunction, Data } from './component' +import { isRef, isReactive, toRefs, isRaw } from './reactivity' +import { + isPlainObject, + assert, + proxy, + warn, + isFunction, + isObject, + def, + isArray, +} from './utils' +import { ref } from './apis' +import vmStateManager from './utils/vmStateManager' +import { + afterRender, + activateCurrentInstance, + resolveScopedSlots, + asVmProperty, + updateVmAttrs, +} from './utils/instance' +import { + getVueConstructor, + SetupContext, + toVue3ComponentInstance, +} from './runtimeContext' +import { createObserver } from './reactivity/reactive' + +export function mixin(Vue: VueConstructor) { + Vue.mixin({ + beforeCreate: functionApiInit, + mounted(this: ComponentInstance) { + afterRender(this) + }, + beforeUpdate() { + updateVmAttrs(this as ComponentInstance) + }, + updated(this: ComponentInstance) { + afterRender(this) + }, + }) + + /** + * Vuex init hook, injected into each instances init hooks list. + */ + + function functionApiInit(this: ComponentInstance) { + const vm = this + const $options = vm.$options + const { setup, render } = $options + + if (render) { + // keep currentInstance accessible for createElement + $options.render = function (...args: any): any { + return activateCurrentInstance(toVue3ComponentInstance(vm), () => + render.apply(this, args) + ) + } + } + + if (!setup) { + return + } + if (!isFunction(setup)) { + if (__DEV__) { + warn( + 'The "setup" option should be a function that returns a object in component definitions.', + vm + ) + } + return + } + + const { data } = $options + // wrapper the data option, so we can invoke setup before data get resolved + $options.data = function wrappedData() { + initSetup(vm, vm.$props) + return isFunction(data) + ? ( + data as (this: ComponentInstance, x: ComponentInstance) => object + ).call(vm, vm) + : data || {} + } + } + + function initSetup(vm: ComponentInstance, props: Record = {}) { + const setup = vm.$options.setup! + const ctx = createSetupContext(vm) + const instance = toVue3ComponentInstance(vm) + instance.setupContext = ctx + + // fake reactive for `toRefs(props)` + def(props, '__ob__', createObserver()) + + // resolve scopedSlots and slots to functions + resolveScopedSlots(vm, ctx.slots) + + let binding: ReturnType> | undefined | null + activateCurrentInstance(instance, () => { + // make props to be fake reactive, this is for `toRefs(props)` + binding = setup(props, ctx) + }) + + if (!binding) return + if (isFunction(binding)) { + // keep typescript happy with the binding type. + const bindingFunc = binding + // keep currentInstance accessible for createElement + vm.$options.render = () => { + resolveScopedSlots(vm, ctx.slots) + return activateCurrentInstance(instance, () => bindingFunc()) + } + return + } else if (isObject(binding)) { + if (isReactive(binding)) { + binding = toRefs(binding) as Data + } + + vmStateManager.set(vm, 'rawBindings', binding) + const bindingObj = binding + + Object.keys(bindingObj).forEach((name) => { + let bindingValue: any = bindingObj[name] + + if (!isRef(bindingValue)) { + if (!isReactive(bindingValue)) { + if (isFunction(bindingValue)) { + const copy = bindingValue + bindingValue = bindingValue.bind(vm) + Object.keys(copy).forEach(function (ele) { + bindingValue[ele] = copy[ele] + }) + } else if (!isObject(bindingValue)) { + bindingValue = ref(bindingValue) + } else if (hasReactiveArrayChild(bindingValue)) { + // creates a custom reactive properties without make the object explicitly reactive + // NOTE we should try to avoid this, better implementation needed + customReactive(bindingValue) + } + } else if (isArray(bindingValue)) { + bindingValue = ref(bindingValue) + } + } + asVmProperty(vm, name, bindingValue) + }) + + return + } + + if (__DEV__) { + assert( + false, + `"setup" must return a "Object" or a "Function", got "${Object.prototype.toString + .call(binding) + .slice(8, -1)}"` + ) + } + } + + function customReactive(target: object, seen = new Set()) { + if (seen.has(target)) return + if ( + !isPlainObject(target) || + isRef(target) || + isReactive(target) || + isRaw(target) + ) + return + const Vue = getVueConstructor() + // @ts-expect-error https://github.com/vuejs/vue/pull/12132 + const defineReactive = Vue.util.defineReactive + + Object.keys(target).forEach((k) => { + const val = target[k] + defineReactive(target, k, val) + if (val) { + seen.add(val) + customReactive(val, seen) + } + return + }) + } + + function hasReactiveArrayChild(target: object, visited = new Map()): boolean { + if (visited.has(target)) { + return visited.get(target) + } + visited.set(target, false) + if (isArray(target) && isReactive(target)) { + visited.set(target, true) + return true + } + + if (!isPlainObject(target) || isRaw(target) || isRef(target)) { + return false + } + return Object.keys(target).some((x) => + hasReactiveArrayChild(target[x], visited) + ) + } + + function createSetupContext( + vm: ComponentInstance & { [x: string]: any } + ): SetupContext { + const ctx = { slots: {} } as SetupContext + + const propsPlain = [ + 'root', + 'parent', + 'refs', + 'listeners', + 'isServer', + 'ssrContext', + ] + const methodReturnVoid = ['emit'] + + propsPlain.forEach((key) => { + let srcKey = `$${key}` + proxy(ctx, key, { + get: () => vm[srcKey], + set() { + __DEV__ && + warn( + `Cannot assign to '${key}' because it is a read-only property`, + vm + ) + }, + }) + }) + + updateVmAttrs(vm, ctx) + + methodReturnVoid.forEach((key) => { + const srcKey = `$${key}` + proxy(ctx, key, { + get() { + return (...args: any[]) => { + const fn: Function = vm[srcKey] + fn.apply(vm, args) + } + }, + }) + }) + if (process.env.NODE_ENV === 'test') { + ;(ctx as any)._vm = vm + } + return ctx + } +} diff --git a/src/reactivity/del.ts b/src/reactivity/del.ts new file mode 100644 index 00000000..a433142f --- /dev/null +++ b/src/reactivity/del.ts @@ -0,0 +1,44 @@ +import { AnyObject } from '../types/basic' +import { getVueConstructor } from '../runtimeContext' +import { + hasOwn, + isPrimitive, + isUndef, + isArray, + isValidArrayIndex, +} from '../utils' + +/** + * Delete a property and trigger change if necessary. + */ +export function del(target: AnyObject, key: any) { + const Vue = getVueConstructor() + const { warn } = Vue.util + + if (__DEV__ && (isUndef(target) || isPrimitive(target))) { + warn( + `Cannot delete reactive property on undefined, null, or primitive value: ${target}` + ) + } + if (isArray(target) && isValidArrayIndex(key)) { + target.splice(key, 1) + return + } + const ob = target.__ob__ + if (target._isVue || (ob && ob.vmCount)) { + __DEV__ && + warn( + 'Avoid deleting properties on a Vue instance or its root $data ' + + '- just set it to null.' + ) + return + } + if (!hasOwn(target, key)) { + return + } + delete target[key] + if (!ob) { + return + } + ob.dep.notify() +} diff --git a/src/reactivity/force.ts b/src/reactivity/force.ts new file mode 100644 index 00000000..727e0f5f --- /dev/null +++ b/src/reactivity/force.ts @@ -0,0 +1,9 @@ +let _isForceTrigger = false + +export function isForceTrigger() { + return _isForceTrigger +} + +export function setForceTrigger(v: boolean) { + _isForceTrigger = v +} diff --git a/src/reactivity/index.ts b/src/reactivity/index.ts index 80cbc53a..c9b2ea19 100644 --- a/src/reactivity/index.ts +++ b/src/reactivity/index.ts @@ -1,2 +1,34 @@ -export { observable } from './observable'; -export { set } from './set'; +export { + reactive, + isReactive, + markRaw, + shallowReactive, + toRaw, + isRaw, +} from './reactive' +export { + ref, + customRef, + isRef, + createRef, + toRefs, + toRef, + unref, + shallowRef, + triggerRef, + proxyRefs, +} from './ref' +export { readonly, isReadonly, shallowReadonly } from './readonly' +export { set } from './set' +export { del } from './del' + +export type { + Ref, + ComputedRef, + WritableComputedRef, + ToRefs, + UnwrapRef, + UnwrapRefSimple, + ShallowUnwrapRef, +} from './ref' +export type { DeepReadonly, UnwrapNestedRefs } from './readonly' diff --git a/src/reactivity/observable.ts b/src/reactivity/observable.ts deleted file mode 100644 index e5a9f553..00000000 --- a/src/reactivity/observable.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { AnyObject } from '../types/basic'; -import { getCurrentVue } from '../runtimeContext'; -import { isObject, def, hasOwn } from '../utils'; -import { isWrapper } from '../wrappers'; -import { ObservableIdentifierKey, AccessControIdentifierlKey } from '../symbols'; - -const AccessControlIdentifier = {}; -const ObservableIdentifier = {}; - -function setupAccessControl(target: AnyObject) { - if (!isObject(target) || isWrapper(target)) { - return; - } - - if ( - hasOwn(target, AccessControIdentifierlKey) && - target[AccessControIdentifierlKey] === AccessControlIdentifier - ) { - return; - } - - def(target, AccessControIdentifierlKey, AccessControlIdentifier); - const keys = Object.keys(target); - for (let i = 0; i < keys.length; i++) { - defineAccessControl(target, keys[i]); - } -} - -/** - * Auto unwrapping when acccess property - */ -export function defineAccessControl(target: AnyObject, key: any, val?: any) { - let getter: (() => any) | undefined; - let setter: ((x: any) => void) | undefined; - const property = Object.getOwnPropertyDescriptor(target, key); - if (property) { - if (property.configurable === false) { - return; - } - getter = property.get; - setter = property.set; - val = target[key]; - } - - setupAccessControl(val); - Object.defineProperty(target, key, { - enumerable: true, - configurable: true, - get: function getterHandler() { - const value = getter ? getter.call(target) : val; - if (isWrapper(value)) { - return value.value; - } else { - return value; - } - }, - set: function setterHandler(newVal) { - if (getter && !setter) return; - - const value = getter ? getter.call(target) : val; - if (isWrapper(value)) { - if (isWrapper(newVal)) { - val = newVal; - } else { - value.value = newVal; - } - } else if (setter) { - setter.call(target, newVal); - } else if (isWrapper(newVal)) { - val = newVal; - } - setupAccessControl(newVal); - }, - }); -} - -export function isObservable(obj: any): boolean { - return ( - hasOwn(obj, ObservableIdentifierKey) && obj[ObservableIdentifierKey] === ObservableIdentifier - ); -} - -export function observable(obj: T): T { - if (!isObject(obj) || isObservable(obj)) { - return obj; - } - - const Vue = getCurrentVue(); - let observed: T; - if (Vue.observable) { - observed = Vue.observable(obj); - } else { - const silent = Vue.config.silent; - Vue.config.silent = true; - const vm = new Vue({ - data: { - $$state: obj, - }, - }); - Vue.config.silent = silent; - observed = vm._data.$$state; - } - - def(observed, ObservableIdentifierKey, ObservableIdentifier); - setupAccessControl(observed); - return observed; -} diff --git a/src/reactivity/reactive.ts b/src/reactivity/reactive.ts new file mode 100644 index 00000000..c15fc447 --- /dev/null +++ b/src/reactivity/reactive.ts @@ -0,0 +1,281 @@ +import { AnyObject } from '../types/basic' +import { getRegisteredVueOrDefault } from '../runtimeContext' +import { + isPlainObject, + def, + warn, + isArray, + hasOwn, + noopFn, + isObject, + proxy, +} from '../utils' +import { isComponentInstance, defineComponentInstance } from '../utils/helper' +import { RefKey } from '../utils/symbols' +import { isRef, UnwrapRef } from './ref' +import { rawSet, accessModifiedSet } from '../utils/sets' +import { isForceTrigger } from './force' + +const SKIPFLAG = '__v_skip' + +export function isRaw(obj: any): boolean { + return Boolean( + obj && + hasOwn(obj, '__ob__') && + typeof obj.__ob__ === 'object' && + obj.__ob__?.[SKIPFLAG] + ) +} + +export function isReactive(obj: any): boolean { + return Boolean( + obj && + hasOwn(obj, '__ob__') && + typeof obj.__ob__ === 'object' && + !obj.__ob__?.[SKIPFLAG] + ) +} + +/** + * Proxing property access of target. + * We can do unwrapping and other things here. + */ +function setupAccessControl(target: AnyObject): void { + if ( + !isPlainObject(target) || + isRaw(target) || + isArray(target) || + isRef(target) || + isComponentInstance(target) || + accessModifiedSet.has(target) + ) + return + + accessModifiedSet.set(target, true) + + const keys = Object.keys(target) + for (let i = 0; i < keys.length; i++) { + defineAccessControl(target, keys[i]) + } +} + +/** + * Auto unwrapping when access property + */ +export function defineAccessControl(target: AnyObject, key: any, val?: any) { + if (key === '__ob__') return + if (isRaw(target[key])) return + + let getter: (() => any) | undefined + let setter: ((x: any) => void) | undefined + const property = Object.getOwnPropertyDescriptor(target, key) + if (property) { + if (property.configurable === false) { + return + } + getter = property.get + setter = property.set + if ( + (!getter || setter) /* not only have getter */ && + arguments.length === 2 + ) { + val = target[key] + } + } + + setupAccessControl(val) + proxy(target, key, { + get: function getterHandler() { + const value = getter ? getter.call(target) : val + // if the key is equal to RefKey, skip the unwrap logic + if (key !== RefKey && isRef(value)) { + return value.value + } else { + return value + } + }, + set: function setterHandler(newVal: any) { + if (getter && !setter) return + + // If the key is equal to RefKey, skip the unwrap logic + // If and only if "value" is ref and "newVal" is not a ref, + // the assignment should be proxied to "value" ref. + if (key !== RefKey && isRef(val) && !isRef(newVal)) { + val.value = newVal + } else if (setter) { + setter.call(target, newVal) + val = newVal + } else { + val = newVal + } + setupAccessControl(newVal) + }, + }) +} + +export function observe(obj: T): T { + const Vue = getRegisteredVueOrDefault() + let observed: T + if (Vue.observable) { + observed = Vue.observable(obj) + } else { + const vm = defineComponentInstance(Vue, { + data: { + $$state: obj, + }, + }) + observed = vm._data.$$state + } + + // in SSR, there is no __ob__. Mock for reactivity check + if (!hasOwn(observed, '__ob__')) { + mockReactivityDeep(observed) + } + + return observed +} + +/** + * Mock __ob__ for object recursively + */ +export function mockReactivityDeep(obj: any, seen = new Set()) { + if (seen.has(obj) || hasOwn(obj, '__ob__') || !Object.isExtensible(obj)) + return + + def(obj, '__ob__', mockObserver(obj)) + seen.add(obj) + + for (const key of Object.keys(obj)) { + const value = obj[key] + if ( + !(isPlainObject(value) || isArray(value)) || + isRaw(value) || + !Object.isExtensible(value) + ) { + continue + } + mockReactivityDeep(value, seen) + } +} + +function mockObserver(value: any = {}): any { + return { + value, + dep: { + notify: noopFn, + depend: noopFn, + addSub: noopFn, + removeSub: noopFn, + }, + } +} + +export function createObserver() { + return observe({}).__ob__ +} + +export function shallowReactive(obj: T): T +export function shallowReactive(obj: any) { + if (!isObject(obj)) { + if (__DEV__) { + warn('"shallowReactive()" must be called on an object.') + } + return obj + } + + if ( + !(isPlainObject(obj) || isArray(obj)) || + isRaw(obj) || + !Object.isExtensible(obj) + ) { + return obj + } + + const observed = observe(isArray(obj) ? [] : {}) + + const ob = (observed as any).__ob__ + + for (const key of Object.keys(obj)) { + let val = obj[key] + let getter: (() => any) | undefined + let setter: ((x: any) => void) | undefined + const property = Object.getOwnPropertyDescriptor(obj, key) + if (property) { + if (property.configurable === false) { + continue + } + getter = property.get + setter = property.set + } + + proxy(observed, key, { + get: function getterHandler() { + ob.dep?.depend() + return val + }, + set: function setterHandler(newVal: any) { + if (getter && !setter) return + + if (!isForceTrigger() && val === newVal) return + if (setter) { + setter.call(obj, newVal) + } else { + val = newVal + } + ob.dep?.notify() + }, + }) + } + return observed +} + +/** + * Make obj reactivity + */ +export function reactive(obj: T): UnwrapRef { + if (!isObject(obj)) { + if (__DEV__) { + warn('"reactive()" must be called on an object.') + } + return obj + } + + if ( + !(isPlainObject(obj) || isArray(obj)) || + isRaw(obj) || + !Object.isExtensible(obj) + ) { + return obj as any + } + + const observed = observe(obj) + setupAccessControl(observed) + return observed as UnwrapRef +} + +/** + * Make sure obj can't be a reactive + */ +export function markRaw(obj: T): T { + if (!(isPlainObject(obj) || isArray(obj)) || !Object.isExtensible(obj)) { + return obj + } + + // set the vue observable flag at obj + const ob = createObserver() + ob[SKIPFLAG] = true + def(obj, '__ob__', ob) + + // mark as Raw + rawSet.set(obj, true) + + return obj +} + +export function toRaw(observed: T): T { + if (isRaw(observed) || !Object.isExtensible(observed)) { + return observed + } + + return (observed as any)?.__ob__?.value || observed +} diff --git a/src/reactivity/readonly.ts b/src/reactivity/readonly.ts new file mode 100644 index 00000000..537e8e74 --- /dev/null +++ b/src/reactivity/readonly.ts @@ -0,0 +1,107 @@ +import { reactive, Ref, UnwrapRefSimple } from '.' +import { isArray, isPlainObject, isObject, warn, proxy } from '../utils' +import { readonlySet } from '../utils/sets' +import { isReactive, observe } from './reactive' +import { isRef, RefImpl } from './ref' + +export function isReadonly(obj: any): boolean { + return readonlySet.has(obj) +} + +type Primitive = string | number | boolean | bigint | symbol | undefined | null +type Builtin = Primitive | Function | Date | Error | RegExp + +// prettier-ignore +export type DeepReadonly = T extends Builtin + ? T + : T extends Map + ? ReadonlyMap, DeepReadonly> + : T extends ReadonlyMap + ? ReadonlyMap, DeepReadonly> + : T extends WeakMap + ? WeakMap, DeepReadonly> + : T extends Set + ? ReadonlySet> + : T extends ReadonlySet + ? ReadonlySet> + : T extends WeakSet + ? WeakSet> + : T extends Promise + ? Promise> + : T extends {} + ? { readonly [K in keyof T]: DeepReadonly } + : Readonly + +// only unwrap nested ref +export type UnwrapNestedRefs = T extends Ref ? T : UnwrapRefSimple + +/** + * **In @vue/composition-api, `reactive` only provides type-level readonly check** + * + * Creates a readonly copy of the original object. Note the returned copy is not + * made reactive, but `readonly` can be called on an already reactive object. + */ +export function readonly( + target: T +): DeepReadonly> { + if (__DEV__ && !isObject(target)) { + warn(`value cannot be made reactive: ${String(target)}`) + } else { + readonlySet.set(target, true) + } + return target as any +} + +export function shallowReadonly(obj: T): Readonly +export function shallowReadonly(obj: any): any { + if (!isObject(obj)) { + if (__DEV__) { + warn(`value cannot be made reactive: ${String(obj)}`) + } + return obj + } + + if ( + !(isPlainObject(obj) || isArray(obj)) || + (!Object.isExtensible(obj) && !isRef(obj)) + ) { + return obj + } + + const readonlyObj = isRef(obj) + ? new RefImpl({} as any) + : isReactive(obj) + ? observe({}) + : {} + const source = reactive({}) + const ob = (source as any).__ob__ + + for (const key of Object.keys(obj)) { + let val = obj[key] + let getter: (() => any) | undefined + const property = Object.getOwnPropertyDescriptor(obj, key) + if (property) { + if (property.configurable === false && !isRef(obj)) { + continue + } + getter = property.get + } + + proxy(readonlyObj, key, { + get: function getterHandler() { + const value = getter ? getter.call(obj) : val + ob.dep.depend() + return value + }, + set(v: any) { + if (__DEV__) { + warn(`Set operation on key "${key}" failed: target is readonly.`) + } + }, + }) + } + + readonlySet.set(readonlyObj, true) + + return readonlyObj +} diff --git a/src/reactivity/ref.ts b/src/reactivity/ref.ts new file mode 100644 index 00000000..c035b445 --- /dev/null +++ b/src/reactivity/ref.ts @@ -0,0 +1,221 @@ +import { RefKey } from '../utils/symbols' +import { proxy, isPlainObject, warn, def } from '../utils' +import { reactive, isReactive, shallowReactive } from './reactive' +import { readonlySet } from '../utils/sets' +import { set } from './set' +import { setForceTrigger } from './force' + +declare const _refBrand: unique symbol +export interface Ref { + readonly [_refBrand]: true + value: T +} + +export interface WritableComputedRef extends Ref { + /** + * `effect` is added to be able to differentiate refs from computed properties. + * **Differently from Vue 3, it's just `true`**. This is because there is no equivalent + * of `ReactiveEffect` in `@vue/composition-api`. + */ + effect: true +} + +export interface ComputedRef extends WritableComputedRef { + readonly value: T +} + +export type ToRefs = { [K in keyof T]: Ref } + +export type CollectionTypes = IterableCollections | WeakCollections + +type IterableCollections = Map | Set +type WeakCollections = WeakMap | WeakSet + +// corner case when use narrows type +// Ex. type RelativePath = string & { __brand: unknown } +// RelativePath extends object -> true +type BaseTypes = string | number | boolean | Node | Window | Date + +export type ShallowUnwrapRef = { + [K in keyof T]: T[K] extends Ref ? V : T[K] +} + +export type UnwrapRef = T extends Ref + ? UnwrapRefSimple + : UnwrapRefSimple + +export type UnwrapRefSimple = T extends + | Function + | CollectionTypes + | BaseTypes + | Ref + ? T + : T extends Array + ? { [K in keyof T]: UnwrapRefSimple } + : T extends object + ? { + [P in keyof T]: P extends symbol ? T[P] : UnwrapRef + } + : T + +interface RefOption { + get(): T + set?(x: T): void +} +export class RefImpl implements Ref { + readonly [_refBrand]!: true + public value!: T + constructor({ get, set }: RefOption) { + proxy(this, 'value', { + get, + set, + }) + } +} + +export function createRef( + options: RefOption, + isReadonly = false, + isComputed = false +): RefImpl { + const r = new RefImpl(options) + + // add effect to differentiate refs from computed + if (isComputed) (r as ComputedRef).effect = true + + // seal the ref, this could prevent ref from being observed + // It's safe to seal the ref, since we really shouldn't extend it. + // related issues: #79 + const sealed = Object.seal(r) + + if (isReadonly) readonlySet.set(sealed, true) + + return sealed +} + +export function ref( + raw: T +): T extends Ref ? T : Ref> +export function ref(raw: T): Ref> +export function ref(): Ref +export function ref(raw?: unknown) { + if (isRef(raw)) { + return raw + } + + const value = reactive({ [RefKey]: raw }) + return createRef({ + get: () => value[RefKey] as any, + set: (v) => ((value[RefKey] as any) = v), + }) +} + +export function isRef(value: any): value is Ref { + return value instanceof RefImpl +} + +export function unref(ref: T | Ref): T { + return isRef(ref) ? (ref.value as any) : ref +} + +export function toRefs(obj: T): ToRefs { + if (__DEV__ && !isReactive(obj)) { + warn(`toRefs() expects a reactive object but received a plain one.`) + } + if (!isPlainObject(obj)) return obj + + const ret: any = {} + for (const key in obj) { + ret[key] = toRef(obj, key) + } + + return ret +} + +export type CustomRefFactory = ( + track: () => void, + trigger: () => void +) => { + get: () => T + set: (value: T) => void +} + +export function customRef(factory: CustomRefFactory): Ref { + const version = ref(0) + return createRef( + factory( + () => void version.value, + () => { + ++version.value + } + ) + ) +} + +export function toRef( + object: T, + key: K +): Ref { + if (!(key in object)) set(object, key, undefined) + const v = object[key] + if (isRef(v)) return v + + return createRef({ + get: () => object[key], + set: (v) => (object[key] = v), + }) +} + +export function shallowRef( + value: T +): T extends Ref ? T : Ref +export function shallowRef(value: T): Ref +export function shallowRef(): Ref +export function shallowRef(raw?: unknown) { + if (isRef(raw)) { + return raw + } + const value = shallowReactive({ [RefKey]: raw }) + return createRef({ + get: () => value[RefKey] as any, + set: (v) => ((value[RefKey] as any) = v), + }) +} + +export function triggerRef(value: any) { + if (!isRef(value)) return + + setForceTrigger(true) + value.value = value.value + setForceTrigger(false) +} + +export function proxyRefs( + objectWithRefs: T +): ShallowUnwrapRef { + if (isReactive(objectWithRefs)) { + return objectWithRefs as ShallowUnwrapRef + } + const value: Record = reactive({ [RefKey]: objectWithRefs }) + + def(value, RefKey, value[RefKey], false) + + for (const key of Object.keys(objectWithRefs)) { + proxy(value, key, { + get() { + if (isRef(value[RefKey][key])) { + return value[RefKey][key].value + } + return value[RefKey][key] + }, + set(v: unknown) { + if (isRef(value[RefKey][key])) { + return (value[RefKey][key].value = unref(v)) + } + value[RefKey][key] = unref(v) + }, + }) + } + + return value as ShallowUnwrapRef +} diff --git a/src/reactivity/set.ts b/src/reactivity/set.ts index e6e95466..01a55f6a 100644 --- a/src/reactivity/set.ts +++ b/src/reactivity/set.ts @@ -1,64 +1,75 @@ -import { getCurrentVue } from '../runtimeContext'; -import { isArray } from '../utils'; -import { defineAccessControl } from './observable'; - -function isUndef(v: any): boolean { - return v === undefined || v === null; -} - -function isPrimitive(value: any): boolean { - return ( - typeof value === 'string' || - typeof value === 'number' || - // $flow-disable-line - typeof value === 'symbol' || - typeof value === 'boolean' - ); -} - -function isValidArrayIndex(val: any): boolean { - const n = parseFloat(String(val)); - return n >= 0 && Math.floor(n) === n && isFinite(val); -} +import { AnyObject } from '../types/basic' +import { getVueConstructor } from '../runtimeContext' +import { + isArray, + isPrimitive, + isUndef, + isValidArrayIndex, + isObject, + hasOwn, +} from '../utils' +import { defineAccessControl, mockReactivityDeep } from './reactive' /** * Set a property on an object. Adds the new property, triggers change * notification and intercept it's subsequent access if the property doesn't * already exist. */ -export function set(target: any, key: any, val: T): T { - const Vue = getCurrentVue(); - const { warn, defineReactive } = Vue.util; - if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target))) { - warn(`Cannot set reactive property on undefined, null, or primitive value: ${target}`); +export function set(target: AnyObject, key: any, val: T): T { + const Vue = getVueConstructor() + // @ts-expect-error https://github.com/vuejs/vue/pull/12132 + const { warn, defineReactive } = Vue.util + if (__DEV__ && (isUndef(target) || isPrimitive(target))) { + warn( + `Cannot set reactive property on undefined, null, or primitive value: ${target}` + ) } - if (isArray(target) && isValidArrayIndex(key)) { - target.length = Math.max(target.length, key); - // IMPORTANT: define access control before trigger watcher - defineAccessControl(target, key, val); - target.splice(key, 1, val); - return val; + + const ob = target.__ob__ + + function ssrMockReactivity() { + // in SSR, there is no __ob__. Mock for reactivity check + if (ob && isObject(val) && !hasOwn(val, '__ob__')) { + mockReactivityDeep(val) + } + } + + if (isArray(target)) { + if (isValidArrayIndex(key)) { + target.length = Math.max(target.length, key) + target.splice(key, 1, val) + ssrMockReactivity() + return val + } else if (key === 'length' && (val as any) !== target.length) { + target.length = val as any + ob?.dep.notify() + return val + } } if (key in target && !(key in Object.prototype)) { - target[key] = val; - return val; + target[key] = val + ssrMockReactivity() + return val } - const ob = (target as any).__ob__; if (target._isVue || (ob && ob.vmCount)) { - process.env.NODE_ENV !== 'production' && + __DEV__ && warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.' - ); - return val; + ) + return val } + if (!ob) { - target[key] = val; - return val; + target[key] = val + return val } - defineReactive(ob.value, key, val); + + defineReactive(ob.value, key, val) // IMPORTANT: define access control before trigger watcher - defineAccessControl(target, key, val); - ob.dep.notify(); - return val; + defineAccessControl(target, key, val) + ssrMockReactivity() + + ob.dep.notify() + return val } diff --git a/src/runtimeContext.ts b/src/runtimeContext.ts index 744b5e51..cc43b5ad 100644 --- a/src/runtimeContext.ts +++ b/src/runtimeContext.ts @@ -1,27 +1,320 @@ -import Vue, { VueConstructor } from 'vue'; -import { assert } from './utils'; +import type { VueConstructor, VNode } from 'vue' +import { bindCurrentScopeToVM, EffectScope } from './apis/effectScope' +import { ComponentInstance, Data } from './component' +import { + assert, + hasOwn, + warn, + proxy, + UnionToIntersection, + isFunction, +} from './utils' +import type Vue$1 from 'vue' -let currentVue: VueConstructor | null = null; -let currentVM: Vue | null = null; +let vueDependency: VueConstructor | undefined = undefined -export function getCurrentVue(): VueConstructor { - if (process.env.NODE_ENV !== 'production') { - assert(currentVue, `must call Vue.use(plugin) before using any function.`); +try { + const requiredVue = require('vue') + if (requiredVue && isVue(requiredVue)) { + vueDependency = requiredVue + } else if ( + requiredVue && + 'default' in requiredVue && + isVue(requiredVue.default) + ) { + vueDependency = requiredVue.default } +} catch { + // not available +} + +let vueConstructor: VueConstructor | null = null +let currentInstance: ComponentInternalInstance | null = null +let currentInstanceTracking = true + +const PluginInstalledFlag = '__composition_api_installed__' + +function isVue(obj: any): obj is VueConstructor { + return obj && isFunction(obj) && obj.name === 'Vue' +} - return currentVue!; +export function isPluginInstalled() { + return !!vueConstructor } -export function setCurrentVue(vue: VueConstructor) { - currentVue = vue; +export function isVueRegistered(Vue: VueConstructor) { + // resolve issue: https://github.com/vuejs/composition-api/issues/876#issue-1087619365 + return vueConstructor && hasOwn(Vue, PluginInstalledFlag) +} + +export function getVueConstructor(): VueConstructor { + if (__DEV__) { + assert( + vueConstructor, + `must call Vue.use(VueCompositionAPI) before using any function.` + ) + } + + return vueConstructor! +} + +// returns registered vue or `vue` dependency +export function getRegisteredVueOrDefault(): VueConstructor { + let constructor = vueConstructor || vueDependency + + if (__DEV__) { + assert(constructor, `No vue dependency found.`) + } + + return constructor! +} + +export function setVueConstructor(Vue: VueConstructor) { + // @ts-ignore + if (__DEV__ && vueConstructor && Vue.__proto__ !== vueConstructor.__proto__) { + warn('[vue-composition-api] another instance of Vue installed') + } + vueConstructor = Vue + Object.defineProperty(Vue, PluginInstalledFlag, { + configurable: true, + writable: true, + value: true, + }) +} + +/** + * For `effectScope` to create instance without populate the current instance + * @internal + **/ +export function withCurrentInstanceTrackingDisabled(fn: () => void) { + const prev = currentInstanceTracking + currentInstanceTracking = false + try { + fn() + } finally { + currentInstanceTracking = prev + } +} + +export function setCurrentVue2Instance(vm: ComponentInstance | null) { + if (!currentInstanceTracking) return + setCurrentInstance(vm ? toVue3ComponentInstance(vm) : vm) +} + +export function setCurrentInstance(instance: ComponentInternalInstance | null) { + if (!currentInstanceTracking) return + const prev = currentInstance + prev?.scope.off() + currentInstance = instance + currentInstance?.scope.on() +} + +export type Slot = (...args: any[]) => VNode[] + +export type InternalSlots = { + [name: string]: Slot | undefined +} + +export type ObjectEmitsOptions = Record< + string, + ((...args: any[]) => any) | null +> +export type EmitsOptions = ObjectEmitsOptions | string[] + +export type EmitFn< + Options = ObjectEmitsOptions, + Event extends keyof Options = keyof Options, + ReturnType extends void | Vue$1 = void +> = Options extends Array + ? (event: V, ...args: any[]) => ReturnType + : {} extends Options // if the emit is empty object (usually the default value for emit) should be converted to function + ? (event: string, ...args: any[]) => ReturnType + : UnionToIntersection< + { + [key in Event]: Options[key] extends (...args: infer Args) => any + ? (event: key, ...args: Args) => ReturnType + : (event: key, ...args: any[]) => ReturnType + }[Event] + > + +export type ComponentRenderEmitFn< + Options = ObjectEmitsOptions, + Event extends keyof Options = keyof Options, + T extends Vue$1 | void = void +> = EmitFn + +export type Slots = Readonly + +export interface SetupContext { + attrs: Data + slots: Slots + emit: EmitFn + /** + * @deprecated not available in Vue 2 + */ + expose: (exposed?: Record) => void + + /** + * @deprecated not available in Vue 3 + */ + readonly parent: ComponentInstance | null + + /** + * @deprecated not available in Vue 3 + */ + readonly root: ComponentInstance + + /** + * @deprecated not available in Vue 3 + */ + readonly listeners: { [key in string]?: Function } + + /** + * @deprecated not available in Vue 3 + */ + readonly refs: { [key: string]: Vue | Element | Vue[] | Element[] } } -export function getCurrentVM() { - return currentVM; +export interface ComponentPublicInstance {} + +/** + * We expose a subset of properties on the internal instance as they are + * useful for advanced external libraries and tools. + */ +export declare interface ComponentInternalInstance { + uid: number + type: Record // ConcreteComponent + parent: ComponentInternalInstance | null + root: ComponentInternalInstance + + //appContext: AppContext + + /** + * Vnode representing this component in its parent's vdom tree + */ + vnode: VNode + /** + * Root vnode of this component's own vdom tree + */ + // subTree: VNode // does not exist in Vue 2 + + /** + * The reactive effect for rendering and patching the component. Callable. + */ + update: Function + + data: Data + props: Data + attrs: Data + refs: Data + emit: EmitFn + + slots: InternalSlots + emitted: Record | null + + proxy: ComponentInstance + + isMounted: boolean + isUnmounted: boolean + isDeactivated: boolean + + /** + * @internal + */ + scope: EffectScope + + /** + * @internal + */ + setupContext: SetupContext | null } -export function setCurrentVM(vue: Vue | null) { - currentVM = vue; +export function getCurrentInstance() { + return currentInstance } -export { currentVue }; +const instanceMapCache = new WeakMap< + ComponentInstance, + ComponentInternalInstance +>() + +export function toVue3ComponentInstance( + vm: ComponentInstance +): ComponentInternalInstance { + if (instanceMapCache.has(vm)) { + return instanceMapCache.get(vm)! + } + + const instance: ComponentInternalInstance = { + proxy: vm, + update: vm.$forceUpdate, + type: vm.$options, + uid: vm._uid, + + // $emit is defined on prototype and it expected to be bound + emit: vm.$emit.bind(vm), + + parent: null, + root: null!, // to be immediately set + } as unknown as ComponentInternalInstance + + bindCurrentScopeToVM(instance) + + // map vm.$props = + const instanceProps = [ + 'data', + 'props', + 'attrs', + 'refs', + 'vnode', + 'slots', + ] as const + + instanceProps.forEach((prop) => { + proxy(instance, prop, { + get() { + return (vm as any)[`$${prop}`] + }, + }) + }) + + proxy(instance, 'isMounted', { + get() { + // @ts-expect-error private api + return vm._isMounted + }, + }) + + proxy(instance, 'isUnmounted', { + get() { + // @ts-expect-error private api + return vm._isDestroyed + }, + }) + + proxy(instance, 'isDeactivated', { + get() { + // @ts-expect-error private api + return vm._inactive + }, + }) + + proxy(instance, 'emitted', { + get() { + // @ts-expect-error private api + return vm._events + }, + }) + + instanceMapCache.set(vm, instance) + + if (vm.$parent) { + instance.parent = toVue3ComponentInstance(vm.$parent) + } + + if (vm.$root) { + instance.root = toVue3ComponentInstance(vm.$root) + } + + return instance +} diff --git a/src/setup.ts b/src/setup.ts deleted file mode 100644 index a3b5e1c7..00000000 --- a/src/setup.ts +++ /dev/null @@ -1,183 +0,0 @@ -import VueInstance, { VueConstructor } from 'vue'; -import { SetupContext } from './types/vue'; -import { isWrapper } from './wrappers'; -import { SetupHookEvent } from './symbols'; -import { setCurrentVM } from './runtimeContext'; -import { isPlainObject, assert, proxy, noopFn } from './utils'; -import { value } from './functions/state'; -import { watch } from './functions/watch'; - -let disableSetup = false; - -// `cb` should be called right after props get resolved -function waitPropsResolved(vm: VueInstance, cb: (v: VueInstance, props: Record) => void) { - const safeRunCb = (props: Record) => { - // Running `cb` under the scope of a dep.Target, otherwise the `Observable` - // in `cb` will be unexpectedly colleced by the current dep.Target. - const dispose = watch( - () => { - cb(vm, props); - }, - noopFn, - { lazy: false, deep: false, flush: 'sync' } - ); - dispose(); - }; - - const opts = vm.$options; - let methods = opts.methods; - - if (!methods) { - opts.methods = { [SetupHookEvent]: noopFn }; - // This will get invoked when assigning to `SetupHookEvent` property of vm. - vm.$once(SetupHookEvent, () => { - // restore `opts` object - delete opts.methods; - safeRunCb(vm.$props); - }); - return; - } - - // Find the first method will re resovled. - // The order will be stable, since we never modify the `methods` object. - let firstMedthodName: string | undefined; - for (const key in methods) { - firstMedthodName = key; - break; - } - - // `methods` is an empty object - if (!firstMedthodName) { - methods[SetupHookEvent] = noopFn; - vm.$once(SetupHookEvent, () => { - // restore `methods` object - delete methods![SetupHookEvent]; - safeRunCb(vm.$props); - }); - return; - } - - proxy(vm, firstMedthodName, { - set(val: any) { - safeRunCb(vm.$props); - - // restore `firstMedthodName` to a noraml property - Object.defineProperty(vm, firstMedthodName!, { - configurable: true, - enumerable: true, - writable: true, - value: val, - }); - }, - }); -} - -export function mixin(Vue: VueConstructor) { - // We define the setup hook on prototype, - // which avoids Object.defineProperty calls for each instance created. - proxy(Vue.prototype, SetupHookEvent, { - get() { - return 'hook'; - }, - set(this: VueInstance) { - this.$emit(SetupHookEvent); - }, - }); - - Vue.mixin({ - beforeCreate: functionApiInit, - }); - - /** - * Vuex init hook, injected into each instances init hooks list. - */ - function functionApiInit(this: VueInstance) { - const vm = this; - const { setup } = vm.$options; - - if (disableSetup || !setup) { - return; - } - - if (typeof setup !== 'function') { - if (process.env.NODE_ENV !== 'production') { - Vue.util.warn( - 'The "setup" option should be a function that returns a object in component definitions.', - vm - ); - } - return; - } - - waitPropsResolved(vm, initSetup); - } - - function initSetup(vm: VueInstance, props: Record = {}) { - const setup = vm.$options.setup!; - const ctx = createSetupContext(vm); - let binding: any; - setCurrentVM(vm); - try { - binding = setup(props, ctx); - } catch (err) { - if (process.env.NODE_ENV !== 'production') { - Vue.util.warn(`there is an error occuring in "setup"`, vm); - } - throw err; - } finally { - setCurrentVM(null); - } - - if (!binding) return; - if (!isPlainObject(binding)) { - if (process.env.NODE_ENV !== 'production') { - assert( - false, - `"setup" must return a "Object", get "${Object.prototype.toString - .call(binding) - .slice(8, -1)}"` - ); - } - return; - } - - Object.keys(binding).forEach(name => { - let bindingValue = binding[name]; - // make plain value reactive - if (!isWrapper(bindingValue)) { - bindingValue = value(bindingValue); - } - // bind to vm - bindingValue.setVmProperty(vm, name); - }); - } - - function createSetupContext(vm: VueInstance & { [x: string]: any }): SetupContext { - const ctx = {} as SetupContext; - const props = ['parent', 'root', 'refs', 'slots', 'attrs']; - const methodReturnVoid = ['emit']; - props.forEach(key => { - proxy(ctx, key, { - get: () => vm[`$${key}`], - set() { - Vue.util.warn(`Cannot assign to '${key}' because it is a read-only property`, vm); - }, - }); - }); - methodReturnVoid.forEach(key => - proxy(ctx, key, { - get() { - const vmKey = `$${key}`; - return (...args: any[]) => { - const fn: Function = vm[vmKey]; - fn.apply(vm, args); - }; - }, - }) - ); - if (process.env.NODE_ENV === 'test') { - (ctx as any)._vm = vm; - } - return ctx; - } -} diff --git a/src/symbols.ts b/src/symbols.ts deleted file mode 100644 index 6792f3b3..00000000 --- a/src/symbols.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { hasSymbol } from './utils'; - -function createSymbol(name: string): string { - return hasSymbol ? (Symbol.for(name) as any) : name; -} - -export const WatcherPreFlushQueueKey = createSymbol('vfa.key.preFlushQueue'); -export const WatcherPostFlushQueueKey = createSymbol('vfa.key.postFlushQueue'); -export const AccessControIdentifierlKey = createSymbol('vfa.key.accessControIdentifier'); -export const ObservableIdentifierKey = createSymbol('vfa.key.observableIdentifier'); -// event name should be a string -export const SetupHookEvent = 'vfa.key.setupHookEvent'; diff --git a/src/ts-api/index.ts b/src/ts-api/index.ts deleted file mode 100644 index 5b742ac3..00000000 --- a/src/ts-api/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -// import Vue from 'vue'; -import Vue, { ComponentOptions } from 'vue'; -import { SetupContext } from '../types/vue'; - -export type PropType = T; - -// type FullPropType = T extends { required: boolean } ? T : T | undefined; -type Omit = Pick>; -type ComponentOptionsWithSetup = Omit, 'props' | 'setup'> & { - props?: Props; - setup?: ( - this: undefined, - props: { [K in keyof Props]: Props[K] }, - context: SetupContext - ) => object | null | undefined | void; -}; - -export function createComponent( - compOpions: ComponentOptionsWithSetup -): ComponentOptions { - return (compOpions as any) as ComponentOptions; -} diff --git a/src/types/basic.ts b/src/types/basic.ts index 504d6bea..9adb6cbb 100644 --- a/src/types/basic.ts +++ b/src/types/basic.ts @@ -1 +1,9 @@ -export type AnyObject = Record; +export type AnyObject = Record + +// Conditional returns can enforce identical types. +// See here: https://github.com/Microsoft/TypeScript/issues/27024#issuecomment-421529650 +// prettier-ignore +type Equal = + (() => U extends Left ? 1 : 0) extends (() => U extends Right ? 1 : 0) ? true : false; + +export type HasDefined = Equal extends true ? false : true diff --git a/src/types/vue.ts b/src/types/vue.ts deleted file mode 100644 index e52ad0a6..00000000 --- a/src/types/vue.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Vue, { VNode } from 'vue/'; - -export interface SetupContext { - readonly parent: Vue; - readonly root: Vue; - readonly refs: { [key: string]: Vue | Element | Vue[] | Element[] }; - readonly slots: { [key: string]: VNode[] | undefined }; - readonly attrs: Record; - - emit(event: string, ...args: any[]): void; -} diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index fa76af32..00000000 --- a/src/utils.ts +++ /dev/null @@ -1,52 +0,0 @@ -const toString = (x: any) => Object.prototype.toString.call(x); - -export const hasSymbol = typeof Symbol === 'function' && Symbol.for; - -export const noopFn: any = (_: any) => _; - -const sharedPropertyDefinition = { - enumerable: true, - configurable: true, - get: noopFn, - set: noopFn, -}; - -export function proxy(target: any, key: string, { get, set }: { get?: Function; set?: Function }) { - sharedPropertyDefinition.get = get || noopFn; - sharedPropertyDefinition.set = set || noopFn; - Object.defineProperty(target, key, sharedPropertyDefinition); -} - -export function def(obj: Object, key: string, val: any, enumerable?: boolean) { - Object.defineProperty(obj, key, { - value: val, - enumerable: !!enumerable, - writable: true, - configurable: true, - }); -} - -const hasOwnProperty = Object.prototype.hasOwnProperty; -export function hasOwn(obj: Object | any[], key: string): boolean { - return hasOwnProperty.call(obj, key); -} - -export function assert(condition: any, msg: string) { - if (!condition) throw new Error(`[vue-function-api] ${msg}`); -} - -export function isArray(x: unknown): x is T[] { - return Array.isArray(x); -} - -export function isObject(val: unknown): val is Record { - return val !== null && typeof val === 'object'; -} - -export function isPlainObject(x: unknown): x is T { - return toString(x) === '[object Object]'; -} - -export function isFunction(x: unknown): x is Function { - return typeof x === 'function'; -} diff --git a/src/utils/helper.ts b/src/utils/helper.ts new file mode 100644 index 00000000..24b93aad --- /dev/null +++ b/src/utils/helper.ts @@ -0,0 +1,117 @@ +import Vue, { VNode, ComponentOptions, VueConstructor } from 'vue' +import { ComponentInstance } from '../component' +import { + ComponentInternalInstance, + getCurrentInstance, + getVueConstructor, + Slot, +} from '../runtimeContext' +import { warn } from './utils' + +export function getCurrentInstanceForFn( + hook: string, + target?: ComponentInternalInstance | null +): ComponentInternalInstance | null { + target = target || getCurrentInstance() + if (__DEV__ && !target) { + warn( + `${hook} is called when there is no active component instance to be ` + + `associated with. ` + + `Lifecycle injection APIs can only be used during execution of setup().` + ) + } + return target +} + +export function defineComponentInstance( + Ctor: VueConstructor, + options: ComponentOptions = {} +) { + const silent = Ctor.config.silent + Ctor.config.silent = true + const vm = new Ctor(options) + Ctor.config.silent = silent + return vm +} + +export function isComponentInstance(obj: any) { + const Vue = getVueConstructor() + return Vue && obj instanceof Vue +} + +export function createSlotProxy(vm: ComponentInstance, slotName: string): Slot { + return ((...args: any) => { + if (!vm.$scopedSlots[slotName]) { + if (__DEV__) + return warn( + `slots.${slotName}() got called outside of the "render()" scope`, + vm + ) + return + } + + return vm.$scopedSlots[slotName]!.apply(vm, args) + }) as Slot +} + +export function resolveSlots( + slots: { [key: string]: Function } | void, + normalSlots: { [key: string]: VNode[] | undefined } +): { [key: string]: true } { + let res: { [key: string]: true } + if (!slots) { + res = {} + } else if (slots._normalized) { + // fast path 1: child component re-render only, parent did not change + return slots._normalized as any + } else { + res = {} + for (const key in slots) { + if (slots[key] && key[0] !== '$') { + res[key] = true + } + } + } + + // expose normal slots on scopedSlots + for (const key in normalSlots) { + if (!(key in res)) { + res[key] = true + } + } + + return res +} + +let vueInternalClasses: + | { + Watcher: any + Dep: any + } + | undefined + +export const getVueInternalClasses = () => { + if (!vueInternalClasses) { + const vm: any = defineComponentInstance(getVueConstructor(), { + computed: { + value() { + return 0 + }, + }, + }) + + // to get Watcher class + const Watcher = vm._computedWatchers.value.constructor + // to get Dep class + const Dep = vm._data.__ob__.dep.constructor + + vueInternalClasses = { + Watcher, + Dep, + } + + vm.$destroy() + } + + return vueInternalClasses +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 00000000..856c3f85 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,3 @@ +export * from './utils' +export * from './helper' +export * from './typeutils' diff --git a/src/utils/instance.ts b/src/utils/instance.ts new file mode 100644 index 00000000..85e717ce --- /dev/null +++ b/src/utils/instance.ts @@ -0,0 +1,211 @@ +import type { VNode } from 'vue' +import { ComponentInstance } from '../component' +import vmStateManager from './vmStateManager' +import { + setCurrentInstance, + getCurrentInstance, + ComponentInternalInstance, + InternalSlots, + SetupContext, +} from '../runtimeContext' +import { Ref, isRef, isReactive } from '../apis' +import { hasOwn, proxy, warn } from './utils' +import { createSlotProxy, resolveSlots } from './helper' +import { reactive } from '../reactivity/reactive' + +export function asVmProperty( + vm: ComponentInstance, + propName: string, + propValue: Ref +) { + const props = vm.$options.props + if (!(propName in vm) && !(props && hasOwn(props, propName))) { + if (isRef(propValue)) { + proxy(vm, propName, { + get: () => propValue.value, + set: (val: unknown) => { + propValue.value = val + }, + }) + } else { + proxy(vm, propName, { + get: () => { + if (isReactive(propValue)) { + ;(propValue as any).__ob__.dep.depend() + } + return propValue + }, + set: (val: any) => { + propValue = val + }, + }) + } + + if (__DEV__) { + // expose binding to Vue Devtool as a data property + // delay this until state has been resolved to prevent repeated works + vm.$nextTick(() => { + if (Object.keys(vm._data).indexOf(propName) !== -1) { + return + } + if (isRef(propValue)) { + proxy(vm._data, propName, { + get: () => propValue.value, + set: (val: unknown) => { + propValue.value = val + }, + }) + } else { + proxy(vm._data, propName, { + get: () => propValue, + set: (val: any) => { + propValue = val + }, + }) + } + }) + } + } else if (__DEV__) { + if (props && hasOwn(props, propName)) { + warn( + `The setup binding property "${propName}" is already declared as a prop.`, + vm + ) + } else { + warn(`The setup binding property "${propName}" is already declared.`, vm) + } + } +} + +function updateTemplateRef(vm: ComponentInstance) { + const rawBindings = vmStateManager.get(vm, 'rawBindings') || {} + if (!rawBindings || !Object.keys(rawBindings).length) return + + const refs = vm.$refs + const oldRefKeys = vmStateManager.get(vm, 'refs') || [] + for (let index = 0; index < oldRefKeys.length; index++) { + const key = oldRefKeys[index] + const setupValue = rawBindings[key] + if (!refs[key] && setupValue && isRef(setupValue)) { + setupValue.value = null + } + } + + const newKeys = Object.keys(refs) + const validNewKeys = [] + for (let index = 0; index < newKeys.length; index++) { + const key = newKeys[index] + const setupValue = rawBindings[key] + if (refs[key] && setupValue && isRef(setupValue)) { + setupValue.value = refs[key] + validNewKeys.push(key) + } + } + vmStateManager.set(vm, 'refs', validNewKeys) +} + +export function afterRender(vm: ComponentInstance) { + const stack = [(vm as any)._vnode as VNode] + while (stack.length) { + const vnode = stack.pop() + if (vnode) { + if (vnode.context) updateTemplateRef(vnode.context) + if (vnode.children) { + for (let i = 0; i < vnode.children.length; ++i) { + stack.push(vnode.children[i]) + } + } + } + } +} + +export function updateVmAttrs(vm: ComponentInstance, ctx?: SetupContext) { + if (!vm) { + return + } + let attrBindings = vmStateManager.get(vm, 'attrBindings') + if (!attrBindings && !ctx) { + // fix 840 + return + } + if (!attrBindings) { + const observedData = reactive({}) + attrBindings = { ctx: ctx!, data: observedData } + vmStateManager.set(vm, 'attrBindings', attrBindings) + proxy(ctx, 'attrs', { + get: () => { + return attrBindings?.data + }, + set() { + __DEV__ && + warn( + `Cannot assign to '$attrs' because it is a read-only property`, + vm + ) + }, + }) + } + + const source = vm.$attrs + for (const attr of Object.keys(source)) { + if (!hasOwn(attrBindings.data, attr)) { + proxy(attrBindings.data, attr, { + get: () => { + // to ensure it always return the latest value + return vm.$attrs[attr] + }, + }) + } + } +} + +export function resolveScopedSlots( + vm: ComponentInstance, + slotsProxy: InternalSlots +): void { + const parentVNode = (vm.$options as any)._parentVnode + if (!parentVNode) return + + const prevSlots = vmStateManager.get(vm, 'slots') || [] + const curSlots = resolveSlots(parentVNode.data.scopedSlots, vm.$slots) + // remove staled slots + for (let index = 0; index < prevSlots.length; index++) { + const key = prevSlots[index] + if (!curSlots[key]) { + delete slotsProxy[key] + } + } + + // proxy fresh slots + const slotNames = Object.keys(curSlots) + for (let index = 0; index < slotNames.length; index++) { + const key = slotNames[index] + if (!slotsProxy[key]) { + slotsProxy[key] = createSlotProxy(vm, key) + } + } + vmStateManager.set(vm, 'slots', slotNames) +} + +export function activateCurrentInstance( + instance: ComponentInternalInstance, + fn: (instance: ComponentInternalInstance) => any, + onError?: (err: Error) => void +) { + let preVm = getCurrentInstance() + setCurrentInstance(instance) + try { + return fn(instance) + } catch ( + // FIXME: remove any + err: any + ) { + if (onError) { + onError(err) + } else { + throw err + } + } finally { + setCurrentInstance(preVm) + } +} diff --git a/src/utils/sets.ts b/src/utils/sets.ts new file mode 100644 index 00000000..fd5f46dd --- /dev/null +++ b/src/utils/sets.ts @@ -0,0 +1,3 @@ +export const accessModifiedSet = new WeakMap() +export const rawSet = new WeakMap() +export const readonlySet = new WeakMap() diff --git a/src/utils/symbols.ts b/src/utils/symbols.ts new file mode 100644 index 00000000..00e5985f --- /dev/null +++ b/src/utils/symbols.ts @@ -0,0 +1,15 @@ +import { hasSymbol } from './utils' + +function createSymbol(name: string): string { + return hasSymbol ? (Symbol.for(name) as any) : name +} + +export const WatcherPreFlushQueueKey = createSymbol( + 'composition-api.preFlushQueue' +) +export const WatcherPostFlushQueueKey = createSymbol( + 'composition-api.postFlushQueue' +) + +// must be a string, symbol key is ignored in reactive +export const RefKey = 'composition-api.refKey' diff --git a/src/utils/typeutils.ts b/src/utils/typeutils.ts new file mode 100644 index 00000000..724acc93 --- /dev/null +++ b/src/utils/typeutils.ts @@ -0,0 +1,5 @@ +export type UnionToIntersection = ( + U extends any ? (k: U) => void : never +) extends (k: infer I) => void + ? I + : never diff --git a/src/utils/utils.ts b/src/utils/utils.ts new file mode 100644 index 00000000..2f0a18b5 --- /dev/null +++ b/src/utils/utils.ts @@ -0,0 +1,128 @@ +import { getRegisteredVueOrDefault } from '../runtimeContext' + +const toString = (x: any) => Object.prototype.toString.call(x) + +export function isNative(Ctor: any): boolean { + return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) +} + +export const hasSymbol = + typeof Symbol !== 'undefined' && + isNative(Symbol) && + typeof Reflect !== 'undefined' && + isNative(Reflect.ownKeys) + +export const noopFn: any = (_: any) => _ + +export function proxy( + target: any, + key: string, + { get, set }: { get?: Function; set?: Function } +) { + Object.defineProperty(target, key, { + enumerable: true, + configurable: true, + get: get || noopFn, + set: set || noopFn, + }) +} + +export function def(obj: Object, key: string, val: any, enumerable?: boolean) { + Object.defineProperty(obj, key, { + value: val, + enumerable: !!enumerable, + writable: true, + configurable: true, + }) +} + +export function hasOwn(obj: Object, key: PropertyKey): boolean { + return Object.hasOwnProperty.call(obj, key) +} + +export function assert(condition: any, msg: string) { + if (!condition) { + throw new Error(`[vue-composition-api] ${msg}`) + } +} + +export function isPrimitive(value: any): boolean { + return ( + typeof value === 'string' || + typeof value === 'number' || + // $flow-disable-line + typeof value === 'symbol' || + typeof value === 'boolean' + ) +} + +export function isArray(x: unknown): x is T[] { + return Array.isArray(x) +} + +export const objectToString = Object.prototype.toString + +export const toTypeString = (value: unknown): string => + objectToString.call(value) + +export const isMap = (val: unknown): val is Map => + toTypeString(val) === '[object Map]' + +export const isSet = (val: unknown): val is Set => + toTypeString(val) === '[object Set]' + +const MAX_VALID_ARRAY_LENGTH = 4294967295 // Math.pow(2, 32) - 1 +export function isValidArrayIndex(val: any): boolean { + const n = parseFloat(String(val)) + return ( + n >= 0 && + Math.floor(n) === n && + isFinite(val) && + n <= MAX_VALID_ARRAY_LENGTH + ) +} + +export function isObject(val: unknown): val is Record { + return val !== null && typeof val === 'object' +} + +export function isPlainObject(x: unknown): x is Record { + return toString(x) === '[object Object]' +} + +export function isFunction(x: unknown): x is Function { + return typeof x === 'function' +} + +export function isUndef(v: any): boolean { + return v === undefined || v === null +} + +export function warn(msg: string, vm?: Vue) { + const Vue = getRegisteredVueOrDefault() + if (!Vue || !Vue.util) console.warn(`[vue-composition-api] ${msg}`) + else Vue.util.warn(msg, vm) +} + +export function logError(err: Error, vm: Vue, info: string) { + if (__DEV__) { + warn(`Error in ${info}: "${err.toString()}"`, vm) + } + if (typeof window !== 'undefined' && typeof console !== 'undefined') { + console.error(err) + } else { + throw err + } +} + +/** + * Object.is polyfill + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is + * */ +export function isSame(value1: any, value2: any): boolean { + if (value1 === value2) { + return value1 !== 0 || 1 / value1 === 1 / value2 + } else { + return value1 !== value1 && value2 !== value2 + } +} diff --git a/src/utils/vmStateManager.ts b/src/utils/vmStateManager.ts new file mode 100644 index 00000000..38963352 --- /dev/null +++ b/src/utils/vmStateManager.ts @@ -0,0 +1,34 @@ +import { ComponentInstance, Data } from '../component' +import { SetupContext } from '../runtimeContext' + +export interface VfaState { + refs?: string[] + rawBindings?: Data + attrBindings?: { + ctx: SetupContext + data: Data + } + slots?: string[] +} + +function set( + vm: ComponentInstance, + key: K, + value: VfaState[K] +): void { + const state = (vm.__composition_api_state__ = + vm.__composition_api_state__ || {}) + state[key] = value +} + +function get( + vm: ComponentInstance, + key: K +): VfaState[K] | undefined { + return (vm.__composition_api_state__ || {})[key] +} + +export default { + set, + get, +} diff --git a/src/wrappers/AbstractWrapper.ts b/src/wrappers/AbstractWrapper.ts deleted file mode 100644 index 525fdd6a..00000000 --- a/src/wrappers/AbstractWrapper.ts +++ /dev/null @@ -1,39 +0,0 @@ -import Vue from 'vue'; -import { getCurrentVue } from '../runtimeContext'; -import { proxy, hasOwn, def } from '../utils'; - -export default abstract class AbstractWrapper { - protected _propName?: string; - protected _vm?: Vue; - abstract value: V; - - setVmProperty(vm: Vue, propName: string) { - def(this, '_vm', vm); - def(this, '_propName', propName); - - const props = vm.$options.props; - const warn = getCurrentVue().util.warn; - if (!(propName in vm) && !(props && hasOwn(props, propName))) { - proxy(vm, propName, { - get: () => this.value, - set: (val: any) => { - this.value = val; - }, - }); - if (process.env.NODE_ENV !== 'production') { - // after data has resolved, expose bindings to vm._data. - vm.$nextTick(() => { - this.exposeToDevtool(); - }); - } - } else if (process.env.NODE_ENV !== 'production') { - if (props && hasOwn(props, propName)) { - warn(`The setup binding property "${propName}" is already declared as a prop.`, vm); - } else { - warn(`The setup binding property "${propName}" is already declared.`, vm); - } - } - } - - protected abstract exposeToDevtool(): void; -} diff --git a/src/wrappers/ComputedWrapper.ts b/src/wrappers/ComputedWrapper.ts deleted file mode 100644 index 06689b21..00000000 --- a/src/wrappers/ComputedWrapper.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { getCurrentVue } from '../runtimeContext'; -import { proxy, def } from '../utils'; -import AbstractWrapper from './AbstractWrapper'; - -interface ComputedInternal { - read(): T; - write?(x: T): void; -} - -export default class ComputedWrapper extends AbstractWrapper { - private _internal!: ComputedInternal; - - constructor(internal: ComputedInternal) { - super(); - def(this, '_internal', internal); - } - - get value() { - return this._internal.read(); - } - - set value(val: V) { - if (!this._internal.write) { - if (process.env.NODE_ENV !== 'production') { - getCurrentVue().util.warn( - 'Computed property' + - (this._propName ? ` "${this._propName}"` : '') + - ' was assigned to but it has no setter.', - this._vm - ); - } - } else { - this._internal.write(val); - } - } - - exposeToDevtool() { - if (process.env.NODE_ENV !== 'production') { - const vm = this._vm!; - const name = this._propName!; - - if (!vm.$options.computed) { - vm.$options.computed = {}; - } - - proxy(vm.$options.computed, name, { - get: () => ({ - get: () => this.value, - set: (val: any) => { - this.value = val; - }, - }), - }); - } - } -} diff --git a/src/wrappers/ValueWrapper.ts b/src/wrappers/ValueWrapper.ts deleted file mode 100644 index 9f76ee3f..00000000 --- a/src/wrappers/ValueWrapper.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { proxy, def } from '../utils'; -import AbstractWrapper from './AbstractWrapper'; - -interface ValueInternal { - $$state: T; -} - -export default class ValueWrapper extends AbstractWrapper { - private _internal!: ValueInternal; - - constructor(internal: ValueInternal) { - super(); - def(this, '_internal', internal); - } - - get value() { - return this._internal.$$state; - } - - set value(v: V) { - this._internal.$$state = v; - } - - exposeToDevtool() { - if (process.env.NODE_ENV !== 'production') { - const vm = this._vm!; - const name = this._propName!; - proxy(vm._data, name, { - get: () => this.value, - set: (val: any) => { - this.value = val; - }, - }); - } - } -} diff --git a/src/wrappers/index.ts b/src/wrappers/index.ts deleted file mode 100644 index 907e112e..00000000 --- a/src/wrappers/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import AbstractWrapper from './AbstractWrapper'; -import ValueWrapper from './ValueWrapper'; -import ComputedWrapper from './ComputedWrapper'; - -export interface Wrapper { - value: V; -} -export function isWrapper(obj: any): obj is AbstractWrapper { - return obj instanceof AbstractWrapper; -} -export { ValueWrapper, ComputedWrapper, AbstractWrapper }; diff --git a/test-dts/defineAsyncComponent.test-d.tsx b/test-dts/defineAsyncComponent.test-d.tsx new file mode 100644 index 00000000..26855d97 --- /dev/null +++ b/test-dts/defineAsyncComponent.test-d.tsx @@ -0,0 +1,53 @@ +import { defineAsyncComponent, defineComponent, expectType, h } from './index' + +const asyncComponent1 = async () => defineComponent({}) + +const asyncComponent2 = async () => ({ template: 'ASYNC' }) + +const syncComponent1 = defineComponent({ + template: '', +}) + +const syncComponent2 = { + template: '', +} + +defineAsyncComponent(asyncComponent1) +defineAsyncComponent(asyncComponent2) + +defineAsyncComponent({ + loader: asyncComponent1, + delay: 200, + timeout: 3000, + errorComponent: syncComponent1, + loadingComponent: syncComponent1, +}) + +defineAsyncComponent({ + loader: asyncComponent2, + delay: 200, + timeout: 3000, + errorComponent: syncComponent2, + loadingComponent: syncComponent2, +}) + +defineAsyncComponent(async () => syncComponent1) + +defineAsyncComponent(async () => syncComponent2) + +const component = defineAsyncComponent({ + loader: asyncComponent1, + loadingComponent: defineComponent({}), + errorComponent: defineComponent({}), + delay: 200, + timeout: 3000, + suspensible: false, + onError(error, retry, fail, attempts) { + expectType<() => void>(retry) + expectType<() => void>(fail) + expectType(attempts) + expectType(error) + }, +}) + +h(component) diff --git a/test-dts/defineComponent-vue2.d.tsx b/test-dts/defineComponent-vue2.d.tsx new file mode 100644 index 00000000..7870c555 --- /dev/null +++ b/test-dts/defineComponent-vue2.d.tsx @@ -0,0 +1,54 @@ +import { + defineComponent, + describe, + expectError, + expectType, + Ref, + ref, +} from './index' +import Vue from 'vue' + +describe('emits', () => { + const testComponent = defineComponent({ + emits: { + click: (n: number) => typeof n === 'number', + input: (b: string) => b.length > 1, + }, + setup(props, { emit }) { + emit('click', 1) + emit('input', 'foo') + }, + created() { + this.$emit('click', 1) + this.$emit('click', 1).$emit('click', 1) + this.$emit('input', 'foo') + this.$emit('input', 'foo').$emit('click', 1) + expectType>(this.$attrs) + // @ts-expect-error + expectError(this.$emit('input', 1).$emit('nope')) + }, + }) + + // interface of vue2's $emit has no generics, notice that untyped types will be "event: string, ...args: any[]) => this" when using vue-class-component. + // but we can get correct type when we use correct params + // maybe we need vue 2.7 to fully support emit type + type VueClass = { new (...args: any[]): V & Vue } & typeof Vue + + function useComponentRef>() { + return ref>(undefined!) as Ref> + } + + const foo = useComponentRef() + + foo.value.$emit('click', 1) + foo.value.$emit('input', 'foo') + foo.value.$emit('click', 1).$emit('click', 1) + // @ts-expect-error + expectError(foo.value.$emit('blah').$emit('click', 1)) + // @ts-expect-error + expectError(foo.value.$emit('click').$emit('click', 1)) + // @ts-expect-error + expectError(foo.value.$emit('blah').$emit('click', 1)) + // @ts-expect-error + expectError(foo.value.$emit('blah')) +}) diff --git a/test-dts/defineComponent.test-d.tsx b/test-dts/defineComponent.test-d.tsx new file mode 100644 index 00000000..2f68a4e0 --- /dev/null +++ b/test-dts/defineComponent.test-d.tsx @@ -0,0 +1,1152 @@ +import { + describe, + // Component, + defineComponent, + PropType, + ref, + reactive, + createApp, + expectError, + expectType, + ComponentPublicInstance, + // ComponentOptions, + // SetupContext, + IsUnion, + h, +} from './index' + +describe('with object props', () => { + interface ExpectedProps { + a?: number | undefined + b: string + e?: Function + h: boolean + j: undefined | (() => string | undefined) + bb: string + bbb: string + bbbb: string | undefined + bbbbb: string | undefined + cc?: string[] | undefined + dd: { n: 1 } + ee?: () => string + ff?: (a: number, b: string) => { a: boolean } + ccc?: string[] | undefined + ddd: string[] + eee: () => { a: string } + fff: (a: number, b: string) => { a: boolean } + hhh: boolean + ggg: 'foo' | 'bar' + ffff: (a: number, b: string) => { a: boolean } + iii?: (() => string) | (() => number) + jjj: ((arg1: string) => string) | ((arg1: string, arg2: string) => string) + kkk?: any + validated?: string + date?: Date + l?: Date + ll?: Date | number + lll?: string | number + } + + type GT = string & { __brand: unknown } + + const MyComponent = defineComponent({ + props: { + a: Number, + // required should make property non-void + b: { + type: String, + required: true, + }, + e: Function, + h: Boolean, + j: Function as PropType string | undefined)>, + // default value should infer type and make it non-void + bb: { + default: 'hello', + }, + bbb: { + // Note: default function value requires arrow syntax + explicit + // annotation + default: (props: any) => (props.bb as string) || 'foo', + }, + bbbb: { + type: String, + default: undefined, + }, + bbbbb: { + type: String, + default: () => undefined, + }, + // explicit type casting + cc: Array as PropType, + // required + type casting + dd: { + type: Object as PropType<{ n: 1 }>, + required: true, + }, + // return type + ee: Function as PropType<() => string>, + // arguments + object return + ff: Function as PropType<(a: number, b: string) => { a: boolean }>, + // explicit type casting with constructor + ccc: Array as () => string[], + // required + constructor type casting + ddd: { + type: Array as () => string[], + required: true, + }, + // required + object return + eee: { + type: Function as PropType<() => { a: string }>, + required: true, + }, + // required + arguments + object return + fff: { + type: Function as PropType<(a: number, b: string) => { a: boolean }>, + required: true, + }, + hhh: { + type: Boolean, + required: true, + }, + // default + type casting + ggg: { + type: String as PropType<'foo' | 'bar'>, + default: 'foo', + }, + // default + function + ffff: { + type: Function as PropType<(a: number, b: string) => { a: boolean }>, + default: (a: number, b: string) => ({ a: a > +b }), + }, + // union + function with different return types + iii: Function as PropType<(() => string) | (() => number)>, + // union + function with different args & same return type + jjj: { + type: Function as PropType< + ((arg1: string) => string) | ((arg1: string, arg2: string) => string) + >, + required: true, + }, + kkk: null, + validated: { + type: String, + // validator requires explicit annotation + validator: (val: unknown) => val !== '', + }, + date: Date, + l: [Date], + ll: [Date, Number], + lll: [String, Number], + }, + setup(props) { + // type assertion. See https://github.com/SamVerschueren/tsd + expectType(props.a) + expectType(props.b) + expectType(props.e) + expectType(props.h) + expectType(props.j) + expectType(props.bb) + expectType(props.bbb) + expectType(props.bbbb) + expectType(props.bbbbb) + expectType(props.cc) + expectType(props.dd) + expectType(props.ee) + expectType(props.ff) + expectType(props.ccc) + expectType(props.ddd) + expectType(props.eee) + expectType(props.fff) + expectType(props.hhh) + expectType(props.ggg) + expectType(props.ffff) + if (typeof props.iii !== 'function') { + expectType(props.iii) + } + expectType(props.iii) + expectType>(true) + expectType(props.jjj) + expectType(props.kkk) + expectType(props.validated) + expectType(props.date) + // FIXME: add Date support + // @ts-ignore + expectType(props.l) + // FIXME: add Date support + // @ts-ignore + expectType(props.ll) + expectType(props.lll) + + // @ts-expect-error props should be readonly + expectError((props.a = 1)) + + // setup context + return { + c: ref(1), + d: { + e: ref('hi'), + }, + f: reactive({ + g: ref('hello' as GT), + }), + } + }, + render() { + const props = this.$props + expectType(props.a) + expectType(props.b) + expectType(props.e) + expectType(props.h) + expectType(props.bb) + expectType(props.cc) + expectType(props.dd) + expectType(props.ee) + expectType(props.ff) + expectType(props.ccc) + expectType(props.ddd) + expectType(props.eee) + expectType(props.fff) + expectType(props.hhh) + expectType(props.ggg) + if (typeof props.iii !== 'function') { + expectType(props.iii) + } + expectType(props.iii) + expectType>(true) + expectType(props.jjj) + expectType(props.kkk) + + // @ts-expect-error props should be readonly + expectError((props.a = 1)) + + // should also expose declared props on `this` + expectType(this.a) + expectType(this.b) + expectType(this.e) + expectType(this.h) + expectType(this.bb) + expectType(this.cc) + expectType(this.dd) + expectType(this.ee) + expectType(this.ff) + expectType(this.ccc) + expectType(this.ddd) + expectType(this.eee) + expectType(this.fff) + expectType(this.hhh) + expectType(this.ggg) + if (typeof this.iii !== 'function') { + expectType(this.iii) + } + expectType(this.iii) + const { jjj } = this + expectType>(true) + expectType(this.jjj) + expectType(this.kkk) + + // @ts-expect-error props on `this` should be readonly + expectError((this.a = 1)) + + // assert setup context unwrapping + expectType(this.c) + expectType(this.d.e.value) + expectType(this.f.g) + + // setup context properties should be mutable + this.c = 2 + + return h('div') + }, + }) + + // expectType(MyComponent) + + // Test TSX + expectType( + {}} + cc={['cc']} + dd={{ n: 1 }} + ee={() => 'ee'} + ccc={['ccc']} + ddd={['ddd']} + eee={() => ({ a: 'eee' })} + fff={(a, b) => ({ a: a > +b })} + hhh={false} + ggg="foo" + jjj={() => ''} + // FIXME + // @ts-ignore + // should allow class/style as attrs + class="bar" + style={{ color: 'red' }} + // should allow key + key={'foo'} + // should allow ref + ref={'foo'} + /> + ) + + // expectType( + // ({ a: 'eee' })} + // fff={(a, b) => ({ a: a > +b })} + // hhh={false} + // jjj={() => ''} + // /> + // ) + + // @ts-expect-error missing required props + expectError() + + expectError( + // @ts-expect-error wrong prop types + + ) + expectError( + // @ts-expect-error wrong prop types + + ) + // @ts-expect-error + expectError() + + // `this` should be void inside of prop validators and prop default factories + defineComponent({ + props: { + myProp: { + type: Number, + validator(val: unknown): boolean { + // FIXME + // @ts-ignore + return val !== this.otherProp + }, + default(): number { + // FIXME + // @ts-ignore + return this.otherProp + 1 + }, + }, + otherProp: { + type: Number, + required: true, + }, + }, + }) +}) + +// describe('type inference w/ optional props declaration', () => { +// const MyComponent = defineComponent<{ a: string[]; msg: string }>({ +// setup(props) { +// expectType(props.msg) +// expectType(props.a) +// return { +// b: 1, +// } +// }, +// }) + +// expectType() +// // @ts-expect-error +// expectError() +// // @ts-expect-error +// expectError() +// }) + +// describe('type inference w/ direct setup function', () => { +// const MyComponent = defineComponent((_props: { msg: string }) => {}) +// expectType() +// // @ts-expect-error +// expectError() +// expectError() +// }) + +// describe('type inference w/ array props declaration', () => { +// const MyComponent = defineComponent({ +// props: ['a', 'b'], +// setup(props) { +// // @ts-expect-error props should be readonly +// expectError((props.a = 1)) +// expectType(props.a) +// expectType(props.b) +// return { +// c: 1, +// } +// }, +// render() { +// expectType(this.$props.a) +// expectType(this.$props.b) +// // @ts-expect-error +// expectError((this.$props.a = 1)) +// expectType(this.a) +// expectType(this.b) +// expectType(this.c) +// }, +// }) +// expectType() +// // @ts-expect-error +// expectError() +// }) + +// describe('type inference w/ options API', () => { +// defineComponent({ +// props: { a: Number }, +// setup() { +// return { +// b: 123, +// } +// }, +// data() { +// // Limitation: we cannot expose the return result of setup() on `this` +// // here in data() - somehow that would mess up the inference +// expectType(this.a) +// return { +// c: this.a || 123, +// someRef: ref(0), +// } +// }, +// computed: { +// d() { +// expectType(this.b) +// return this.b + 1 +// }, +// e: { +// get() { +// expectType(this.b) +// expectType(this.d) + +// return this.b + this.d +// }, +// set(v: number) { +// expectType(this.b) +// expectType(this.d) +// expectType(v) +// }, +// }, +// }, +// watch: { +// a() { +// expectType(this.b) +// this.b + 1 +// }, +// }, +// created() { +// // props +// expectType(this.a) +// // returned from setup() +// expectType(this.b) +// // returned from data() +// expectType(this.c) +// // computed +// expectType(this.d) +// // computed get/set +// expectType(this.e) +// expectType(this.someRef) +// }, +// methods: { +// doSomething() { +// // props +// expectType(this.a) +// // returned from setup() +// expectType(this.b) +// // returned from data() +// expectType(this.c) +// // computed +// expectType(this.d) +// // computed get/set +// expectType(this.e) +// }, +// returnSomething() { +// return this.a +// }, +// }, +// render() { +// // props +// expectType(this.a) +// // returned from setup() +// expectType(this.b) +// // returned from data() +// expectType(this.c) +// // computed +// expectType(this.d) +// // computed get/set +// expectType(this.e) +// // method +// expectType<() => number | undefined>(this.returnSomething) +// }, +// }) +// }) + +// describe('with mixins', () => { +// const MixinA = defineComponent({ +// emits: ['bar'], +// props: { +// aP1: { +// type: String, +// default: 'aP1', +// }, +// aP2: Boolean, +// }, +// data() { +// return { +// a: 1, +// } +// }, +// }) +// const MixinB = defineComponent({ +// props: ['bP1', 'bP2'], +// data() { +// return { +// b: 2, +// } +// }, +// }) +// const MixinC = defineComponent({ +// data() { +// return { +// c: 3, +// } +// }, +// }) +// const MixinD = defineComponent({ +// mixins: [MixinA], +// data() { +// //@ts-expect-error computed are not available on data() +// expectError(this.dC1) +// //@ts-expect-error computed are not available on data() +// expectError(this.dC2) + +// return { +// d: 4, +// } +// }, +// setup(props) { +// expectType(props.aP1) +// }, +// computed: { +// dC1() { +// return this.d + this.a +// }, +// dC2() { +// return this.aP1 + 'dC2' +// }, +// }, +// }) +// const MyComponent = defineComponent({ +// mixins: [MixinA, MixinB, MixinC, MixinD], +// emits: ['click'], +// props: { +// // required should make property non-void +// z: { +// type: String, +// required: true, +// }, +// }, + +// data(vm) { +// expectType(vm.a) +// expectType(vm.b) +// expectType(vm.c) +// expectType(vm.d) + +// // should also expose declared props on `this` +// expectType(this.a) +// expectType(this.aP1) +// expectType(this.aP2) +// expectType(this.b) +// expectType(this.bP1) +// expectType(this.c) +// expectType(this.d) + +// return {} +// }, + +// setup(props) { +// expectType(props.z) +// // props +// expectType<((...args: any[]) => any) | undefined>(props.onClick) +// // from Base +// expectType<((...args: any[]) => any) | undefined>(props.onBar) +// expectType(props.aP1) +// expectType(props.aP2) +// expectType(props.bP1) +// expectType(props.bP2) +// expectType(props.z) +// }, +// render() { +// const props = this.$props +// // props +// expectType<((...args: any[]) => any) | undefined>(props.onClick) +// // from Base +// expectType<((...args: any[]) => any) | undefined>(props.onBar) +// expectType(props.aP1) +// expectType(props.aP2) +// expectType(props.bP1) +// expectType(props.bP2) +// expectType(props.z) + +// const data = this.$data +// expectType(data.a) +// expectType(data.b) +// expectType(data.c) +// expectType(data.d) + +// // should also expose declared props on `this` +// expectType(this.a) +// expectType(this.aP1) +// expectType(this.aP2) +// expectType(this.b) +// expectType(this.bP1) +// expectType(this.c) +// expectType(this.d) +// expectType(this.dC1) +// expectType(this.dC2) + +// // props should be readonly +// // @ts-expect-error +// expectError((this.aP1 = 'new')) +// // @ts-expect-error +// expectError((this.z = 1)) + +// // props on `this` should be readonly +// // @ts-expect-error +// expectError((this.bP1 = 1)) + +// // string value can not assigned to number type value +// // @ts-expect-error +// expectError((this.c = '1')) + +// // setup context properties should be mutable +// this.d = 5 + +// return h('div') +// }, +// }) + +// // Test TSX +// expectType( +// +// ) + +// // missing required props +// // @ts-expect-error +// expectError() + +// // wrong prop types +// // @ts-expect-error +// expectError() +// // @ts-expect-error +// expectError() +// }) + +// describe('with extends', () => { +// const Base = defineComponent({ +// props: { +// aP1: Boolean, +// aP2: { +// type: Number, +// default: 2, +// }, +// }, +// data() { +// return { +// a: 1, +// } +// }, +// computed: { +// c(): number { +// return this.aP2 + this.a +// }, +// }, +// }) +// const MyComponent = defineComponent({ +// extends: Base, +// props: { +// // required should make property non-void +// z: { +// type: String, +// required: true, +// }, +// }, +// render() { +// const props = this.$props +// // props +// expectType(props.aP1) +// expectType(props.aP2) +// expectType(props.z) + +// const data = this.$data +// expectType(data.a) + +// // should also expose declared props on `this` +// expectType(this.a) +// expectType(this.aP1) +// expectType(this.aP2) + +// // setup context properties should be mutable +// this.a = 5 + +// return h('div') +// }, +// }) + +// // Test TSX +// expectType() + +// // missing required props +// // @ts-expect-error +// expectError() + +// // wrong prop types +// // @ts-expect-error +// expectError() +// // @ts-expect-error +// expectError() +// }) + +// describe('extends with mixins', () => { +// const Mixin = defineComponent({ +// emits: ['bar'], +// props: { +// mP1: { +// type: String, +// default: 'mP1', +// }, +// mP2: Boolean, +// mP3: { +// type: Boolean, +// required: true, +// }, +// }, +// data() { +// return { +// a: 1, +// } +// }, +// }) +// const Base = defineComponent({ +// emits: ['foo'], +// props: { +// p1: Boolean, +// p2: { +// type: Number, +// default: 2, +// }, +// p3: { +// type: Boolean, +// required: true, +// }, +// }, +// data() { +// return { +// b: 2, +// } +// }, +// computed: { +// c(): number { +// return this.p2 + this.b +// }, +// }, +// }) +// const MyComponent = defineComponent({ +// extends: Base, +// mixins: [Mixin], +// emits: ['click'], +// props: { +// // required should make property non-void +// z: { +// type: String, +// required: true, +// }, +// }, +// render() { +// const props = this.$props +// // props +// expectType<((...args: any[]) => any) | undefined>(props.onClick) +// // from Mixin +// expectType<((...args: any[]) => any) | undefined>(props.onBar) +// // from Base +// expectType<((...args: any[]) => any) | undefined>(props.onFoo) +// expectType(props.p1) +// expectType(props.p2) +// expectType(props.z) +// expectType(props.mP1) +// expectType(props.mP2) + +// const data = this.$data +// expectType(data.a) +// expectType(data.b) + +// // should also expose declared props on `this` +// expectType(this.a) +// expectType(this.b) +// expectType(this.p1) +// expectType(this.p2) +// expectType(this.mP1) +// expectType(this.mP2) + +// // setup context properties should be mutable +// this.a = 5 + +// return h('div') +// }, +// }) + +// // Test TSX +// expectType() + +// // mP1, mP2, p1, and p2 have default value. these are not required +// expectType() + +// // missing required props +// // @ts-expect-error +// expectError() +// // missing required props from mixin +// // @ts-expect-error +// expectError() +// // missing required props from extends +// // @ts-expect-error +// expectError() + +// // wrong prop types +// // @ts-expect-error +// expectError() +// // @ts-expect-error +// expectError() + +// // #3468 +// const CompWithD = defineComponent({ +// data() { +// return { foo: 1 } +// }, +// }) +// const CompWithC = defineComponent({ +// computed: { +// foo() { +// return 1 +// }, +// }, +// }) +// const CompWithM = defineComponent({ methods: { foo() {} } }) +// const CompEmpty = defineComponent({}) + +// defineComponent({ +// mixins: [CompWithD, CompEmpty], +// mounted() { +// expectType(this.foo) +// }, +// }) +// defineComponent({ +// mixins: [CompWithC, CompEmpty], +// mounted() { +// expectType(this.foo) +// }, +// }) +// defineComponent({ +// mixins: [CompWithM, CompEmpty], +// mounted() { +// expectType<() => void>(this.foo) +// }, +// }) +// }) + +describe('compatibility w/ createApp', () => { + const comp = defineComponent({}) + createApp(comp).mount('#hello') + + const comp2 = defineComponent({ + props: { foo: String }, + }) + createApp(comp2).mount('#hello') + + const comp3 = defineComponent({ + setup() { + return { + a: 1, + } + }, + }) + createApp(comp3).mount('#hello') +}) + +describe('defineComponent', () => { + test('should accept components defined with defineComponent', () => { + const comp = defineComponent({}) + defineComponent({ + components: { comp }, + }) + }) + + test('should accept class components with receiving constructor arguments', () => { + // class Comp { + // static __vccOpts = {} + // constructor(_props: { foo: string }) {} + // } + // defineComponent({ + // components: { Comp }, + // }) + }) +}) + +describe('emits', () => { + // Note: for TSX inference, ideally we want to map emits to onXXX props, + // but that requires type-level string constant concatenation as suggested in + // https://github.com/Microsoft/TypeScript/issues/12754 + + // The workaround for TSX users is instead of using emits, declare onXXX props + // and call them instead. Since `v-on:click` compiles to an `onClick` prop, + // this would also support other users consuming the component in templates + // with `v-on` listeners. + + // with object emits + defineComponent({ + emits: { + click: (n: number) => typeof n === 'number', + input: (b: string) => b.length > 1, + }, + // setup(props, { emit }) { + // expectType<((n: number) => boolean) | undefined>(props.onClick) + // expectType<((b: string) => boolean) | undefined>(props.onInput) + // emit('click', 1) + // emit('input', 'foo') + // // @ts-expect-error + // expectError(emit('nope')) + // // @ts-expect-error + // expectError(emit('click')) + // // @ts-expect-error + // expectError(emit('click', 'foo')) + // // @ts-expect-error + // expectError(emit('input')) + // // @ts-expect-error + // expectError(emit('input', 1)) + // }, + created() { + this.$emit('click', 1) + this.$emit('input', 'foo') + // @ts-expect-error + expectError(this.$emit('nope')) + // @ts-expect-error + expectError(this.$emit('click')) + // @ts-expect-error + expectError(this.$emit('click', 'foo')) + // @ts-expect-error + expectError(this.$emit('input')) + // @ts-expect-error + expectError(this.$emit('input', 1)) + }, + // mounted() { + // // #3599 + // this.$nextTick(function () { + // // this should be bound to this instance + + // this.$emit('click', 1) + // this.$emit('input', 'foo') + // // @ts-expect-error + // expectError(this.$emit('nope')) + // // @ts-expect-error + // expectError(this.$emit('click')) + // // @ts-expect-error + // expectError(this.$emit('click', 'foo')) + // // @ts-expect-error + // expectError(this.$emit('input')) + // // @ts-expect-error + // expectError(this.$emit('input', 1)) + // }) + // }, + }) + + // with array emits + // defineComponent({ + // emits: ['foo', 'bar'], + // setup(props, { emit }) { + // expectType<((...args: any[]) => any) | undefined>(props.onFoo) + // expectType<((...args: any[]) => any) | undefined>(props.onBar) + // emit('foo') + // emit('foo', 123) + // emit('bar') + // // @ts-expect-error + // expectError(emit('nope')) + // }, + // created() { + // this.$emit('foo') + // this.$emit('foo', 123) + // this.$emit('bar') + // // @ts-expect-error + // expectError(this.$emit('nope')) + // }, + // }) + + // with tsx + // const Component = defineComponent({ + // emits: { + // click: (n: number) => typeof n === 'number', + // }, + // // setup(props, { emit }) { + // // expectType<((n: number) => any) | undefined>(props.onClick) + // // emit('click', 1) + // // // @ts-expect-error + // // expectError(emit('click')) + // // // @ts-expect-error + // // expectError(emit('click', 'foo')) + // // }, + // }) + // + // defineComponent({ + // render() { + // return ( + // { + // return n + 1 + // }} + // /> + // ) + // }, + // }) + + // without emits + defineComponent({ + setup(props, { emit }) { + emit('test', 1) + emit('test') + }, + }) + + // emit should be valid when ComponentPublicInstance is used. + const instance = {} as ComponentPublicInstance + instance.$emit('test', 1) + instance.$emit('test') + + // `this` should be void inside of emits validators + defineComponent({ + props: ['bar'], + emits: { + foo(): boolean { + // @ts-expect-error + return this.bar === 3 + }, + }, + }) +}) + +// describe('componentOptions setup should be `SetupContext`', () => { +// expect( +// {} as (props: Record, ctx: SetupContext) => any +// ) +// }) + +// describe('extract instance type', () => { +// const Base = defineComponent({ +// props: { +// baseA: { +// type: Number, +// default: 1, +// }, +// }, +// }) +// const MixinA = defineComponent({ +// props: { +// mA: { +// type: String, +// default: '', +// }, +// }, +// }) +// const CompA = defineComponent({ +// extends: Base, +// mixins: [MixinA], +// props: { +// a: { +// type: Boolean, +// default: false, +// }, +// b: { +// type: String, +// required: true, +// }, +// c: Number, +// }, +// }) + +// const compA = {} as InstanceType + +// expectType(compA.a) +// expectType(compA.b) +// expectType(compA.c) +// // mixins +// expectType(compA.mA) +// // extends +// expectType(compA.baseA) + +// // @ts-expect-error +// expectError((compA.a = true)) +// // @ts-expect-error +// expectError((compA.b = 'foo')) +// // @ts-expect-error +// expectError((compA.c = 1)) +// // @ts-expect-error +// expectError((compA.mA = 'foo')) +// // @ts-expect-error +// expectError((compA.baseA = 1)) +// }) + +// describe('async setup', () => { +// type GT = string & { __brand: unknown } +// const Comp = defineComponent({ +// async setup() { +// // setup context +// return { +// a: ref(1), +// b: { +// c: ref('hi'), +// }, +// d: reactive({ +// e: ref('hello' as GT), +// }), +// } +// }, +// render() { +// // assert setup context unwrapping +// expectType(this.a) +// expectType(this.b.c.value) +// expectType(this.d.e) + +// // setup context properties should be mutable +// this.a = 2 +// }, +// }) + +// const vm = {} as InstanceType +// // assert setup context unwrapping +// expectType(vm.a) +// expectType(vm.b.c.value) +// expectType(vm.d.e) + +// // setup context properties should be mutable +// vm.a = 2 +// }) + +// check if defineComponent can be exported +export default { + // function components + a: defineComponent((props: any) => h('div')), + // no props + b: defineComponent({ + data() { + return {} + }, + }), + c: defineComponent({ + props: ['a'], + }), + d: defineComponent({ + props: { + a: Number, + }, + }), +} diff --git a/test-dts/index.d.ts b/test-dts/index.d.ts new file mode 100644 index 00000000..8ba1def4 --- /dev/null +++ b/test-dts/index.d.ts @@ -0,0 +1,20 @@ +import type {} from '@vue/runtime-dom' +export * from '@vue/composition-api' +// export * from 'vue3' + +export function describe(_name: string, _fn: () => void): void + +export function expectType(value: T): void +export function expectError(value: T): void +export function expectAssignable(value: T2): void + +// https://stackoverflow.com/questions/49927523/disallow-call-with-any/49928360#49928360 +type IfNotAny = 0 extends 1 & T ? never : T +type IfNotUndefined = Exclude extends never ? never : T +export function isNotAnyOrUndefined(value: IfNotAny>): void + +export type IsUnion = ( + T extends any ? (U extends T ? false : true) : never +) extends false + ? false + : true diff --git a/test-dts/readonly.test-d.tsx b/test-dts/readonly.test-d.tsx new file mode 100644 index 00000000..1ca26a56 --- /dev/null +++ b/test-dts/readonly.test-d.tsx @@ -0,0 +1,48 @@ +import { expectType, readonly, ref } from './index' + +describe('readonly', () => { + it('nested', () => { + const r = readonly({ + obj: { k: 'v' }, + arr: [1, 2, '3'], + objInArr: [{ foo: 'bar' }], + }) + + // @ts-expect-error + r.obj = {} + // @ts-expect-error + r.obj.k = 'x' + + // @ts-expect-error + r.arr.push(42) + // @ts-expect-error + r.objInArr[0].foo = 'bar2' + }) + + it('with ref', () => { + const r = readonly( + ref({ + obj: { k: 'v' }, + arr: [1, 2, '3'], + objInArr: [{ foo: 'bar' }], + }) + ) + + console.log(r.value) + + expectType(r.value.obj.k) + + // @ts-expect-error + r.value = {} + + // @ts-expect-error + r.value.obj = {} + // @ts-expect-error + r.value.obj.k = 'x' + + // @ts-expect-error + r.value.arr.push(42) + // @ts-expect-error + r.value.objInArr[0].foo = 'bar2' + }) +}) diff --git a/test-dts/ref.test-d.tsx b/test-dts/ref.test-d.tsx new file mode 100644 index 00000000..374b0398 --- /dev/null +++ b/test-dts/ref.test-d.tsx @@ -0,0 +1,143 @@ +import { + Ref, + ref, + shallowRef, + isRef, + unref, + reactive, + expectType, +} from './index' + +function plainType(arg: number | Ref) { + // ref coercing + const coerced = ref(arg) + expectType>(coerced) + + // isRef as type guard + if (isRef(arg)) { + expectType>(arg) + } + + // ref unwrapping + expectType(unref(arg)) + + // ref inner type should be unwrapped + const nestedRef = ref({ + foo: ref(1), + }) + expectType>(nestedRef) + expectType<{ foo: number }>(nestedRef.value) + + // ref boolean + const falseRef = ref(false) + expectType>(falseRef) + expectType(falseRef.value) + + // ref true + const trueRef = ref(true) + expectType>(trueRef) + expectType(trueRef.value) + + // tuple + expectType<[number, string]>(unref(ref([1, '1']))) + + interface IteratorFoo { + [Symbol.iterator]: any + } + + // with symbol + expectType>( + ref() + ) +} + +plainType(1) + +function bailType(arg: HTMLElement | Ref) { + // ref coercing + const coerced = ref(arg) + expectType>(coerced) + + // isRef as type guard + if (isRef(arg)) { + expectType>(arg) + } + + // ref unwrapping + expectType(unref(arg)) + + // ref inner type should be unwrapped + const nestedRef = ref({ foo: ref(document.createElement('DIV')) }) + + expectType>(nestedRef) + expectType<{ foo: HTMLElement }>(nestedRef.value) +} +const el = document.createElement('DIV') +bailType(el) + +function withSymbol() { + const customSymbol = Symbol() + const obj = { + [Symbol.asyncIterator]: ref(1), + [Symbol.hasInstance]: { a: ref('a') }, + [Symbol.isConcatSpreadable]: { b: ref(true) }, + [Symbol.iterator]: [ref(1)], + [Symbol.match]: new Set>(), + [Symbol.matchAll]: new Map>(), + [Symbol.replace]: { arr: [ref('a')] }, + [Symbol.search]: { set: new Set>() }, + [Symbol.species]: { map: new Map>() }, + [Symbol.split]: new WeakSet>(), + [Symbol.toPrimitive]: new WeakMap, string>(), + [Symbol.toStringTag]: { weakSet: new WeakSet>() }, + [Symbol.unscopables]: { weakMap: new WeakMap, string>() }, + [customSymbol]: { arr: [ref(1)] }, + } + + const objRef = ref(obj) + + expectType>(objRef.value[Symbol.asyncIterator]) + expectType<{ a: Ref }>(objRef.value[Symbol.hasInstance]) + expectType<{ b: Ref }>(objRef.value[Symbol.isConcatSpreadable]) + expectType[]>(objRef.value[Symbol.iterator]) + expectType>>(objRef.value[Symbol.match]) + expectType>>(objRef.value[Symbol.matchAll]) + expectType<{ arr: Ref[] }>(objRef.value[Symbol.replace]) + expectType<{ set: Set> }>(objRef.value[Symbol.search]) + expectType<{ map: Map> }>(objRef.value[Symbol.species]) + expectType>>(objRef.value[Symbol.split]) + expectType, string>>(objRef.value[Symbol.toPrimitive]) + expectType<{ weakSet: WeakSet> }>( + objRef.value[Symbol.toStringTag] + ) + expectType<{ weakMap: WeakMap, string> }>( + objRef.value[Symbol.unscopables] + ) + expectType<{ arr: Ref[] }>(objRef.value[customSymbol]) +} + +withSymbol() + +const state = reactive({ + foo: { + value: 1, + label: 'bar', + }, +}) + +expectType(state.foo.label) + +type Status = 'initial' | 'ready' | 'invalidating' +const shallowStatus = shallowRef('initial') +if (shallowStatus.value === 'initial') { + expectType>(shallowStatus) + expectType(shallowStatus.value) + shallowStatus.value = 'invalidating' +} + +const refStatus = ref('initial') +if (refStatus.value === 'initial') { + expectType>(shallowStatus) + expectType(shallowStatus.value) + refStatus.value = 'invalidating' +} diff --git a/test-dts/tsconfig.build.json b/test-dts/tsconfig.build.json new file mode 100644 index 00000000..abe06c0f --- /dev/null +++ b/test-dts/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "paths": { + "@vue/composition-api": ["../dist/vue-composition-api.d.ts"] + } + } +} diff --git a/test-dts/tsconfig.json b/test-dts/tsconfig.json new file mode 100644 index 00000000..17a568b7 --- /dev/null +++ b/test-dts/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "noEmit": true, + "declaration": true, + "baseUrl": ".", + "jsx": "preserve", + "paths": { + "@vue/composition-api": ["../src"] + } + }, + "exclude": ["../test"], + "include": ["../src", "./*.ts", "./*.tsx"] +} diff --git a/test-dts/tsconfig.vue3.json b/test-dts/tsconfig.vue3.json new file mode 100644 index 00000000..12e21409 --- /dev/null +++ b/test-dts/tsconfig.vue3.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "paths": { + "@vue/composition-api": ["../node_modules/vue3/dist/vue.d.ts"] + } + }, + "exclude": ["./defineComponent-vue2.d.tsx"] +} diff --git a/test-dts/watch.test-d.tsx b/test-dts/watch.test-d.tsx new file mode 100644 index 00000000..581faa2a --- /dev/null +++ b/test-dts/watch.test-d.tsx @@ -0,0 +1,90 @@ +import { ref, computed, watch, expectType } from './index' + +const source = ref('foo') +const source2 = computed(() => source.value) +const source3 = () => 1 + +// lazy watcher will have consistent types for oldValue. +watch(source, (value, oldValue) => { + expectType(value) + expectType(oldValue) +}) + +// spread array +watch( + [source, source2, source3], + ([source1, source2, source3], [oldSource1, oldSource2, oldSource3]) => { + expectType(source1) + expectType(source2) + expectType(source3) + expectType(oldSource1) + expectType(oldSource2) + expectType(oldSource3) + } +) + +// const array +watch([source, source2, source3] as const, (values, oldValues) => { + expectType>(values) + expectType>(oldValues) + expectType(values[0]) + expectType(values[1]) + expectType(values[2]) + expectType(oldValues[0]) + expectType(oldValues[1]) + expectType(oldValues[2]) +}) + +// const spread array +watch( + [source, source2, source3] as const, + ([source1, source2, source3], [oldSource1, oldSource2, oldSource3]) => { + expectType(source1) + expectType(source2) + expectType(source3) + expectType(oldSource1) + expectType(oldSource2) + expectType(oldSource3) + } +) + +// immediate watcher's oldValue will be undefined on first run. +watch( + source, + (value, oldValue) => { + expectType(value) + expectType(oldValue) + }, + { immediate: true } +) + +watch( + [source, source2, source3], + (values, oldValues) => { + expectType<(string | number)[]>(values) + expectType<(string | number | undefined)[]>(oldValues) + }, + { immediate: true } +) + +// const array +watch( + [source, source2, source3] as const, + (values, oldValues) => { + expectType>(values) + expectType< + Readonly<[string | undefined, string | undefined, number | undefined]> + >(oldValues) + }, + { immediate: true } +) + +// should provide correct ref.value inner type to callbacks +const nestedRefSource = ref({ + foo: ref(1), +}) + +watch(nestedRefSource, (v, ov) => { + expectType<{ foo: number }>(v) + expectType<{ foo: number }>(ov) +}) diff --git a/test/apis/computed.spec.js b/test/apis/computed.spec.js new file mode 100644 index 00000000..9779e96a --- /dev/null +++ b/test/apis/computed.spec.js @@ -0,0 +1,278 @@ +import Vue from 'vue/dist/vue.common.js' +import { ref, computed, isReadonly, reactive, isRef, toRef } from '../../src' + +describe('Hooks computed', () => { + let warn = null + + beforeEach(() => { + warn = vi.spyOn(global.console, 'error').mockImplementation(() => null) + }) + afterEach(() => { + warn.mockRestore() + }) + + it('basic usage', () => + new Promise((done, reject) => { + done.fail = reject + + const vm = new Vue({ + template: '

{{ b }}
', + setup() { + const a = ref(1) + const b = computed(() => a.value + 1) + return { + a, + b, + } + }, + }).$mount() + expect(vm.b).toBe(2) + expect(vm.$el.textContent).toBe('2') + vm.a = 2 + expect(vm.b).toBe(3) + waitForUpdate(() => { + expect(vm.$el.textContent).toBe('3') + }).then(done) + })) + + it('with setter', () => + new Promise((done, reject) => { + done.fail = reject + + const vm = new Vue({ + template: '
{{ b }}
', + setup() { + const a = ref(1) + const b = computed({ + get: () => a.value + 1, + set: (v) => (a.value = v - 1), + }) + return { + a, + b, + } + }, + }).$mount() + expect(vm.b).toBe(2) + expect(vm.$el.textContent).toBe('2') + vm.a = 2 + expect(vm.b).toBe(3) + waitForUpdate(() => { + expect(vm.$el.textContent).toBe('3') + vm.b = 1 + expect(vm.a).toBe(0) + }) + .then(() => { + expect(vm.$el.textContent).toBe('1') + }) + .then(done) + })) + + it('warn assigning to computed with no setter', () => { + const vm = new Vue({ + setup() { + const b = computed(() => 1) + return { + b, + } + }, + }) + vm.b = 2 + expect(warn.mock.calls[0][0]).toMatch( + '[Vue warn]: Write operation failed: computed value is readonly.' + ) + }) + + it('watching computed', () => + new Promise((done, reject) => { + done.fail = reject + + const spy = vi.fn() + const vm = new Vue({ + setup() { + const a = ref(1) + const b = computed(() => a.value + 1) + return { + a, + b, + } + }, + }) + vm.$watch('b', spy) + vm.a = 2 + waitForUpdate(() => { + expect(spy).toHaveBeenCalledWith(3, 2) + }).then(done) + })) + + it('caching', () => { + const spy = vi.fn() + const vm = new Vue({ + setup() { + const a = ref(1) + const b = computed(() => { + spy() + return a.value + 1 + }) + return { + a, + b, + } + }, + }) + expect(spy.mock.calls.length).toBe(0) + vm.b + expect(spy.mock.calls.length).toBe(1) + vm.b + expect(spy.mock.calls.length).toBe(1) + }) + + it('as component', () => + new Promise((done, reject) => { + done.fail = reject + + const Comp = Vue.extend({ + template: `
{{ b }} {{ c }}
`, + setup() { + const a = ref(1) + const b = computed(() => { + return a.value + 1 + }) + return { + a, + b, + } + }, + }) + + const vm = new Comp({ + setup(_, { _vm }) { + const c = computed(() => { + return _vm.b + 1 + }) + + return { + c, + } + }, + }).$mount() + expect(vm.b).toBe(2) + expect(vm.c).toBe(3) + expect(vm.$el.textContent).toBe('2 3') + vm.a = 2 + expect(vm.b).toBe(3) + expect(vm.c).toBe(4) + waitForUpdate(() => { + expect(vm.$el.textContent).toBe('3 4') + }).then(done) + })) + + it('rethrow computed error', () => { + const vm = new Vue({ + setup() { + const a = computed(() => { + throw new Error('rethrow') + }) + + return { + a, + } + }, + }) + expect(() => vm.a).toThrowError('rethrow') + }) + + it('Mixins should not break computed properties', () => { + const ExampleComponent = Vue.extend({ + props: ['test'], + render: (h) => h('div'), + setup: (props) => ({ example: computed(() => props.test) }), + }) + + Vue.mixin({ + computed: { + foobar() { + return 'test' + }, + }, + }) + + const app = new Vue({ + render: (h) => + h('div', [ + h(ExampleComponent, { props: { test: 'A' } }), + h(ExampleComponent, { props: { test: 'B' } }), + ]), + }).$mount() + + expect(app.$children[0].example).toBe('A') + expect(app.$children[1].example).toBe('B') + }) + + it('should watch a reactive property created via toRef', () => + new Promise((done, reject) => { + done.fail = reject + + const spy = vi.fn() + const vm = new Vue({ + setup() { + const a = reactive({}) + const b = toRef(a, 'b') + + return { + a, + b, + } + }, + }) + vm.$watch('b', spy) + vm.b = 2 + waitForUpdate(() => { + expect(spy).toHaveBeenCalledWith(2, undefined) + }).then(done) + })) + + it('should be readonly', () => { + let a = { a: 1 } + const x = computed(() => a) + expect(isReadonly(x)).toBe(true) + expect(isReadonly(x.value)).toBe(false) + expect(isReadonly(x.value.a)).toBe(false) + const z = computed({ + get() { + return a + }, + set(v) { + a = v + }, + }) + expect(isReadonly(z.value)).toBe(false) + expect(isReadonly(z.value.a)).toBe(false) + }) + + it('passes isComputed', () => { + function isComputed(o) { + return !!(o && isRef(o) && o.effect) + } + + expect(isComputed(computed(() => 2))).toBe(true) + expect( + isComputed( + computed({ + get: () => 2, + set: () => {}, + }) + ) + ).toBe(true) + + expect(isComputed(ref({}))).toBe(false) + expect(isComputed(reactive({}))).toBe(false) + expect(isComputed({})).toBe(false) + expect(isComputed(undefined)).toBe(false) + expect(isComputed(null)).toBe(false) + expect(isComputed(true)).toBe(false) + expect(isComputed(20)).toBe(false) + expect(isComputed('hey')).toBe(false) + expect(isComputed('')).toBe(false) + }) +}) diff --git a/test/apis/inject.spec.js b/test/apis/inject.spec.js new file mode 100644 index 00000000..655967dd --- /dev/null +++ b/test/apis/inject.spec.js @@ -0,0 +1,177 @@ +import Vue from 'vue/dist/vue.common.js' +import { inject, provide, ref, reactive } from '../../src' + +let injected +const injectedComp = { + render() {}, + setup() { + return { + foo: inject('foo'), + bar: inject('bar'), + } + }, + created() { + injected = [this.foo, this.bar] + }, +} + +beforeEach(() => { + injected = null +}) + +describe('Hooks provide/inject', () => { + let warn = null + + beforeEach(() => { + warn = vi.spyOn(global.console, 'error').mockImplementation(() => null) + }) + afterEach(() => { + warn.mockRestore() + }) + + it('should work', () => { + new Vue({ + template: ``, + setup() { + const count = ref(1) + provide('foo', count) + provide('bar', false) + }, + components: { + child: { + template: ``, + components: { + injectedComp, + }, + }, + }, + }).$mount() + + expect(injected).toEqual([1, false]) + }) + + it('should return a default value when inject not found', () => { + let injected + new Vue({ + template: ``, + components: { + child: { + template: `
{{ msg }}
`, + setup() { + injected = inject('not-existed-inject-key', 'foo') + return { + injected, + } + }, + }, + }, + }).$mount() + + expect(injected).toBe('foo') + }) + + it('should work for ref value', () => + new Promise((done, reject) => { + done.fail = reject + + const Msg = Symbol() + const app = new Vue({ + template: ``, + setup() { + provide(Msg, ref('hello')) + }, + components: { + child: { + template: `
{{ msg }}
`, + setup() { + return { + msg: inject(Msg), + } + }, + }, + }, + }).$mount() + + app.$children[0].msg = 'bar' + waitForUpdate(() => { + expect(app.$el.textContent).toBe('bar') + }).then(done) + })) + + it('should work for reactive value', () => + new Promise((done, reject) => { + done.fail = reject + + const State = Symbol() + let obj + const app = new Vue({ + template: ``, + setup() { + provide(State, reactive({ msg: 'foo' })) + }, + components: { + child: { + template: `
{{ state.msg }}
`, + setup() { + obj = inject(State) + return { + state: obj, + } + }, + }, + }, + }).$mount() + expect(obj.msg).toBe('foo') + app.$children[0].state.msg = 'bar' + waitForUpdate(() => { + expect(app.$el.textContent).toBe('bar') + }).then(done) + })) + + it('should work when combined with 2.x provide option', () => { + const State = Symbol() + let obj1 + let obj2 + new Vue({ + template: ``, + setup() { + provide(State, { msg: 'foo' }) + }, + provide: { + X: { msg: 'bar' }, + }, + components: { + child: { + setup() { + obj1 = inject(State) + obj2 = inject('X') + }, + template: `
`, + }, + }, + }).$mount() + expect(obj1.msg).toBe('foo') + expect(obj2.msg).toBe('bar') + }) + + it('should call default value as factory', () => { + const State = Symbol() + let fn = vi.fn() + new Vue({ + template: ``, + setup() {}, + provide: { + X: { msg: 'bar' }, + }, + components: { + child: { + setup() { + inject(State, fn, true) + }, + template: `
`, + }, + }, + }).$mount() + expect(fn).toHaveBeenCalled() + }) +}) diff --git a/test/apis/lifecycle.spec.js b/test/apis/lifecycle.spec.js new file mode 100644 index 00000000..8ae55537 --- /dev/null +++ b/test/apis/lifecycle.spec.js @@ -0,0 +1,406 @@ +import Vue from 'vue/dist/vue.common.js' +import { + onBeforeMount, + onMounted, + onBeforeUpdate, + onUpdated, + onBeforeUnmount, + onUnmounted, + onErrorCaptured, + getCurrentInstance, +} from '../../src' + +describe('Hooks lifecycle', () => { + describe('beforeMount', () => { + it('should not have mounted', () => { + const spy = vi.fn() + const vm = new Vue({ + render() {}, + setup(_, { _vm }) { + onBeforeMount(() => { + expect(_vm._isMounted).toBe(false) + expect(_vm.$el).toBeUndefined() // due to empty mount + expect(_vm._vnode).toBeNull() + expect(_vm._watcher).toBeNull() + spy() + }) + }, + }) + expect(spy).not.toHaveBeenCalled() + vm.$mount() + expect(spy).toHaveBeenCalled() + }) + }) + + describe('mounted', () => { + it('should have mounted', () => { + const spy = vi.fn() + const vm = new Vue({ + template: '
', + setup(_, { _vm }) { + onMounted(() => { + expect(_vm._isMounted).toBe(true) + expect(_vm.$el.tagName).toBe('DIV') + expect(_vm._vnode.tag).toBe('div') + spy() + }) + }, + }) + expect(spy).not.toHaveBeenCalled() + vm.$mount() + expect(spy).toHaveBeenCalled() + }) + + it('should call for manually mounted instance with parent', () => { + const spy = vi.fn() + const parent = new Vue() + expect(spy).not.toHaveBeenCalled() + new Vue({ + parent, + template: '
', + setup() { + onMounted(() => { + spy() + }) + }, + }).$mount() + expect(spy).toHaveBeenCalled() + }) + + it('should mount child parent in correct order', () => { + const calls = [] + new Vue({ + template: '
', + setup() { + onMounted(() => { + calls.push('parent') + }) + }, + components: { + test: { + template: '', + setup(_, { _vm }) { + onMounted(() => { + expect(_vm.$el.parentNode).toBeTruthy() + calls.push('child') + }) + }, + components: { + nested: { + template: '
', + setup(_, { _vm }) { + onMounted(() => { + expect(_vm.$el.parentNode).toBeTruthy() + calls.push('nested') + }) + }, + }, + }, + }, + }, + }).$mount() + expect(calls).toEqual(['nested', 'child', 'parent']) + }) + + it('getCurrentInstance should be available', () => { + const parent = new Vue() + let instance + new Vue({ + parent, + template: '
', + setup() { + onMounted(() => { + instance = getCurrentInstance() + }) + }, + }).$mount() + expect(instance).toBeDefined() + }) + + it('getCurrentInstance should not be available on promised hook', () => { + const parent = new Vue() + let instance + let promisedInstance + new Vue({ + parent, + template: '
', + setup() { + onMounted(async () => { + instance = getCurrentInstance() + await Promise.resolve() + promisedInstance = getCurrentInstance() + }) + }, + }).$mount() + expect(instance).toBeDefined() + expect(promisedInstance).not.toBeDefined() + }) + }) + + describe('beforeUpdate', () => { + it('should be called before update', () => + new Promise((done, reject) => { + done.fail = reject + + const spy = vi.fn() + const vm = new Vue({ + template: '
{{ msg }}
', + data: { msg: 'foo' }, + setup(_, { _vm }) { + onBeforeUpdate(() => { + expect(_vm.$el.textContent).toBe('foo') + spy() + }) + }, + }).$mount() + expect(spy).not.toHaveBeenCalled() + vm.msg = 'bar' + expect(spy).not.toHaveBeenCalled() // should be async + waitForUpdate(() => { + expect(spy).toHaveBeenCalled() + }).then(done) + })) + + it('should be called before render and allow mutating state', () => + new Promise((done, reject) => { + done.fail = reject + + const vm = new Vue({ + template: '
{{ msg }}
', + data: { msg: 'foo' }, + setup(_, { _vm }) { + onBeforeUpdate(() => { + _vm.msg += '!' + }) + }, + }).$mount() + expect(vm.$el.textContent).toBe('foo') + vm.msg = 'bar' + waitForUpdate(() => { + expect(vm.$el.textContent).toBe('bar!') + }).then(done) + })) + + it('should not be called after destroy', () => + new Promise((done, reject) => { + done.fail = reject + + const beforeUpdate = vi.fn() + const destroyed = vi.fn() + + Vue.component('todo', { + template: '
{{todo.done}}
', + props: ['todo'], + setup() { + onBeforeUpdate(beforeUpdate) + onUnmounted(destroyed) + }, + }) + + const vm = new Vue({ + template: ` +
+ +
+ `, + data() { + return { + todos: [{ id: 1, done: false }], + } + }, + computed: { + pendingTodos() { + return this.todos.filter((t) => !t.done) + }, + }, + }).$mount() + + vm.todos[0].done = true + waitForUpdate(() => { + expect(destroyed).toHaveBeenCalled() + expect(beforeUpdate).not.toHaveBeenCalled() + }).then(done) + })) + }) + + describe('updated', () => { + it('should be called after update', () => + new Promise((done, reject) => { + done.fail = reject + + const spy = vi.fn() + const vm = new Vue({ + template: '
{{ msg }}
', + data: { msg: 'foo' }, + setup(_, { _vm }) { + onUpdated(() => { + expect(_vm.$el.textContent).toBe('bar') + spy() + }) + }, + }).$mount() + expect(spy).not.toHaveBeenCalled() + vm.msg = 'bar' + expect(spy).not.toHaveBeenCalled() // should be async + waitForUpdate(() => { + expect(spy).toHaveBeenCalled() + }).then(done) + })) + + it('should be called after children are updated', () => + new Promise((done, reject) => { + done.fail = reject + + const calls = [] + const vm = new Vue({ + template: '
{{ msg }}
', + data: { msg: 'foo' }, + components: { + test: { + template: `
`, + setup(_, { _vm }) { + onUpdated(() => { + expect(_vm.$el.textContent).toBe('bar') + calls.push('child') + }) + }, + }, + }, + setup(_, { _vm }) { + onUpdated(() => { + expect(_vm.$el.textContent).toBe('bar') + calls.push('parent') + }) + }, + }).$mount() + + expect(calls).toEqual([]) + vm.msg = 'bar' + expect(calls).toEqual([]) + waitForUpdate(() => { + expect(calls).toEqual(['child', 'parent']) + }).then(done) + })) + + it('should not be called after destroy', () => + new Promise((done, reject) => { + done.fail = reject + + const updated = vi.fn() + const destroyed = vi.fn() + + Vue.component('todo', { + template: '
{{todo.done}}
', + props: ['todo'], + setup() { + onUpdated(updated) + onUnmounted(destroyed) + }, + }) + + const vm = new Vue({ + template: ` +
+ +
+ `, + data() { + return { + todos: [{ id: 1, done: false }], + } + }, + computed: { + pendingTodos() { + return this.todos.filter((t) => !t.done) + }, + }, + }).$mount() + + vm.todos[0].done = true + waitForUpdate(() => { + expect(destroyed).toHaveBeenCalled() + expect(updated).not.toHaveBeenCalled() + }).then(done) + })) + }) + + describe('beforeUnmount', () => { + it('should be called before destroy', () => { + const spy = vi.fn() + const vm = new Vue({ + render() {}, + setup(_, { _vm }) { + onBeforeUnmount(() => { + expect(_vm._isBeingDestroyed).toBe(false) + expect(_vm._isDestroyed).toBe(false) + spy() + }) + }, + }).$mount() + expect(spy).not.toHaveBeenCalled() + vm.$destroy() + vm.$destroy() + expect(spy).toHaveBeenCalled() + expect(spy.mock.calls.length).toBe(1) + }) + }) + + describe('unmounted', () => { + it('should be called after destroy', () => { + const spy = vi.fn() + const vm = new Vue({ + render() {}, + setup(_, { _vm }) { + onUnmounted(() => { + expect(_vm._isBeingDestroyed).toBe(true) + expect(_vm._isDestroyed).toBe(true) + spy() + }) + }, + }).$mount() + expect(spy).not.toHaveBeenCalled() + vm.$destroy() + vm.$destroy() + expect(spy).toHaveBeenCalled() + expect(spy.mock.calls.length).toBe(1) + }) + }) + + describe('errorCaptured', () => { + let globalSpy + + beforeEach(() => { + globalSpy = Vue.config.errorHandler = vi.fn() + }) + + afterEach(() => { + Vue.config.errorHandler = null + }) + + it('should capture error from child component', () => { + const spy = vi.fn() + + let child + let err + const Child = { + setup(_, { _vm }) { + child = _vm + err = new Error('child') + throw err + }, + render() {}, + } + + new Vue({ + setup() { + onErrorCaptured(spy) + }, + render: (h) => h(Child), + }).$mount() + + expect(spy).toHaveBeenCalledWith(err, child, 'data()') + // should propagate by default + expect(globalSpy).toHaveBeenCalledWith(err, child, 'data()') + }) + }) +}) diff --git a/test/apis/state.spec.js b/test/apis/state.spec.js new file mode 100644 index 00000000..936ff7f5 --- /dev/null +++ b/test/apis/state.spec.js @@ -0,0 +1,409 @@ +import Vue from 'vue/dist/vue.common.js' +import { reactive, ref, watch, set, toRefs, computed, unref } from '../../src' + +describe('api/ref', () => { + it('should work with array', () => { + let arr + new Vue({ + setup() { + arr = ref([2]) + arr.value.push(3) + arr.value.unshift(1) + }, + }) + expect(arr.value).toEqual([1, 2, 3]) + }) + + it('should hold a value', () => { + const a = ref(1) + expect(a.value).toBe(1) + a.value = 2 + expect(a.value).toBe(2) + }) + + it('should be reactive', () => + new Promise((done, reject) => { + done.fail = reject + + const a = ref(1) + let dummy + watch( + a, + () => { + dummy = a.value + }, + { immediate: true } + ) + expect(dummy).toBe(1) + a.value = 2 + waitForUpdate(() => { + expect(dummy).toBe(2) + }).then(done) + })) + + it('should make nested properties reactive', () => + new Promise((done, reject) => { + done.fail = reject + + const a = ref({ + count: 1, + }) + let dummy + watch( + a, + () => { + dummy = a.value.count + }, + { deep: true, immediate: true } + ) + expect(dummy).toBe(1) + a.value.count = 2 + waitForUpdate(() => { + expect(dummy).toBe(2) + }).then(done) + })) +}) + +describe('api/reactive', () => { + it('should work', () => + new Promise((done, reject) => { + done.fail = reject + + const app = new Vue({ + setup() { + return { + state: reactive({ + count: 0, + }), + } + }, + render(h) { + return h('div', [h('span', this.state.count)]) + }, + }).$mount() + + expect(app.$el.querySelector('span').textContent).toBe('0') + app.state.count++ + waitForUpdate(() => { + expect(app.$el.querySelector('span').textContent).toBe('1') + }).then(done) + })) + + it('should warn for non-object params', () => { + let warn = vi.spyOn(global.console, 'error').mockImplementation(() => null) + reactive() + expect(warn.mock.calls[0][0]).toMatch( + '[Vue warn]: "reactive()" must be called on an object.' + ) + reactive(false) + expect(warn.mock.calls[1][0]).toMatch( + '[Vue warn]: "reactive()" must be called on an object.' + ) + expect(warn).toBeCalledTimes(2) + warn.mockRestore() + }) +}) + +describe('api/toRefs', () => { + it('should work', () => + new Promise((done, reject) => { + done.fail = reject + + const state = reactive({ + foo: 1, + bar: 2, + }) + + let dummy + watch( + () => state, + () => { + dummy = state.foo + }, + { immediate: true } + ) + const stateAsRefs = toRefs(state) + expect(dummy).toBe(1) + expect(stateAsRefs.foo.value).toBe(1) + expect(stateAsRefs.bar.value).toBe(2) + state.foo++ + waitForUpdate(() => { + dummy = 2 + expect(stateAsRefs.foo.value).toBe(2) + stateAsRefs.foo.value++ + }) + .then(() => { + dummy = 3 + expect(state.foo).toBe(3) + }) + .then(done) + })) + + it('should proxy plain object but not make it a reactive', () => { + let warn = vi.spyOn(global.console, 'error').mockImplementation(() => null) + const spy = vi.fn() + const state = { + foo: 1, + bar: 2, + } + + watch(() => state, spy, { flush: 'sync', lazy: true }) + const stateAsRefs = toRefs(state) + + expect(warn.mock.calls[0][0]).toMatch( + '[Vue warn]: toRefs() expects a reactive object but received a plain one.' + ) + + expect(stateAsRefs.foo.value).toBe(1) + expect(stateAsRefs.bar.value).toBe(2) + state.foo++ + expect(stateAsRefs.foo.value).toBe(2) + + stateAsRefs.foo.value++ + expect(state.foo).toBe(3) + + expect(spy).not.toHaveBeenCalled() + expect(warn).toBeCalledTimes(1) + warn.mockRestore() + }) +}) + +describe('unwrapping', () => { + it('should work', () => { + const obj = reactive({ + a: ref(0), + }) + const objWrapper = ref(obj) + let dummy + watch( + () => obj, + () => { + dummy = obj.a + }, + { deep: true, flush: 'sync', immediate: true } + ) + expect(dummy).toBe(0) + expect(obj.a).toBe(0) + expect(objWrapper.value.a).toBe(0) + obj.a++ + expect(dummy).toBe(1) + objWrapper.value.a++ + expect(dummy).toBe(2) + }) + + it('should not unwrap a ref', () => { + const a = ref(0) + const b = ref(a) + expect(a.value).toBe(0) + expect(b).toBe(a) + }) + + it('should not unwrap a ref when re-assign', () => { + const a = ref('foo') + expect(a.value).toBe('foo') + const b = ref() + a.value = b + expect(a.value).toBe(b) + }) + + it('should unwrap ref in a nested object', () => { + const a = ref(0) + const b = ref({ + count: a, + }) + expect(b.value.count).toBe(0) + a.value++ + expect(b.value.count).toBe(1) + }) + + it('should unwrap when re-assign', () => { + const a = ref() + const b = ref(a) + expect(b.value).toBe(a.value) + const c = ref(0) + b.value = { + count: c, + } + expect(b.value.count).toBe(0) + c.value++ + expect(b.value.count).toBe(1) + }) + + it('should keep reactivity(same ref)', () => { + const a = ref(1) + const obj = reactive({ + a, + b: { + c: a, + }, + }) + let dummy1 + let dummy2 + watch( + () => obj, + () => { + dummy1 = obj.a + dummy2 = obj.b.c + }, + { deep: true, flush: 'sync', immediate: true } + ) + expect(dummy1).toBe(1) + expect(dummy2).toBe(1) + a.value++ + expect(dummy1).toBe(2) + expect(dummy2).toBe(2) + obj.a++ + expect(dummy1).toBe(3) + expect(dummy2).toBe(3) + }) + + it('should keep reactivity(different ref)', () => { + const count = ref(1) + const count1 = ref(1) + const obj = reactive({ + a: count, + b: { + c: count1, + }, + }) + + let dummy1 + let dummy2 + watch( + () => obj, + () => { + dummy1 = obj.a + dummy2 = obj.b.c + }, + { deep: true, flush: 'sync', immediate: true } + ) + expect(dummy1).toBe(1) + expect(dummy2).toBe(1) + expect(obj.a).toBe(1) + expect(obj.b.c).toBe(1) + obj.a++ + expect(dummy1).toBe(2) + expect(dummy2).toBe(1) + expect(count.value).toBe(2) + expect(count1.value).toBe(1) + count.value++ + expect(dummy1).toBe(3) + expect(count.value).toBe(3) + count1.value++ + expect(dummy2).toBe(2) + expect(count1.value).toBe(2) + }) + + it('should keep reactivity(new property of object)', () => { + const count = ref(1) + const obj = reactive({ + a: {}, + b: [], + }) + let dummy + watch( + () => obj, + () => { + dummy = obj.a.foo + }, + { deep: true, flush: 'sync' } + ) + expect(dummy).toBe(undefined) + set(obj.a, 'foo', count) + expect(dummy).toBe(1) + count.value++ + expect(dummy).toBe(2) + obj.a.foo++ + expect(dummy).toBe(3) + }) + + it('ref should be replaced)', () => { + const bRef = ref(1) + const obj = reactive({ + a: { + b: bRef, + }, + }) + + let dummy + watch( + () => obj, + () => { + dummy = obj.a.b + }, + { deep: true, lazy: true, flush: 'sync' } + ) + expect(dummy).toBeUndefined() + const replacedRef = ref(2) + obj.a.b = replacedRef + expect(dummy).toBe(2) + obj.a.b++ + expect(replacedRef.value).toBe(3) + expect(dummy).toBe(3) + + // bRef.value should not change + expect(bRef.value).toBe(1) + }) + + it('should not unwrap ref in Array index', () => { + const a = ref(0) + const state = reactive({ + list: [a], + }) + + expect(state.list[0]).toBe(a) + expect(state.list[0].value).toBe(0) + }) + + it('should unrwap ref', () => { + expect(unref(0)).toBe(0) + expect(unref(ref(0))).toBe(0) + expect(unref({ value: 1 })).toStrictEqual({ value: 1 }) + }) + + it('should now unwrap plain object when using set at Array', () => { + const state = reactive({ + list: [], + }) + + let dummy + watch( + () => state.list, + () => { + dummy = state.list[0].count + }, + { lazy: true, flush: 'sync' } + ) + expect(dummy).toBeUndefined() + const a = ref(0) + set(state.list, 0, { + count: a, + }) + expect(dummy).toBe(a) + }) + + it('should not call the computed property until accessing it', () => { + const spy = vi.fn() + const state = reactive({ + count: 1, + double: computed(() => { + spy() + return state.count * 2 + }), + }) + + expect(spy).not.toHaveBeenCalled() + expect(state.double).toBe(2) + expect(spy).toHaveBeenCalled() + }) + + // #517 + it('should not throw callstack error', () => { + const a = { + b: 1, + } + a.a = a + + reactive(a) + }) +}) diff --git a/test/apis/useCssModule.spec.js b/test/apis/useCssModule.spec.js new file mode 100644 index 00000000..0580ddf5 --- /dev/null +++ b/test/apis/useCssModule.spec.js @@ -0,0 +1,32 @@ +import Vue from 'vue/dist/vue.common.js' +import { useCssModule } from '../../src' + +const style = { whateverStyle: 'whateverStyle' } + +function injectStyles() { + Object.defineProperty(this, '$style', { + configurable: true, + get: function () { + return style + }, + }) +} + +describe('api/useCssModule', () => { + it('should get the same object', () => + new Promise((done) => { + const vm = new Vue({ + beforeCreate() { + injectStyles.call(this) + }, + template: '
{{style}}
', + setup() { + const style = useCssModule() + return { style } + }, + }) + vm.$mount() + expect(vm.style).toBe(style) + done() + })) +}) diff --git a/test/apis/warn.spec.js b/test/apis/warn.spec.js new file mode 100644 index 00000000..2b6aa665 --- /dev/null +++ b/test/apis/warn.spec.js @@ -0,0 +1,34 @@ +import Vue from 'vue/dist/vue.common.js' +import { warn as apiWarn } from '../../src' + +describe('api/warn', () => { + let warn = null + + beforeEach(() => { + warn = vi.spyOn(global.console, 'error').mockImplementation(() => null) + }) + afterEach(() => { + warn.mockRestore() + }) + + it('can be called inside a component', () => { + new Vue({ + setup() { + apiWarn('warned') + }, + template: `
`, + }).$mount() + + expect(warn).toHaveBeenCalledTimes(1) + expect(warn.mock.calls[0][0]).toMatch( + /\[Vue warn\]: warned[\s\S]*\(found in \)/ + ) + }) + + it('can be called outside a component', () => { + apiWarn('warned') + + expect(warn).toHaveBeenCalledTimes(1) + expect(warn).toHaveBeenCalledWith('[Vue warn]: warned') + }) +}) diff --git a/test/apis/watch.spec.js b/test/apis/watch.spec.js new file mode 100644 index 00000000..982f0769 --- /dev/null +++ b/test/apis/watch.spec.js @@ -0,0 +1,950 @@ +import Vue from 'vue/dist/vue.common.js' +import { + ref, + reactive, + watch, + watchEffect, + set, + computed, + nextTick, + markRaw, +} from '../../src' +import { mockWarn } from '../helpers' + +describe('api/watch', () => { + mockWarn(true) + const anyFn = expect.any(Function) + let spy + beforeEach(() => { + spy = vi.fn() + }) + + afterEach(() => { + spy.mockReset() + }) + + it('should work', () => + new Promise((done, reject) => { + done.fail = reject + + const onCleanupSpy = vi.fn() + const vm = new Vue({ + setup() { + const a = ref(1) + watch( + a, + (n, o, _onCleanup) => { + spy(n, o, _onCleanup) + _onCleanup(onCleanupSpy) + }, + { immediate: true } + ) + return { + a, + } + }, + template: `
{{a}}
`, + }).$mount() + expect(spy).toBeCalledTimes(1) + expect(spy).toHaveBeenLastCalledWith(1, undefined, anyFn) + expect(onCleanupSpy).toHaveBeenCalledTimes(0) + vm.a = 2 + vm.a = 3 + expect(spy).toBeCalledTimes(1) + waitForUpdate(() => { + expect(spy).toBeCalledTimes(2) + expect(spy).toHaveBeenLastCalledWith(3, 1, anyFn) + expect(onCleanupSpy).toHaveBeenCalledTimes(1) + + vm.$destroy() + }) + .then(() => { + expect(onCleanupSpy).toHaveBeenCalledTimes(2) + }) + .then(done) + })) + + it('basic usage(value wrapper)', () => + new Promise((done, reject) => { + done.fail = reject + + const vm = new Vue({ + setup() { + const a = ref(1) + watch(a, (n, o) => spy(n, o), { flush: 'pre', immediate: true }) + + return { + a, + } + }, + template: `
{{a}}
`, + }).$mount() + expect(spy).toBeCalledTimes(1) + expect(spy).toHaveBeenLastCalledWith(1, undefined) + vm.a = 2 + expect(spy).toBeCalledTimes(1) + waitForUpdate(() => { + expect(spy).toBeCalledTimes(2) + expect(spy).toHaveBeenLastCalledWith(2, 1) + }).then(done) + })) + + it('basic usage(function)', () => + new Promise((done, reject) => { + done.fail = reject + + const vm = new Vue({ + setup() { + const a = ref(1) + watch( + () => a.value, + (n, o) => spy(n, o), + { immediate: true } + ) + + return { + a, + } + }, + template: `
{{a}}
`, + }).$mount() + expect(spy).toBeCalledTimes(1) + expect(spy).toHaveBeenLastCalledWith(1, undefined) + vm.a = 2 + expect(spy).toBeCalledTimes(1) + waitForUpdate(() => { + expect(spy).toBeCalledTimes(2) + expect(spy).toHaveBeenLastCalledWith(2, 1) + }).then(done) + })) + + it('multiple cbs (after option merge)', () => + new Promise((done, reject) => { + done.fail = reject + + const spy1 = vi.fn() + const a = ref(1) + const Test = Vue.extend({ + setup() { + watch(a, (n, o) => spy1(n, o)) + }, + }) + new Test({ + setup() { + watch(a, (n, o) => spy(n, o)) + return { + a, + } + }, + template: `
{{a}}
`, + }).$mount() + a.value = 2 + waitForUpdate(() => { + expect(spy1).toHaveBeenLastCalledWith(2, 1) + expect(spy).toHaveBeenLastCalledWith(2, 1) + }).then(done) + })) + + it('with option: lazy', () => + new Promise((done, reject) => { + done.fail = reject + + const vm = new Vue({ + setup() { + const a = ref(1) + watch(a, (n, o) => spy(n, o), { lazy: true }) + + return { + a, + } + }, + template: `
{{a}}
`, + }).$mount() + expect(spy).not.toHaveBeenCalled() + vm.a = 2 + waitForUpdate(() => { + expect(spy).toHaveBeenLastCalledWith(2, 1) + }).then(done) + })) + + it('with option: deep', () => + new Promise((done, reject) => { + done.fail = reject + + const vm = new Vue({ + setup() { + const a = ref({ b: 1 }) + watch(a, (n, o) => spy(n, o), { lazy: true, deep: true }) + + return { + a, + } + }, + template: `
{{a}}
`, + }).$mount() + const oldA = vm.a + expect(spy).not.toHaveBeenCalled() + vm.a.b = 2 + expect(spy).not.toHaveBeenCalled() + waitForUpdate(() => { + expect(spy).toHaveBeenLastCalledWith(vm.a, vm.a) + vm.a = { b: 3 } + }) + .then(() => { + expect(spy).toHaveBeenLastCalledWith(vm.a, oldA) + }) + .then(done) + })) + + it('markRaw', () => + new Promise((done, reject) => { + done.fail = reject + + const nestedState = ref(100) + + const state = ref({ + rawValue: markRaw({ + nestedState, + }), + }) + + watch( + state, + () => { + spy() + }, + { deep: true } + ) + + function changeRawValue() { + nestedState.value = Math.random() + } + + changeRawValue() + + waitForUpdate(() => { + expect(spy).not.toBeCalled() + }).then(done) + })) + + it('should flush after render (immediate=false)', () => + new Promise((done, reject) => { + done.fail = reject + + let rerenderedText + const vm = new Vue({ + setup() { + const a = ref(1) + watch( + a, + (newVal, oldVal) => { + spy(newVal, oldVal) + rerenderedText = vm.$el.textContent + }, + { lazy: true, flush: 'post' } + ) + return { + a, + } + }, + render(h) { + return h('div', this.a) + }, + }).$mount() + expect(spy).not.toHaveBeenCalled() + vm.a = 2 + waitForUpdate(() => { + expect(rerenderedText).toBe('2') + expect(spy).toBeCalledTimes(1) + expect(spy).toHaveBeenLastCalledWith(2, 1) + }).then(done) + })) + + it('should flush after render (immediate=true)', () => + new Promise((done, reject) => { + done.fail = reject + + let rerenderedText + var vm = new Vue({ + setup() { + const a = ref(1) + watch( + a, + (newVal, oldVal) => { + spy(newVal, oldVal) + if (vm) { + rerenderedText = vm.$el.textContent + } + }, + { immediate: true, flush: 'post' } + ) + return { + a, + } + }, + render(h) { + return h('div', this.a) + }, + }).$mount() + expect(spy).toBeCalledTimes(1) + expect(spy).toHaveBeenLastCalledWith(1, undefined) + vm.a = 2 + waitForUpdate(() => { + expect(rerenderedText).toBe('2') + expect(spy).toBeCalledTimes(2) + expect(spy).toHaveBeenLastCalledWith(2, 1) + }).then(done) + })) + + it('should flush before render', () => + new Promise((done, reject) => { + done.fail = reject + + const vm = new Vue({ + setup() { + const a = ref(1) + watch( + a, + (newVal, oldVal) => { + spy(newVal, oldVal) + expect(vm.$el.textContent).toBe('1') + }, + { lazy: true, flush: 'pre' } + ) + return { + a, + } + }, + render(h) { + return h('div', this.a) + }, + }).$mount() + vm.a = 2 + waitForUpdate(() => { + expect(spy).toBeCalledTimes(1) + expect(spy).toHaveBeenLastCalledWith(2, 1) + }).then(done) + })) + + it('should flush synchronously', () => + new Promise((done, reject) => { + done.fail = reject + + const vm = new Vue({ + setup() { + const a = ref(1) + watch(a, (n, o) => spy(n, o), { lazy: true, flush: 'sync' }) + return { + a, + } + }, + render(h) { + return h('div', this.a) + }, + }).$mount() + expect(spy).not.toHaveBeenCalled() + vm.a = 2 + expect(spy).toHaveBeenLastCalledWith(2, 1) + vm.a = 3 + expect(spy).toHaveBeenLastCalledWith(3, 2) + waitForUpdate(() => { + expect(spy).toBeCalledTimes(2) + }).then(done) + })) + + it('should support watching unicode paths', () => + new Promise((done, reject) => { + done.fail = reject + + const vm = new Vue({ + setup() { + const a = ref(1) + watch(a, (n, o) => spy(n, o), { lazy: true }) + + return { + 数据: a, + } + }, + render(h) { + return h('div', this['数据']) + }, + }).$mount() + expect(spy).not.toHaveBeenCalled() + vm['数据'] = 2 + expect(spy).not.toHaveBeenCalled() + waitForUpdate(() => { + expect(spy).toHaveBeenLastCalledWith(2, 1) + }).then(done) + })) + + it('should allow to be triggered in setup', () => { + new Vue({ + setup() { + const count = ref(0) + watch(count, (n, o) => spy(n, o), { flush: 'sync', immediate: true }) + count.value++ + }, + }) + expect(spy).toBeCalledTimes(2) + expect(spy).toHaveBeenNthCalledWith(1, 0, undefined) + expect(spy).toHaveBeenNthCalledWith(2, 1, 0) + }) + + it('should run in a expected order', () => + new Promise((done, reject) => { + done.fail = reject + + const result = [] + var vm = new Vue({ + setup() { + const x = ref(0) + + // prettier-ignore + watchEffect(() => { void x.value; result.push('sync effect'); }, { flush: 'sync' }); + // prettier-ignore + watchEffect(() => { void x.value; result.push('pre effect'); }, { flush: 'pre' }); + // prettier-ignore + watchEffect(() => { void x.value; result.push('post effect'); }, { flush: 'post' }); + + // prettier-ignore + watch(x, () => { result.push('sync callback') }, { flush: 'sync', immediate: true }) + // prettier-ignore + watch(x, () => { result.push('pre callback') }, { flush: 'pre', immediate: true }) + // prettier-ignore + watch(x, () => { result.push('post callback') }, { flush: 'post', immediate: true }) + + const inc = () => { + result.push('before inc') + x.value++ + result.push('after inc') + } + + return { x, inc } + }, + template: `
{{x}}
`, + }).$mount() + expect(result).toEqual([ + 'sync effect', + 'pre effect', + 'post effect', + 'sync callback', + 'pre callback', + 'post callback', + ]) + result.length = 0 + + waitForUpdate(() => { + expect(result).toEqual([]) + result.length = 0 + + vm.inc() + }) + .then(() => { + expect(result).toEqual([ + 'before inc', + 'sync effect', + 'sync callback', + 'after inc', + 'pre effect', + 'pre callback', + 'post effect', + 'post callback', + ]) + }) + .then(done) + })) + + describe('simple effect', () => { + it('should work', () => + new Promise((done, reject) => { + done.fail = reject + + let onCleanup + const onCleanupSpy = vi.fn() + const vm = new Vue({ + setup() { + const count = ref(0) + watchEffect((_onCleanup) => { + onCleanup = _onCleanup + _onCleanup(onCleanupSpy) + spy(count.value) + }) + + return { + count, + } + }, + render(h) { + return h('div', this.count) + }, + }).$mount() + expect(spy).toHaveBeenCalled() + waitForUpdate(() => { + expect(onCleanup).toEqual(anyFn) + expect(onCleanupSpy).toHaveBeenCalledTimes(0) + expect(spy).toHaveBeenLastCalledWith(0) + vm.count++ + }) + .then(() => { + expect(spy).toHaveBeenLastCalledWith(1) + expect(onCleanupSpy).toHaveBeenCalledTimes(1) + vm.$destroy() + }) + .then(() => { + expect(onCleanupSpy).toHaveBeenCalledTimes(2) + }) + .then(done) + })) + + it('sync=true', () => { + const vm = new Vue({ + setup() { + const count = ref(0) + watchEffect( + () => { + spy(count.value) + }, + { + flush: 'sync', + } + ) + + return { + count, + } + }, + }) + expect(spy).toHaveBeenLastCalledWith(0) + vm.count++ + expect(spy).toHaveBeenLastCalledWith(1) + }) + + it('warn immediate option when using effect', async () => { + const count = ref(0) + let dummy + watchEffect( + () => { + dummy = count.value + }, + { immediate: false } + ) + expect(dummy).toBe(0) + expect(`"immediate" option is only respected`).toHaveBeenWarned() + + count.value++ + await nextTick() + expect(dummy).toBe(1) + }) + + it('warn and not respect deep option when using effect', async () => { + const arr = ref([1, [2]]) + const spy = vi.fn() + watchEffect( + () => { + spy() + return arr + }, + { deep: true } + ) + expect(spy).toHaveBeenCalledTimes(1) + arr.value[1][0] = 3 + await nextTick() + expect(spy).toHaveBeenCalledTimes(1) + expect(`"deep" option is only respected`).toHaveBeenWarned() + }) + }) + + describe('Multiple sources', () => { + let obj1, obj2 + it('do not store the intermediate state', () => + new Promise((done, reject) => { + done.fail = reject + + new Vue({ + setup() { + obj1 = reactive({ a: 1 }) + obj2 = reactive({ a: 2 }) + watch([() => obj1.a, () => obj2.a], (n, o) => spy(n, o), { + immediate: true, + }) + return { + obj1, + obj2, + } + }, + template: `
{{obj1.a}} {{obj2.a}}
`, + }).$mount() + expect(spy).toBeCalledTimes(1) + expect(spy).toHaveBeenLastCalledWith([1, 2], []) + obj1.a = 2 + obj2.a = 3 + + obj1.a = 3 + obj2.a = 4 + waitForUpdate(() => { + expect(spy).toBeCalledTimes(2) + expect(spy).toHaveBeenLastCalledWith([3, 4], [1, 2]) + obj2.a = 5 + obj2.a = 6 + }) + .then(() => { + expect(spy).toBeCalledTimes(3) + expect(spy).toHaveBeenLastCalledWith([3, 6], [3, 4]) + }) + .then(done) + })) + + it('basic usage(immediate=true, flush=none-sync)', () => + new Promise((done, reject) => { + done.fail = reject + + const vm = new Vue({ + setup() { + const a = ref(1) + const b = ref(1) + watch([a, b], (n, o) => spy(n, o), { + flush: 'post', + immediate: true, + }) + + return { + a, + b, + } + }, + template: `
{{a}} {{b}}
`, + }).$mount() + expect(spy).toBeCalledTimes(1) + expect(spy).toHaveBeenLastCalledWith([1, 1], []) + vm.a = 2 + expect(spy).toBeCalledTimes(1) + waitForUpdate(() => { + expect(spy).toBeCalledTimes(2) + expect(spy).toHaveBeenLastCalledWith([2, 1], [1, 1]) + vm.a = 3 + vm.b = 3 + }) + .then(() => { + expect(spy).toBeCalledTimes(3) + expect(spy).toHaveBeenLastCalledWith([3, 3], [2, 1]) + }) + .then(done) + })) + + it('basic usage(immediate=false, flush=none-sync)', () => + new Promise((done, reject) => { + done.fail = reject + + const vm = new Vue({ + setup() { + const a = ref(1) + const b = ref(1) + watch([a, b], (n, o) => spy(n, o), { + immediate: false, + flush: 'post', + }) + + return { + a, + b, + } + }, + template: `
{{a}} {{b}}
`, + }).$mount() + vm.a = 2 + expect(spy).not.toHaveBeenCalled() + waitForUpdate(() => { + expect(spy).toBeCalledTimes(1) + expect(spy).toHaveBeenLastCalledWith([2, 1], [1, 1]) + vm.a = 3 + vm.b = 3 + }) + .then(() => { + expect(spy).toBeCalledTimes(2) + expect(spy).toHaveBeenLastCalledWith([3, 3], [2, 1]) + }) + .then(done) + })) + + it('basic usage(immediate=true, flush=sync)', () => { + const vm = new Vue({ + setup() { + const a = ref(1) + const b = ref(1) + watch([a, b], (n, o) => spy(n, o), { immediate: true, flush: 'sync' }) + + return { + a, + b, + } + }, + }) + expect(spy).toBeCalledTimes(1) + expect(spy).toHaveBeenLastCalledWith([1, 1], []) + vm.a = 2 + expect(spy).toBeCalledTimes(2) + expect(spy).toHaveBeenLastCalledWith([2, 1], [1, 1]) + vm.a = 3 + vm.b = 3 + expect(spy.mock.calls.length).toBe(4) + expect(spy).toHaveBeenNthCalledWith(3, [3, 1], [2, 1]) + expect(spy).toHaveBeenNthCalledWith(4, [3, 3], [3, 1]) + }) + + it('basic usage(immediate=false, flush=sync)', () => { + const vm = new Vue({ + setup() { + const a = ref(1) + const b = ref(1) + watch([a, b], (n, o) => spy(n, o), { lazy: true, flush: 'sync' }) + + return { + a, + b, + } + }, + }) + expect(spy).not.toHaveBeenCalled() + vm.a = 2 + expect(spy).toBeCalledTimes(1) + expect(spy).toHaveBeenLastCalledWith([2, 1], [1, 1]) + vm.a = 3 + vm.b = 3 + expect(spy).toBeCalledTimes(3) + expect(spy).toHaveBeenNthCalledWith(2, [3, 1], [2, 1]) + expect(spy).toHaveBeenNthCalledWith(3, [3, 3], [3, 1]) + }) + + it('config.errorHandler should capture render errors', async () => { + new Vue({ + setup() { + const a = ref(1) + watch( + a, + async () => { + throw new Error('userWatcherCallback error') + }, + { immediate: true } + ) + return { + a, + } + }, + template: `
{{a}}
`, + }).$mount() + await nextTick() + expect(`userWatcherCallback error`).toHaveBeenWarned() + }) + }) + + describe('Out of setup', () => { + it('should work', () => + new Promise((done, reject) => { + done.fail = reject + + const obj = reactive({ a: 1 }) + watch( + () => obj.a, + (n, o) => spy(n, o), + { immediate: true } + ) + expect(spy).toHaveBeenLastCalledWith(1, undefined) + obj.a = 2 + waitForUpdate(() => { + expect(spy).toBeCalledTimes(2) + expect(spy).toHaveBeenLastCalledWith(2, 1) + }).then(done) + })) + + it('simple effect', () => + new Promise((done, reject) => { + done.fail = reject + + const obj = reactive({ a: 1 }) + watchEffect(() => spy(obj.a)) + expect(spy).toHaveBeenCalled() + waitForUpdate(() => { + expect(spy).toBeCalledTimes(1) + expect(spy).toHaveBeenLastCalledWith(1) + obj.a = 2 + }) + .then(() => { + expect(spy).toBeCalledTimes(2) + expect(spy).toHaveBeenLastCalledWith(2) + }) + .then(done) + })) + }) + + describe('cleanup', () => { + function getAsyncValue(val) { + let handle + let resolve + const p = new Promise((_resolve) => { + resolve = _resolve + handle = setTimeout(() => { + resolve(val) + }, 0) + }) + + p.cancel = () => { + clearTimeout(handle) + resolve('canceled') + } + return p + } + + it('work with effect', () => + new Promise((done, reject) => { + done.fail = reject + + const id = ref(1) + const promises = [] + watchEffect((onCleanup) => { + const val = getAsyncValue(id.value) + promises.push(val) + onCleanup(() => { + val.cancel() + }) + }) + waitForUpdate(() => { + id.value = 2 + }) + .thenWaitFor(async (next) => { + const values = await Promise.all(promises) + expect(values).toEqual(['canceled', 2]) + next() + }) + .then(done) + })) + + it('run cleanup when watch stops (effect)', () => + new Promise((done, reject) => { + done.fail = reject + + const spy = vi.fn() + const cleanup = vi.fn() + const stop = watchEffect((onCleanup) => { + spy() + onCleanup(cleanup) + }) + waitForUpdate(() => { + expect(spy).toHaveBeenCalled() + stop() + }) + .then(() => { + expect(cleanup).toHaveBeenCalled() + }) + .then(done) + })) + + it('run cleanup when watch stops', () => { + const id = ref(1) + const spy = vi.fn() + const cleanup = vi.fn() + const stop = watch( + id, + (value, oldValue, onCleanup) => { + spy(value) + onCleanup(cleanup) + }, + { immediate: true } + ) + + expect(spy).toHaveBeenCalledWith(1) + stop() + expect(cleanup).toHaveBeenCalled() + }) + + it('should not collect reactive in onCleanup', () => + new Promise((done, reject) => { + done.fail = reject + + const ref1 = ref(1) + const ref2 = ref(1) + watchEffect((onCleanup) => { + spy(ref1.value) + onCleanup(() => { + ref2.value = ref2.value + 1 + }) + }) + waitForUpdate(() => { + expect(spy).toBeCalledTimes(1) + expect(spy).toHaveBeenLastCalledWith(1) + ref1.value++ + }) + .then(() => { + expect(spy).toBeCalledTimes(2) + expect(spy).toHaveBeenLastCalledWith(2) + ref2.value = 10 + }) + .then(() => { + expect(spy).toBeCalledTimes(2) + }) + .then(done) + })) + + it('work with callback ', () => + new Promise((done, reject) => { + done.fail = reject + + const id = ref(1) + const promises = [] + watch( + id, + (newVal, oldVal, onCleanup) => { + const val = getAsyncValue(newVal) + promises.push(val) + onCleanup(() => { + val.cancel() + }) + }, + { immediate: true } + ) + id.value = 2 + waitForUpdate() + .thenWaitFor(async (next) => { + const values = await Promise.all(promises) + expect(values).toEqual(['canceled', 2]) + next() + }) + .then(done) + })) + }) + + it('should execute watch when new key is added', () => { + const r = reactive({}) + + const cb = vi.fn() + + watch(r, cb, { deep: true }) + + set(r, 'a', 1) + + expect(cb).toHaveBeenCalled() + }) + + it('watching sources: ref<[]>', async () => { + const foo = ref([1]) + const cb = vi.fn() + watch(foo, cb) + foo.value = foo.value.slice() + await nextTick() + expect(cb).toBeCalledTimes(1) + }) + + it('watching multiple sources: computed', async () => { + const number = ref(1) + const div2 = computed(() => { + return number.value > 2 ? '>2' : '<=2' + }) + const div3 = computed(() => { + return number.value > 3 ? '>3' : '<=3' + }) + const cb = vi.fn() + watch([div2, div3], cb) + number.value = 2 + await nextTick() + expect(cb).toHaveBeenCalledTimes(0) + }) +}) diff --git a/test/createApp.spec.ts b/test/createApp.spec.ts new file mode 100644 index 00000000..097a52c9 --- /dev/null +++ b/test/createApp.spec.ts @@ -0,0 +1,106 @@ +import { createApp, defineComponent, ref, nextTick } from '../src' + +describe('createApp', () => { + it('should work', async () => { + const app = createApp({ + setup() { + return { + a: ref(1), + } + }, + template: '

{{a}}

', + }) + const vm = app.mount() + + await nextTick() + expect(vm.$el.textContent).toBe('1') + }) + + it('should work with rootProps', async () => { + const app = createApp( + defineComponent({ + props: { + msg: String, + }, + template: '

{{msg}}

', + }), + { + msg: 'foobar', + } + ) + const vm = app.mount() + + await nextTick() + expect(vm.$el.textContent).toBe('foobar') + }) + + it('should work with components', async () => { + const Foo = defineComponent({ + props: { + msg: { + type: String, + required: true, + }, + }, + template: '

{{msg}}

', + }) + + const app = createApp( + defineComponent({ + props: { + msg: String, + }, + template: '', + }), + { + msg: 'foobar', + } + ) + app.component('Foo', Foo) + const vm = app.mount() + + await nextTick() + expect(vm.$el.textContent).toBe('foobar') + }) + + it('should work with provide', async () => { + const Foo = defineComponent({ + inject: ['msg'], + template: '

{{msg}}

', + }) + + const app = createApp( + defineComponent({ + template: '', + components: { Foo }, + }) + ) + app.provide('msg', 'foobar') + const vm = app.mount() + + await nextTick() + expect(vm.$el.textContent).toBe('foobar') + }) + + it("should respect root component's provide", async () => { + const Foo = defineComponent({ + inject: ['msg'], + template: '

{{msg}}

', + }) + + const app = createApp( + defineComponent({ + template: '', + provide: { + msg: 'root component', + }, + components: { Foo }, + }) + ) + app.provide('msg', 'application') + const vm = app.mount() + + await nextTick() + expect(vm.$el.textContent).toBe('root component') + }) +}) diff --git a/test/functions/computed.spec.js b/test/functions/computed.spec.js deleted file mode 100644 index aebc4dab..00000000 --- a/test/functions/computed.spec.js +++ /dev/null @@ -1,169 +0,0 @@ -const Vue = require('vue/dist/vue.common.js'); -const { plugin, value, computed } = require('../../src'); - -Vue.use(plugin); - -describe('Hooks computed', () => { - beforeEach(() => { - warn = jest.spyOn(global.console, 'error').mockImplementation(() => null); - }); - afterEach(() => { - warn.mockRestore(); - }); - - it('basic usage', done => { - const vm = new Vue({ - template: '
{{ b }}
', - setup() { - const a = value(1); - const b = computed(() => a.value + 1); - return { - a, - b, - }; - }, - }).$mount(); - expect(vm.b).toBe(2); - expect(vm.$el.textContent).toBe('2'); - vm.a = 2; - expect(vm.b).toBe(3); - waitForUpdate(() => { - expect(vm.$el.textContent).toBe('3'); - }).then(done); - }); - - it('with setter', done => { - const vm = new Vue({ - template: '
{{ b }}
', - setup() { - const a = value(1); - const b = computed(() => a.value + 1, v => (a.value = v - 1)); - return { - a, - b, - }; - }, - }).$mount(); - expect(vm.b).toBe(2); - expect(vm.$el.textContent).toBe('2'); - vm.a = 2; - expect(vm.b).toBe(3); - waitForUpdate(() => { - expect(vm.$el.textContent).toBe('3'); - vm.b = 1; - expect(vm.a).toBe(0); - }) - .then(() => { - expect(vm.$el.textContent).toBe('1'); - }) - .then(done); - }); - - it('warn assigning to computed with no setter', () => { - const vm = new Vue({ - setup() { - const b = computed(() => 1); - return { - b, - }; - }, - }); - vm.b = 2; - expect(warn.mock.calls[0][0]).toMatch( - '[Vue warn]: Computed property "b" was assigned to but it has no setter.' - ); - }); - - it('watching computed', done => { - const spy = jest.fn(); - const vm = new Vue({ - setup() { - const a = value(1); - const b = computed(() => a.value + 1); - return { - a, - b, - }; - }, - }); - vm.$watch('b', spy); - vm.a = 2; - waitForUpdate(() => { - expect(spy).toHaveBeenCalledWith(3, 2); - }).then(done); - }); - - it('caching', () => { - const spy = jest.fn(); - const vm = new Vue({ - setup() { - const a = value(1); - const b = computed(() => { - spy(); - return a.value + 1; - }); - return { - a, - b, - }; - }, - }); - expect(spy.mock.calls.length).toBe(0); - vm.b; - expect(spy.mock.calls.length).toBe(1); - vm.b; - expect(spy.mock.calls.length).toBe(1); - }); - - it('as component', done => { - const Comp = Vue.extend({ - template: `
{{ b }} {{ c }}
`, - setup() { - const a = value(1); - const b = computed(() => { - return a.value + 1; - }); - return { - a, - b, - }; - }, - }); - - const vm = new Comp({ - setup(_, { _vm }) { - const c = computed(() => { - return _vm.b + 1; - }); - - return { - c, - }; - }, - }).$mount(); - expect(vm.b).toBe(2); - expect(vm.c).toBe(3); - expect(vm.$el.textContent).toBe('2 3'); - vm.a = 2; - expect(vm.b).toBe(3); - expect(vm.c).toBe(4); - waitForUpdate(() => { - expect(vm.$el.textContent).toBe('3 4'); - }).then(done); - }); - - it('rethrow computed error', () => { - const vm = new Vue({ - setup() { - const a = computed(() => { - throw new Error('rethrow'); - }); - - return { - a, - }; - }, - }); - expect(() => vm.a).toThrowError('rethrow'); - }); -}); diff --git a/test/functions/inject.spec.js b/test/functions/inject.spec.js deleted file mode 100644 index dc8e0627..00000000 --- a/test/functions/inject.spec.js +++ /dev/null @@ -1,125 +0,0 @@ -const Vue = require('vue/dist/vue.common.js'); -const { plugin, inject, provide, value } = require('../../src'); - -Vue.use(plugin); - -let injected; -const injectedComp = { - render() {}, - setup() { - return { - foo: inject('foo'), - bar: inject('bar'), - }; - }, - created() { - injected = [this.foo, this.bar]; - }, -}; - -beforeEach(() => { - injected = null; -}); - -describe('Hooks provide/inject', () => { - beforeEach(() => { - warn = jest.spyOn(global.console, 'error').mockImplementation(() => null); - }); - afterEach(() => { - warn.mockRestore(); - }); - - it('should work', () => { - new Vue({ - template: ``, - setup() { - provide('foo', 1); - provide('bar', false); - }, - components: { - child: { - template: ``, - components: { - injectedComp, - }, - }, - }, - }).$mount(); - - expect(injected).toEqual([1, false]); - }); - - it('should work for reactive value', done => { - const Msg = Symbol(); - const app = new Vue({ - template: ``, - setup() { - provide(Msg, value('hello')); - }, - components: { - child: { - template: `
{{ msg }}
`, - setup() { - return { - msg: inject(Msg), - }; - }, - }, - }, - }).$mount(); - - app.$children[0].msg = 'bar'; - waitForUpdate(() => { - expect(app.$el.textContent).toBe('bar'); - }).then(done); - }); - - it('should return wrapper values', done => { - const State = Symbol(); - let obj; - const app = new Vue({ - template: ``, - setup() { - provide(State, { msg: 'foo' }); - }, - components: { - child: { - template: `
{{ state.msg }}
`, - setup() { - obj = inject(State); - return { - state: obj, - }; - }, - }, - }, - }).$mount(); - expect(obj.value.msg).toBe('foo'); - app.$children[0].state.msg = 'bar'; - waitForUpdate(() => { - expect(app.$el.textContent).toBe('bar'); - }).then(done); - }); - - it('should warn when assign to a injected value', () => { - const State = Symbol(); - let obj; - new Vue({ - template: ``, - setup() { - provide(State, { msg: 'foo' }); - }, - components: { - child: { - setup() { - obj = inject(State); - }, - template: `
`, - }, - }, - }).$mount(); - expect(obj.value.msg).toBe('foo'); - obj.value = {}; - expect(warn.mock.calls[0][0]).toMatch("[Vue warn]: The injectd value can't be re-assigned"); - }); -}); diff --git a/test/functions/lifecycle.spec.js b/test/functions/lifecycle.spec.js deleted file mode 100644 index 1ae33e63..00000000 --- a/test/functions/lifecycle.spec.js +++ /dev/null @@ -1,338 +0,0 @@ -const Vue = require('vue/dist/vue.common.js'); -const { - plugin, - onCreated, - onBeforeMount, - onMounted, - onBeforeUpdate, - onUpdated, - onBeforeDestroy, - onDestroyed, -} = require('../../src'); - -Vue.use(plugin); - -describe('Hooks lifecycle', () => { - describe('created', () => { - it('should have completed observation', () => { - const spy = jest.fn(); - new Vue({ - data() { - return { - a: 1, - }; - }, - setup(_, { _vm }) { - onCreated(() => { - expect(_vm.a).toBe(1); - spy(); - }); - }, - }); - expect(spy).toHaveBeenCalled(); - }); - }); - - describe('beforeMount', () => { - it('should not have mounted', () => { - const spy = jest.fn(); - const vm = new Vue({ - render() {}, - setup(_, { _vm }) { - onBeforeMount(() => { - expect(_vm._isMounted).toBe(false); - expect(_vm.$el).toBeUndefined(); // due to empty mount - expect(_vm._vnode).toBeNull(); - expect(_vm._watcher).toBeNull(); - spy(); - }); - }, - }); - expect(spy).not.toHaveBeenCalled(); - vm.$mount(); - expect(spy).toHaveBeenCalled(); - }); - }); - - describe('mounted', () => { - it('should have mounted', () => { - const spy = jest.fn(); - const vm = new Vue({ - template: '
', - setup(_, { _vm }) { - onMounted(() => { - expect(_vm._isMounted).toBe(true); - expect(_vm.$el.tagName).toBe('DIV'); - expect(_vm._vnode.tag).toBe('div'); - spy(); - }); - }, - }); - expect(spy).not.toHaveBeenCalled(); - vm.$mount(); - expect(spy).toHaveBeenCalled(); - }); - - it('should call for manually mounted instance with parent', () => { - const spy = jest.fn(); - const parent = new Vue(); - expect(spy).not.toHaveBeenCalled(); - new Vue({ - parent, - template: '
', - setup() { - onMounted(() => { - spy(); - }); - }, - }).$mount(); - expect(spy).toHaveBeenCalled(); - }); - - it('should mount child parent in correct order', () => { - const calls = []; - new Vue({ - template: '
', - setup() { - onMounted(() => { - calls.push('parent'); - }); - }, - components: { - test: { - template: '', - setup(_, { _vm }) { - onMounted(() => { - expect(_vm.$el.parentNode).toBeTruthy(); - calls.push('child'); - }); - }, - components: { - nested: { - template: '
', - setup(_, { _vm }) { - onMounted(() => { - expect(_vm.$el.parentNode).toBeTruthy(); - calls.push('nested'); - }); - }, - }, - }, - }, - }, - }).$mount(); - expect(calls).toEqual(['nested', 'child', 'parent']); - }); - }); - - describe('beforeUpdate', () => { - it('should be called before update', done => { - const spy = jest.fn(); - const vm = new Vue({ - template: '
{{ msg }}
', - data: { msg: 'foo' }, - setup(_, { _vm }) { - onBeforeUpdate(() => { - expect(_vm.$el.textContent).toBe('foo'); - spy(); - }); - }, - }).$mount(); - expect(spy).not.toHaveBeenCalled(); - vm.msg = 'bar'; - expect(spy).not.toHaveBeenCalled(); // should be async - waitForUpdate(() => { - expect(spy).toHaveBeenCalled(); - }).then(done); - }); - - it('should be called before render and allow mutating state', done => { - const vm = new Vue({ - template: '
{{ msg }}
', - data: { msg: 'foo' }, - setup(_, { _vm }) { - onBeforeUpdate(() => { - _vm.msg += '!'; - }); - }, - }).$mount(); - expect(vm.$el.textContent).toBe('foo'); - vm.msg = 'bar'; - waitForUpdate(() => { - expect(vm.$el.textContent).toBe('bar!'); - }).then(done); - }); - - it('should not be called after destroy', done => { - const beforeUpdate = jest.fn(); - const destroyed = jest.fn(); - - Vue.component('todo', { - template: '
{{todo.done}}
', - props: ['todo'], - setup() { - onBeforeUpdate(beforeUpdate); - onDestroyed(destroyed); - }, - }); - - const vm = new Vue({ - template: ` -
- -
- `, - data() { - return { - todos: [{ id: 1, done: false }], - }; - }, - computed: { - pendingTodos() { - return this.todos.filter(t => !t.done); - }, - }, - }).$mount(); - - vm.todos[0].done = true; - waitForUpdate(() => { - expect(destroyed).toHaveBeenCalled(); - expect(beforeUpdate).not.toHaveBeenCalled(); - }).then(done); - }); - }); - - describe('updated', () => { - it('should be called after update', done => { - const spy = jest.fn(); - const vm = new Vue({ - template: '
{{ msg }}
', - data: { msg: 'foo' }, - setup(_, { _vm }) { - onUpdated(() => { - expect(_vm.$el.textContent).toBe('bar'); - spy(); - }); - }, - }).$mount(); - expect(spy).not.toHaveBeenCalled(); - vm.msg = 'bar'; - expect(spy).not.toHaveBeenCalled(); // should be async - waitForUpdate(() => { - expect(spy).toHaveBeenCalled(); - }).then(done); - }); - - it('should be called after children are updated', done => { - const calls = []; - const vm = new Vue({ - template: '
{{ msg }}
', - data: { msg: 'foo' }, - components: { - test: { - template: `
`, - setup(_, { _vm }) { - onUpdated(() => { - expect(_vm.$el.textContent).toBe('bar'); - calls.push('child'); - }); - }, - }, - }, - setup(_, { _vm }) { - onUpdated(() => { - expect(_vm.$el.textContent).toBe('bar'); - calls.push('parent'); - }); - }, - }).$mount(); - - expect(calls).toEqual([]); - vm.msg = 'bar'; - expect(calls).toEqual([]); - waitForUpdate(() => { - expect(calls).toEqual(['child', 'parent']); - }).then(done); - }); - - it('should not be called after destroy', done => { - const updated = jest.fn(); - const destroyed = jest.fn(); - - Vue.component('todo', { - template: '
{{todo.done}}
', - props: ['todo'], - setup() { - onUpdated(updated); - onDestroyed(destroyed); - }, - }); - - const vm = new Vue({ - template: ` -
- -
- `, - data() { - return { - todos: [{ id: 1, done: false }], - }; - }, - computed: { - pendingTodos() { - return this.todos.filter(t => !t.done); - }, - }, - }).$mount(); - - vm.todos[0].done = true; - waitForUpdate(() => { - expect(destroyed).toHaveBeenCalled(); - expect(updated).not.toHaveBeenCalled(); - }).then(done); - }); - }); - - describe('beforeDestroy', () => { - it('should be called before destroy', () => { - const spy = jest.fn(); - const vm = new Vue({ - render() {}, - setup(_, { _vm }) { - onBeforeDestroy(() => { - expect(_vm._isBeingDestroyed).toBe(false); - expect(_vm._isDestroyed).toBe(false); - spy(); - }); - }, - }).$mount(); - expect(spy).not.toHaveBeenCalled(); - vm.$destroy(); - vm.$destroy(); - expect(spy).toHaveBeenCalled(); - expect(spy.mock.calls.length).toBe(1); - }); - }); - - describe('destroyed', () => { - it('should be called after destroy', () => { - const spy = jest.fn(); - const vm = new Vue({ - render() {}, - setup(_, { _vm }) { - onDestroyed(() => { - expect(_vm._isBeingDestroyed).toBe(true); - expect(_vm._isDestroyed).toBe(true); - spy(); - }); - }, - }).$mount(); - expect(spy).not.toHaveBeenCalled(); - vm.$destroy(); - vm.$destroy(); - expect(spy).toHaveBeenCalled(); - expect(spy.mock.calls.length).toBe(1); - }); - }); -}); diff --git a/test/functions/state.spec.js b/test/functions/state.spec.js deleted file mode 100644 index bfccdb17..00000000 --- a/test/functions/state.spec.js +++ /dev/null @@ -1,222 +0,0 @@ -const Vue = require('vue/dist/vue.common.js'); -const { plugin, state, value, watch, set } = require('../../src'); - -Vue.use(plugin); - -describe('Hooks value', () => { - it('should proxy and be reactive', done => { - const vm = new Vue({ - setup() { - return { - name: value(null), - msg: value('foo'), - }; - }, - template: '
{{name}}, {{ msg }}
', - }).$mount(); - vm.name = 'foo'; - vm.msg = 'bar'; - waitForUpdate(() => { - expect(vm.$el.textContent).toBe('foo, bar'); - }).then(done); - }); -}); - -describe('Hooks state', () => { - it('should work', done => { - const app = new Vue({ - setup() { - return { - state: state({ - count: 0, - }), - }; - }, - render(h) { - return h('div', [h('span', this.state.count)]); - }, - }).$mount(); - - expect(app.$el.querySelector('span').textContent).toBe('0'); - app.state.count++; - waitForUpdate(() => { - expect(app.$el.querySelector('span').textContent).toBe('1'); - }).then(done); - }); -}); - -describe('reactivity/value', () => { - it('should hold a value', () => { - const a = value(1); - expect(a.value).toBe(1); - a.value = 2; - expect(a.value).toBe(2); - }); - - it('should be reactive', () => { - const a = value(1); - let dummy; - watch(a, () => { - dummy = a.value; - }); - expect(dummy).toBe(1); - a.value = 2; - expect(dummy).toBe(2); - }); - - it('should make nested properties reactive', () => { - const a = value({ - count: 1, - }); - let dummy; - watch( - a, - () => { - dummy = a.value.count; - }, - { deep: true } - ); - expect(dummy).toBe(1); - a.value.count = 2; - expect(dummy).toBe(2); - }); - - it('should work like a normal property when nested in an observable(same ref)', () => { - const a = value(1); - const obj = state({ - a, - b: { - c: a, - d: [a], - }, - }); - let dummy1; - let dummy2; - let dummy3; - watch( - () => obj, - () => { - dummy1 = obj.a; - dummy2 = obj.b.c; - dummy3 = obj.b.d[0]; - }, - { deep: true } - ); - expect(dummy1).toBe(1); - expect(dummy2).toBe(1); - expect(dummy3).toBe(1); - a.value++; - expect(dummy1).toBe(2); - expect(dummy2).toBe(2); - expect(dummy3).toBe(2); - obj.a++; - expect(dummy1).toBe(3); - expect(dummy2).toBe(3); - expect(dummy3).toBe(3); - }); - - it('should work like a normal property when nested in an observable(different ref)', () => { - const count = value(1); - const count1 = value(1); - const obj = state({ - a: count, - b: { - c: count1, - }, - }); - - let dummy1; - let dummy2; - watch( - () => obj, - () => { - dummy1 = obj.a; - dummy2 = obj.b.c; - }, - { deep: true } - ); - expect(dummy1).toBe(1); - expect(dummy2).toBe(1); - expect(obj.a).toBe(1); - expect(obj.b.c).toBe(1); - obj.a++; - expect(dummy1).toBe(2); - expect(dummy2).toBe(1); - expect(count.value).toBe(2); - expect(count1.value).toBe(1); - count.value++; - expect(dummy1).toBe(3); - expect(count.value).toBe(3); - count1.value++; - expect(dummy2).toBe(2); - expect(count1.value).toBe(2); - }); - - it('should work like a normal property when nested in an observable(wrapper overwrite)', () => { - const obj = state({ - a: { - b: 1, - }, - }); - - let dummy; - watch( - () => obj, - () => { - dummy = obj.a.b; - }, - { deep: true, lazy: true } - ); - expect(dummy).toBeUndefined(); - const wrapperC = value(1); - obj.a.b = wrapperC; - expect(dummy).toBe(1); - obj.a.b++; - expect(dummy).toBe(2); - }); - - it('should work like a normal property when nested in an observable(new property of object)', () => { - const count = value(1); - const obj = state({ - a: {}, - b: [], - }); - let dummy; - watch( - () => obj, - () => { - dummy = obj.a.foo; - }, - { deep: true } - ); - expect(dummy).toBe(undefined); - set(obj.a, 'foo', count); - expect(dummy).toBe(1); - count.value++; - expect(dummy).toBe(2); - obj.a.foo++; - expect(dummy).toBe(3); - }); - - it('should work like a normal property when nested in an observable(new property of array)', () => { - const count = value(1); - const obj = state({ - a: [], - }); - let dummy; - watch( - () => obj, - () => { - dummy = obj.a[0]; - }, - { deep: true } - ); - expect(dummy).toBe(undefined); - set(obj.a, 0, count); - expect(dummy).toBe(1); - count.value++; - expect(dummy).toBe(2); - obj.a[0]++; - expect(dummy).toBe(3); - }); -}); diff --git a/test/functions/watch.spec.js b/test/functions/watch.spec.js deleted file mode 100644 index 48a98e29..00000000 --- a/test/functions/watch.spec.js +++ /dev/null @@ -1,371 +0,0 @@ -const Vue = require('vue/dist/vue.common.js'); -const { plugin, value, state, watch } = require('../../src'); - -Vue.use(plugin); - -describe('Hooks watch', () => { - let spy; - beforeEach(() => { - spy = jest.fn(); - }); - - afterEach(() => { - spy.mockReset(); - }); - - it('basic usage(value warpper)', done => { - const vm = new Vue({ - setup() { - const a = value(1); - watch(a, spy, { flush: 'pre' }); - - return { - a, - }; - }, - template: `
{{a}}
`, - }).$mount(); - expect(spy.mock.calls.length).toBe(1); - expect(spy).toHaveBeenLastCalledWith(1, undefined); - vm.a = 2; - expect(spy.mock.calls.length).toBe(1); - waitForUpdate(() => { - expect(spy.mock.calls.length).toBe(2); - expect(spy).toHaveBeenLastCalledWith(2, 1); - }).then(done); - }); - - it('basic usage(function)', done => { - const vm = new Vue({ - setup() { - const a = value(1); - watch(() => a.value, spy); - - return { - a, - }; - }, - template: `
{{a}}
`, - }).$mount(); - expect(spy.mock.calls.length).toBe(1); - expect(spy).toHaveBeenLastCalledWith(1, undefined); - vm.a = 2; - expect(spy.mock.calls.length).toBe(1); - waitForUpdate(() => { - expect(spy.mock.calls.length).toBe(2); - expect(spy).toHaveBeenLastCalledWith(2, 1); - }).then(done); - }); - - it('basic usage(multiple sources, lazy=false, flush=none-sync)', done => { - const vm = new Vue({ - setup() { - const a = value(1); - const b = value(1); - watch([a, b], spy, { lazy: false, flush: 'post' }); - - return { - a, - b, - }; - }, - template: `
{{a}} {{b}}
`, - }).$mount(); - expect(spy.mock.calls.length).toBe(1); - expect(spy).toHaveBeenLastCalledWith([1, 1], [undefined, undefined]); - vm.a = 2; - expect(spy.mock.calls.length).toBe(1); - waitForUpdate(() => { - expect(spy.mock.calls.length).toBe(2); - expect(spy).toHaveBeenLastCalledWith([2, 1], [1, undefined]); - vm.a = 3; - vm.b = 3; - }) - .then(() => { - expect(spy.mock.calls.length).toBe(3); - expect(spy).toHaveBeenLastCalledWith([3, 3], [2, 1]); - }) - .then(done); - }); - - it('basic usage(multiple sources, lazy=true, flush=none-sync)', done => { - const vm = new Vue({ - setup() { - const a = value(1); - const b = value(1); - watch([a, b], spy, { lazy: true, flush: 'post' }); - - return { - a, - b, - }; - }, - template: `
{{a}} {{b}}
`, - }).$mount(); - vm.a = 2; - expect(spy).not.toHaveBeenCalled(); - waitForUpdate(() => { - expect(spy.mock.calls.length).toBe(1); - expect(spy).toHaveBeenLastCalledWith([2, 1], [1, undefined]); - vm.a = 3; - vm.b = 3; - }) - .then(() => { - expect(spy.mock.calls.length).toBe(2); - expect(spy).toHaveBeenLastCalledWith([3, 3], [2, 1]); - }) - .then(done); - }); - - it('basic usage(multiple sources, lazy=false, flush=sync)', done => { - const vm = new Vue({ - setup() { - const a = value(1); - const b = value(1); - watch([a, b], spy, { lazy: false, flush: 'sync' }); - - return { - a, - b, - }; - }, - }); - expect(spy.mock.calls.length).toBe(1); - expect(spy).toHaveBeenLastCalledWith([1, 1], [undefined, undefined]); - vm.a = 2; - expect(spy.mock.calls.length).toBe(1); - waitForUpdate(() => { - expect(spy.mock.calls.length).toBe(2); - expect(spy).toHaveBeenLastCalledWith([2, 1], [1, undefined]); - vm.a = 3; - vm.b = 3; - }) - .then(() => { - expect(spy.mock.calls.length).toBe(3); - expect(spy).toHaveBeenLastCalledWith([3, 3], [2, 1]); - }) - .then(done); - }); - - it('basic usage(multiple sources, lazy=true, flush=sync)', done => { - const vm = new Vue({ - setup() { - const a = value(1); - const b = value(1); - watch([a, b], spy, { lazy: true, flush: 'sync' }); - - return { - a, - b, - }; - }, - }); - vm.a = 2; - expect(spy).not.toHaveBeenCalled(); - waitForUpdate(() => { - expect(spy.mock.calls.length).toBe(1); - expect(spy).toHaveBeenLastCalledWith([2, 1], [1, undefined]); - vm.a = 3; - vm.b = 3; - }) - .then(() => { - expect(spy.mock.calls.length).toBe(2); - expect(spy).toHaveBeenLastCalledWith([3, 3], [2, 1]); - }) - .then(done); - }); - - it('out of setup', done => { - const obj = state({ a: 1 }); - watch(() => obj.a, spy); - expect(spy.mock.calls.length).toBe(1); - expect(spy).toHaveBeenLastCalledWith(1, undefined); - obj.a = 2; - waitForUpdate(() => { - expect(spy.mock.calls.length).toBe(2); - expect(spy).toHaveBeenCalledWith(2, 1); - }).then(done); - }); - - it('out of setup(multiple sources)', done => { - const obj1 = state({ a: 1 }); - const obj2 = state({ a: 2 }); - watch([() => obj1.a, () => obj2.a], spy); - expect(spy.mock.calls.length).toBe(1); - expect(spy).toHaveBeenLastCalledWith([1, 2], [undefined, undefined]); - obj1.a = 2; - obj2.a = 3; - waitForUpdate(() => { - expect(spy.mock.calls.length).toBe(2); - expect(spy).toHaveBeenLastCalledWith([2, 3], [1, 2]); - }).then(done); - }); - - it('multiple cbs (after option merge)', done => { - const spy1 = jest.fn(); - const a = value(1); - const Test = Vue.extend({ - setup() { - watch(a, spy1); - }, - }); - new Test({ - setup() { - watch(a, spy); - return { - a, - }; - }, - template: `
{{a}}
`, - }).$mount(); - a.value = 2; - waitForUpdate(() => { - expect(spy1).toHaveBeenCalledWith(2, 1); - expect(spy).toHaveBeenCalledWith(2, 1); - }).then(done); - }); - - it('with option: lazy', done => { - const vm = new Vue({ - setup() { - const a = value(1); - watch(a, spy, { lazy: true }); - - return { - a, - }; - }, - template: `
{{a}}
`, - }).$mount(); - expect(spy).not.toHaveBeenCalled(); - vm.a = 2; - waitForUpdate(() => { - expect(spy).toHaveBeenCalledWith(2, 1); - }).then(done); - }); - - it('with option: deep', done => { - const vm = new Vue({ - setup() { - const a = value({ b: 1 }); - watch(a, spy, { lazy: true, deep: true }); - - return { - a, - }; - }, - template: `
{{a}}
`, - }).$mount(); - const oldA = vm.a; - expect(spy).not.toHaveBeenCalled(); - vm.a.b = 2; - expect(spy).not.toHaveBeenCalled(); - waitForUpdate(() => { - expect(spy).toHaveBeenCalledWith(vm.a, vm.a); - vm.a = { b: 3 }; - }) - .then(() => { - expect(spy).toHaveBeenCalledWith(vm.a, oldA); - }) - .then(done); - }); - - it('should flush after render', done => { - const vm = new Vue({ - setup() { - const a = value(1); - watch( - a, - (newVal, oldVal) => { - spy(newVal, oldVal); - expect(vm.$el.textContent).toBe('2'); - }, - { lazy: true } - ); - return { - a, - }; - }, - render(h) { - return h('div', this.a); - }, - }).$mount(); - vm.a = 2; - waitForUpdate(() => { - expect(spy.mock.calls.length).toBe(1); - expect(spy).toHaveBeenCalledWith(2, 1); - }).then(done); - }); - - it('should flush before render', done => { - const vm = new Vue({ - setup() { - const a = value(1); - watch( - a, - (newVal, oldVal) => { - spy(newVal, oldVal); - expect(vm.$el.textContent).toBe('1'); - }, - { lazy: true, flush: 'pre' } - ); - return { - a, - }; - }, - render(h) { - return h('div', this.a); - }, - }).$mount(); - vm.a = 2; - waitForUpdate(() => { - expect(spy.mock.calls.length).toBe(1); - expect(spy).toHaveBeenCalledWith(2, 1); - }).then(done); - }); - - it('should flush synchronously', done => { - const vm = new Vue({ - setup() { - const a = value(1); - watch(a, spy, { lazy: true, flush: 'sync' }); - return { - a, - }; - }, - render(h) { - return h('div', this.a); - }, - }).$mount(); - expect(spy).not.toHaveBeenCalled(); - vm.a = 2; - expect(spy).toHaveBeenCalledWith(2, 1); - vm.a = 3; - expect(spy).toHaveBeenCalledWith(3, 2); - waitForUpdate(() => { - expect(spy.mock.calls.length).toBe(2); - }).then(done); - }); - - it('should support watching unicode paths', done => { - const vm = new Vue({ - setup() { - const a = value(1); - watch(a, spy, { lazy: true }); - - return { - 数据: a, - }; - }, - render(h) { - return h('div', this['数据']); - }, - }).$mount(); - expect(spy).not.toHaveBeenCalled(); - vm['数据'] = 2; - expect(spy).not.toHaveBeenCalled(); - waitForUpdate(() => { - expect(spy).toHaveBeenCalledWith(2, 1); - }).then(done); - }); -}); diff --git a/test/globals.d.ts b/test/globals.d.ts new file mode 100644 index 00000000..640a71c6 --- /dev/null +++ b/test/globals.d.ts @@ -0,0 +1,5 @@ +declare function waitForUpdate(cb: Function): Promise + +declare interface Window { + waitForUpdate(cb: Function): Promise +} diff --git a/test/helpers/create-local-vue.ts b/test/helpers/create-local-vue.ts new file mode 100644 index 00000000..12a8ce3a --- /dev/null +++ b/test/helpers/create-local-vue.ts @@ -0,0 +1,33 @@ +import Vue, { VueConstructor } from 'vue' + +// based on https://github.com/vuejs/vue-test-utils/blob/dev/packages/test-utils/src/create-local-vue.js + +export function createLocalVue(_Vue: VueConstructor = Vue) { + const instance = _Vue.extend() + + Object.keys(_Vue).forEach((key) => { + // @ts-ignore + instance[key] = _Vue[key] + }) + + // @ts-ignore + if (instance._installedPlugins && instance._installedPlugins.length) { + // @ts-ignore + instance._installedPlugins.length = 0 + } + + instance.config = _Vue.config + + const use = instance.use + //@ts-ignore + instance.use = (plugin, ...rest) => { + if (plugin.installed === true) { + plugin.installed = false + } + if (plugin.install && plugin.install.installed === true) { + plugin.install.installed = false + } + use.call(instance, plugin, ...rest) + } + return instance +} diff --git a/test/helpers/index.ts b/test/helpers/index.ts new file mode 100644 index 00000000..32ab1e93 --- /dev/null +++ b/test/helpers/index.ts @@ -0,0 +1,2 @@ +export * from './mockWarn' +export * from './utils' diff --git a/test/helpers/mockWarn.ts b/test/helpers/mockWarn.ts new file mode 100644 index 00000000..c9be10d0 --- /dev/null +++ b/test/helpers/mockWarn.ts @@ -0,0 +1,107 @@ +declare global { + namespace Vi { + interface JestAssertion { + toHaveBeenWarned(): T + toHaveBeenWarnedLast(): T + toHaveBeenWarnedTimes(n: number): T + } + } +} + +import type { SpyInstance } from 'vitest' + +export const mockError = () => mockWarn(true) + +export function mockWarn(asError = false) { + expect.extend({ + toHaveBeenWarned(received: string) { + asserted.add(received) + const passed = warn.mock.calls.some( + (args) => args[0].toString().indexOf(received) > -1 + ) + if (passed) { + return { + pass: true, + message: () => `expected "${received}" not to have been warned.`, + } + } else { + const msgs = warn.mock.calls.map((args) => args[0]).join('\n - ') + return { + pass: false, + message: () => + `expected "${received}" to have been warned.\n\nActual messages:\n\n - ${msgs}`, + } + } + }, + + toHaveBeenWarnedLast(received: string) { + asserted.add(received) + const passed = + warn.mock.calls[warn.mock.calls.length - 1][0].indexOf(received) > -1 + if (passed) { + return { + pass: true, + message: () => `expected "${received}" not to have been warned last.`, + } + } else { + const msgs = warn.mock.calls.map((args) => args[0]).join('\n - ') + return { + pass: false, + message: () => + `expected "${received}" to have been warned last.\n\nActual messages:\n\n - ${msgs}`, + } + } + }, + + toHaveBeenWarnedTimes(received: string, n: number) { + asserted.add(received) + let found = 0 + warn.mock.calls.forEach((args) => { + if (args[0].indexOf(received) > -1) { + found++ + } + }) + + if (found === n) { + return { + pass: true, + message: () => + `expected "${received}" to have been warned ${n} times.`, + } + } else { + return { + pass: false, + message: () => + `expected "${received}" to have been warned ${n} times but got ${found}.`, + } + } + }, + }) + + let warn: SpyInstance + const asserted: Set = new Set() + + beforeEach(() => { + asserted.clear() + warn = vi.spyOn(console, asError ? 'error' : 'warn') + warn.mockImplementation(() => {}) + }) + + afterEach(() => { + const assertedArray = Array.from(asserted) + const nonAssertedWarnings = warn.mock.calls + .map((args) => args[0]) + .filter((received) => { + return !assertedArray.some((assertedMsg) => { + return received.toString().indexOf(assertedMsg) > -1 + }) + }) + warn.mockRestore() + if (nonAssertedWarnings.length) { + nonAssertedWarnings.forEach((warning) => { + console.warn(warning) + }) + throw new Error(`test case threw unexpected warnings.`) + } + }) +} diff --git a/test/helpers/utils.ts b/test/helpers/utils.ts new file mode 100644 index 00000000..ba4060d6 --- /dev/null +++ b/test/helpers/utils.ts @@ -0,0 +1,9 @@ +const Vue = require('vue/dist/vue.common.js') + +export function nextTick(): Promise { + return Vue.nextTick() +} + +export function sleep(ms = 100) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} diff --git a/test/helpers/wait-for-update.js b/test/helpers/wait-for-update.ts similarity index 51% rename from test/helpers/wait-for-update.js rename to test/helpers/wait-for-update.ts index cecdca63..c125757d 100644 --- a/test/helpers/wait-for-update.js +++ b/test/helpers/wait-for-update.ts @@ -1,4 +1,4 @@ -const Vue = require('vue'); +import Vue from 'vue' // helper for async assertions. // Use like this: @@ -12,62 +12,63 @@ const Vue = require('vue'); // // more assertions... // }) // .then(done) -window.waitForUpdate = initialCb => { - let end; - const queue = initialCb ? [initialCb] : []; + +export const waitForUpdate = (initialCb) => { + let end + const queue = initialCb ? [initialCb] : [] function shift() { - const job = queue.shift(); + const job = queue.shift() if (queue.length) { - let hasError = false; + let hasError = false try { - job.wait ? job(shift) : job(); + job.wait ? job(shift) : job() } catch (e) { - hasError = true; - const done = queue[queue.length - 1]; + hasError = true + const done = queue[queue.length - 1] if (done && done.fail) { - done.fail(e); + done.fail(e) } } if (!hasError && !job.wait) { if (queue.length) { - Vue.nextTick(shift); + Vue.nextTick(shift) } } } else if (job && (job.fail || job === end)) { - job(); // done + job() // done } } Vue.nextTick(() => { if (!queue.length || (!end && !queue[queue.length - 1].fail)) { - throw new Error('waitForUpdate chain is missing .then(done)'); + throw new Error('waitForUpdate chain is missing .then(done)') } - shift(); - }); + shift() + }) const chainer = { - then: nextCb => { - queue.push(nextCb); - return chainer; + then: (nextCb) => { + queue.push(nextCb) + return chainer }, - thenWaitFor: wait => { + thenWaitFor: (wait) => { if (typeof wait === 'number') { - wait = timeout(wait); + wait = timeout(wait) } - wait.wait = true; - queue.push(wait); - return chainer; + wait.wait = true + queue.push(wait) + return chainer }, - end: endFn => { - queue.push(endFn); - end = endFn; + end: (endFn) => { + queue.push(endFn) + end = endFn }, - }; + } - return chainer; -}; + return chainer +} function timeout(n) { - return next => setTimeout(next, n); + return (next) => setTimeout(next, n) } diff --git a/test/misc.spec.ts b/test/misc.spec.ts new file mode 100644 index 00000000..9d205145 --- /dev/null +++ b/test/misc.spec.ts @@ -0,0 +1,79 @@ +import Vue from './vue' +import { ref, nextTick, isReactive } from '../src' + +describe('nextTick', () => { + it('should works with callbacks', () => { + const vm = new Vue<{ a: number }>({ + template: `
{{a}}
`, + setup() { + return { + a: ref(1), + } + }, + }).$mount() + + expect(vm.$el.textContent).toBe('1') + vm.a = 2 + expect(vm.$el.textContent).toBe('1') + + nextTick(() => { + expect(vm.$el.textContent).toBe('2') + vm.a = 3 + expect(vm.$el.textContent).toBe('2') + + nextTick(() => { + expect(vm.$el.textContent).toBe('3') + }) + }) + }) + + it('should works with await', async () => { + const vm = new Vue<{ a: number }>({ + template: `
{{a}}
`, + setup() { + return { + a: ref(1), + } + }, + }).$mount() + + expect(vm.$el.textContent).toBe('1') + vm.a = 2 + expect(vm.$el.textContent).toBe('1') + + await nextTick() + expect(vm.$el.textContent).toBe('2') + vm.a = 3 + expect(vm.$el.textContent).toBe('2') + + await nextTick() + expect(vm.$el.textContent).toBe('3') + }) +}) + +describe('observable', () => { + it('observable should be reactive', () => { + const o: Record = Vue.observable({ + a: 1, + b: [{ a: 1 }], + }) + + expect(isReactive(o)).toBe(true) + + expect(isReactive(o.b)).toBe(true) + expect(isReactive(o.b[0])).toBe(true) + + // TODO new array items should be reactive + // o.b.push({ a: 2 }) + // expect(isReactive(o.b[1])).toBe(true) + }) + + it('nested deps should keep __ob__', () => { + const o: any = Vue.observable({ + a: { b: 1 }, + }) + + expect(o.__ob__).not.toBeUndefined() + expect(o.a.__ob__).not.toBeUndefined() + }) +}) diff --git a/test/setup.spec.js b/test/setup.spec.js index 81fb9578..8d5e75e2 100644 --- a/test/setup.spec.js +++ b/test/setup.spec.js @@ -1,157 +1,163 @@ -const Vue = require('vue/dist/vue.common.js'); -const { plugin, value, computed } = require('../src'); - -Vue.use(plugin); +import Vue from 'vue/dist/vue.common.js' +import { + ref, + computed, + h, + provide, + inject, + reactive, + toRefs, + markRaw, + toRaw, + nextTick, + isReactive, + watchEffect, + defineComponent, + onMounted, + set, + del, +} from '../src' +import { sleep } from './helpers/utils' describe('setup', () => { + let warn = null + beforeEach(() => { - warn = jest.spyOn(global.console, 'error').mockImplementation(() => null); - }); + warn = vi.spyOn(global.console, 'error').mockImplementation(() => null) + }) afterEach(() => { - warn.mockRestore(); - }); + warn.mockRestore() + }) - it('should be called before `methods` gets resolved(no methods option)', () => { + it('should works', () => { const vm = new Vue({ setup() { return { - a: value(1), - }; - }, - data() { - return { - b: this.a, - }; + a: ref(1), + } }, - }).$mount(); - expect(vm.a).toBe(1); - expect(vm.b).toBe(1); - }); + }).$mount() + expect(vm.a).toBe(1) + }) - it('should be called before `methods` gets resolved(empty methods option)', () => { + it('should work with non reactive null', () => { const vm = new Vue({ setup() { return { - a: value(1), - }; + a: null, + } }, - data() { + }).$mount() + expect(vm.a).toBe(null) + }) + + it('should work with non reactive undefined', () => { + const vm = new Vue({ + setup() { return { - b: this.a, - }; + a: undefined, + b: 'foobar', + } }, - methods: {}, - }).$mount(); - expect(vm.a).toBe(1); - expect(vm.b).toBe(1); - }); + }).$mount() + expect(vm.a).toBe(undefined) + expect(vm.b).toBe('foobar') + }) - it('should be called before `methods` gets resolved(multiple methods)', () => { + it('should be overridden by data option of plain object', () => { const vm = new Vue({ setup() { return { - a: value(0), - }; + a: ref(1), + } }, - created() { - this.m1(); - this.m2(); - this.m3(); + data: { + a: 2, }, - methods: { - m1() { - this.a++; - }, - m2() { - this.a++; - }, - m3() { - this.a++; - }, - }, - }).$mount(); - expect(vm.a).toBe(3); - }); + }).$mount() + expect(vm.a).toBe(2) + }) - it('should work with `methods` and `data` options', done => { - let calls = 0; + it("should access setup's value in data", () => { const vm = new Vue({ - template: `
{{a}}{{b}}{{c}}
`, setup() { return { - a: value(1), - }; - }, - beforeUpdate() { - calls++; - }, - created() { - this.m(); + a: ref(1), + } }, data() { return { b: this.a, - c: 0, - }; + } }, - methods: { - m() { - this.c = this.a; - }, - }, - }).$mount(); - expect(vm.a).toBe(1); - expect(vm.b).toBe(1); - expect(vm.c).toBe(1); - vm.a = 2; - waitForUpdate(() => { - expect(calls).toBe(1); - expect(vm.a).toBe(2); - expect(vm.b).toBe(1); - expect(vm.c).toBe(1); - vm.b = 2; - }) - .then(() => { - expect(calls).toBe(2); - expect(vm.a).toBe(2); - expect(vm.b).toBe(2); - expect(vm.c).toBe(1); + }).$mount() + expect(vm.a).toBe(1) + expect(vm.b).toBe(1) + }) + + it('should work with `methods` and `data` options', () => + new Promise((done, reject) => { + done.fail = reject + + let calls = 0 + const vm = new Vue({ + template: `
{{a}}{{b}}{{c}}
`, + setup() { + return { + a: ref(1), + } + }, + beforeUpdate() { + calls++ + }, + created() { + this.m() + }, + data() { + return { + b: this.a, + c: 0, + } + }, + methods: { + m() { + this.c = this.a + }, + }, + }).$mount() + expect(vm.a).toBe(1) + expect(vm.b).toBe(1) + expect(vm.c).toBe(1) + vm.a = 2 + waitForUpdate(() => { + expect(calls).toBe(1) + expect(vm.a).toBe(2) + expect(vm.b).toBe(1) + expect(vm.c).toBe(1) + vm.b = 2 }) - .then(done); - }); + .then(() => { + expect(calls).toBe(2) + expect(vm.a).toBe(2) + expect(vm.b).toBe(2) + expect(vm.c).toBe(1) + }) + .then(done) + })) - it('should reveive props as first params', () => { - let props; + it('should receive props as first params', () => { + let props new Vue({ props: ['a'], setup(_props) { - props = _props; + props = _props }, propsData: { a: 1, }, - }).$mount(); - expect(props.a).toBe(1); - }); - - it('should reveive context second params', done => { - new Vue({ - setup(_, ctx) { - expect(ctx).toBeDefined(); - expect('parent' in ctx).toBe(true); - expect(ctx).toEqual( - expect.objectContaining({ - root: expect.any(Object), - refs: expect.any(Object), - slots: expect.any(Object), - attrs: expect.any(Object), - emit: expect.any(Function), - }) - ); - done(); - }, - }); - }); + }).$mount() + expect(props.a).toBe(1) + }) it('warn for existing props', () => { new Vue({ @@ -159,55 +165,113 @@ describe('setup', () => { a: {}, }, setup() { - const a = value(); + const a = ref() return { a, - }; + } }, - }); + }) expect(warn.mock.calls[0][0]).toMatch( '[Vue warn]: The setup binding property "a" is already declared as a prop.' - ); - }); + ) + }) it('warn for existing instance properties', () => { new Vue({ setup(_, { _vm }) { - _vm.a = 1; + _vm.a = 1 return { - a: value(), - }; + a: ref(), + } }, - }); + }) expect(warn.mock.calls[0][0]).toMatch( '[Vue warn]: The setup binding property "a" is already declared.' - ); - }); + ) + }) + + // `props` are not deeply reactive + it('not warn doing toRef on props', async () => { + const Foo = { + props: { + obj: { + type: Object, + required: true, + }, + }, + setup(props) { + return () => + h('div', null, [ + h('span', toRefs(props.obj).bar.value), + h('span', toRefs(props.obj.nested).baz.value), + ]) + }, + } + + let bar + let baz + + const vm = new Vue({ + template: `
`, + components: { Foo }, + setup() { + bar = ref(3) + baz = ref(1) + return { + obj: { + bar, + nested: { + baz, + }, + }, + } + }, + }) + vm.$mount() + + expect(vm.$el.textContent).toBe('31') + + bar.value = 4 + baz.value = 2 + + await vm.$nextTick() + expect(vm.$el.textContent).toBe('42') + + expect(warn).toHaveBeenCalledTimes(4) // 2 renders - 2 calls each render + }) it('should merge result properly', () => { + const injectKey = Symbol('foo') const A = Vue.extend({ setup() { - return { a: 1 }; + provide(injectKey, 'foo') + return { a: 1 } }, - }); + }) const Test = Vue.extend({ extends: A, - setup() {}, - }); + setup() { + const injectVal = inject(injectKey) + return { + injectVal, + } + }, + }) let vm = new Test({ setup() { - return { b: 2 }; + return { b: 2 } }, - }); - expect(vm.a).toBe(1); - expect(vm.b).toBe(2); + }) + expect(vm.a).toBe(1) + expect(vm.b).toBe(2) + expect(vm.injectVal).toBe('foo') // no instance data - vm = new Test(); - expect(vm.a).toBe(1); + vm = new Test() + expect(vm.a).toBe(1) // no child-val - const Extended = Test.extend({}); - vm = new Extended(); - expect(vm.a).toBe(1); + const Extended = Test.extend({}) + vm = new Extended() + expect(vm.a).toBe(1) // recursively merge objects const WithObject = Vue.extend({ setup() { @@ -215,21 +279,21 @@ describe('setup', () => { obj: { a: 1, }, - }; + } }, - }); + }) vm = new WithObject({ setup() { return { obj: { b: 2, }, - }; + } }, - }); - expect(vm.obj.a).toBe(1); - expect(vm.obj.b).toBe(2); - }); + }) + expect(vm.obj.a).toBe(1) + expect(vm.obj.b).toBe(2) + }) it('should have access to props', () => { const Test = { @@ -238,95 +302,1097 @@ describe('setup', () => { setup(props) { return { b: props.a, - }; + } }, - }; + } const vm = new Vue({ template: ``, components: { Test }, - }).$mount(); - expect(vm.$refs.test.b).toBe(1); - }); + }).$mount() + expect(vm.$refs.test.b).toBe(1) + }) + + it('props should be reactive', () => + new Promise((done, reject) => { + done.fail = reject + + let calls = 0 + let _props + const vm = new Vue({ + template: ``, + setup() { + return { msg: ref('hello') } + }, + beforeUpdate() { + calls++ + }, + components: { + child: { + template: `{{ localMsg }}`, + props: ['msg'], + setup(props) { + _props = props + + return { + localMsg: props.msg, + computedMsg: computed(() => props.msg + ' world'), + } + }, + }, + }, + }).$mount() + + expect(isReactive(_props)).toBe(true) + + const child = vm.$children[0] + expect(child.localMsg).toBe('hello') + expect(child.computedMsg).toBe('hello world') + expect(calls).toBe(0) + vm.msg = 'hi' + waitForUpdate(() => { + expect(child.localMsg).toBe('hello') + expect(child.computedMsg).toBe('hi world') + expect(calls).toBe(1) + }).then(done) + })) + + it('toRefs(props) should not warn', async () => { + let a + + const child = { + template: `
`, + + props: { + r: Number, + }, + setup(props) { + a = toRefs(props).r + }, + } - it('props should not be reactive', done => { - let calls = 0; const vm = new Vue({ - template: ``, + template: ``, + components: { + child, + }, + + data() { + return { + r: 1, + } + }, + }).$mount() + + expect(a.value).toBe(1) + vm.r = 3 + + await Vue.nextTick() + + expect(a.value).toBe(3) + + expect(warn).not.toHaveBeenCalled() + }) + + it('Should allow to return Object.freeze', () => { + const vm = new Vue({ + template: `
{{foo.bar}}
`, setup() { - return { msg: value('hello') }; + const foo = Object.freeze({ bar: 'baz' }) + return { + foo, + } }, - beforeUpdate() { - calls++; + }).$mount() + expect(vm.$el.textContent).toBe('baz') + }) + + it('this should be undefined', () => { + const vm = new Vue({ + template: '
', + setup() { + expect(this).toBe(global) }, + }).$mount() + }) + + it('should not make returned non-reactive object reactive', () => + new Promise((done, reject) => { + done.fail = reject + + const vm = new Vue({ + setup() { + return { + form: { + a: 1, + b: 2, + }, + } + }, + template: '
{{ form.a }}, {{ form.b }}
', + }).$mount() + expect(vm.$el.textContent).toBe('1, 2') + + // should not trigger a re-render + vm.form.a = 2 + waitForUpdate(() => { + expect(vm.$el.textContent).toBe('1, 2') + + // not trigger event + vm.form = { a: 2, b: 3 } + }) + .then(() => { + expect(vm.$el.textContent).toBe('1, 2') + }) + .then(done) + })) + + it("should put a unenumerable '__ob__' for non-reactive object", () => { + const clone = (obj) => JSON.parse(JSON.stringify(obj)) + const componentSetup = vi.fn((props) => { + const internalOptions = clone(props.options) + return { internalOptions } + }) + const ExternalComponent = { + props: ['options'], + setup: componentSetup, + } + new Vue({ + components: { ExternalComponent }, + setup: () => ({ options: {} }), + template: ``, + }).$mount() + expect(componentSetup).toReturn() + }) + + it('current vue should exist in nested setup call', () => { + const spy = vi.fn() + new Vue({ + setup() { + new Vue({ + setup() { + spy(1) + }, + }) + + spy(2) + }, + }) + expect(spy.mock.calls.length).toBe(2) + expect(spy).toHaveBeenNthCalledWith(1, 1) + expect(spy).toHaveBeenNthCalledWith(2, 2) + }) + + it('inline render function should receive proper params', () => { + let p + const vm = new Vue({ + template: ``, components: { child: { - template: `{{ localMsg }}`, + name: 'child', props: ['msg'], - setup(props) { - return { localMsg: props.msg, computedMsg: computed(() => props.msg + ' world') }; + setup() { + return (props) => { + p = props + return null + } }, }, }, - }).$mount(); - const child = vm.$children[0]; - expect(child.localMsg).toBe('hello'); - expect(child.computedMsg).toBe('hello world'); - expect(calls).toBe(0); - vm.msg = 'hi'; - waitForUpdate(() => { - expect(child.localMsg).toBe('hello'); - expect(child.computedMsg).toBe('hi world'); - expect(calls).toBe(1); - }).then(done); - }); + }).$mount() + expect(p).toBe(undefined) + }) + + it('inline render function should work', () => + new Promise((done, reject) => { + done.fail = reject + + // let createElement; + const vm = new Vue({ + props: ['msg'], + template: '
1
', + setup(props) { + const count = ref(0) + const increment = () => { + count.value++ + } + + return () => + h('div', [ + h('span', props.msg), + h( + 'button', + { + on: { + click: increment, + }, + }, + count.value + ), + ]) + }, + propsData: { + msg: 'foo', + }, + }).$mount() + expect(vm.$el.querySelector('span').textContent).toBe('foo') + expect(vm.$el.querySelector('button').textContent).toBe('0') + vm.$el.querySelector('button').click() + waitForUpdate(() => { + expect(vm.$el.querySelector('button').textContent).toBe('1') + vm.msg = 'bar' + }) + .then(() => { + expect(vm.$el.querySelector('span').textContent).toBe('bar') + }) + .then(done) + })) + + describe('setup unwrap', () => { + test('ref', () => { + const vm = new Vue({ + setup() { + const r = ref('r') + + const refList = ref([ref('1'), ref('2'), ref('3')]) + const list = [ref('a'), ref('b')] + + return { + r, + refList, + list, + } + }, + template: `
+

{{r}}

+

{{list}}

+

{{refList}}

+
`, + }).$mount() + + expect(vm.$el.querySelector('#r').textContent).toBe('r') + + // shouldn't unwrap arrays + expect( + JSON.parse(vm.$el.querySelector('#list').textContent) + ).toMatchObject([{ value: 'a' }, { value: 'b' }]) + expect( + JSON.parse(vm.$el.querySelector('#refList').textContent) + ).toMatchObject([{ value: '1' }, { value: '2' }, { value: '3' }]) + + expect(warn).not.toHaveBeenCalled() + }) + + test('nested', () => { + const vm = new Vue({ + setup() { + const nested = { + a: ref('a'), + aa: { + b: ref('aa'), + bb: { + cc: ref('aa'), + c: 'aa', + }, + }, + + aaa: reactive({ + b: ref('aaa'), + bb: { + c: ref('aaa'), + cc: 'aaa', + }, + }), + + aaaa: { + b: [1], + bb: ref([1]), + bbb: reactive({ + c: [1], + cc: ref([1]), + }), + bbbb: [ref(1)], + }, + } + + return { + nested, + } + }, + template: `
+

{{nested.a}}

+ +

{{ nested.aa.b }}

+

{{ nested.aa.bb.c }}

+

{{ nested.aa.bb.cc }}

+ +

{{ nested.aaa.b }}

+

{{ nested.aaa.bb.c }}

+

{{ nested.aaa.bb.cc }}

+ +

{{ nested.aaaa.b }}

+

{{ nested.aaaa.bb }}

+

{{ nested.aaaa.bbb.c }}

+

{{ nested.aaaa.bbb.cc }}

+

{{ nested.aaaa.bbbb }}

+
`, + }).$mount() + + expect( + JSON.parse(vm.$el.querySelector('#nested').textContent) + ).toMatchObject({ + value: 'a', + }) + + expect( + JSON.parse(vm.$el.querySelector('#nested_aa_b').textContent) + ).toMatchObject({ + value: 'aa', + }) + expect(vm.$el.querySelector('#nested_aa_bb_c').textContent).toBe('aa') + expect( + JSON.parse(vm.$el.querySelector('#nested_aa_bb_cc').textContent) + ).toMatchObject({ value: 'aa' }) + + expect(vm.$el.querySelector('#nested_aaa_b').textContent).toBe('aaa') + expect(vm.$el.querySelector('#nested_aaa_bb_c').textContent).toBe('aaa') + expect(vm.$el.querySelector('#nested_aaa_bb_cc').textContent).toBe('aaa') + + expect(warn).not.toHaveBeenCalled() + }) + + it('recursive', () => { + const vm = new Vue({ + setup() { + const b = { + c: 'c', + } + + const recursive = { + a: { + a: 'a', + b, + }, + } + + b.recursive = recursive + b.r = ref('r') + + return { + recursive, + } + }, + template: `
+

{{recursive.a.a}}

+

{{recursive.a.b.c}}

+

{{recursive.a.b.r}}

+ +

{{recursive.a.b.recursive.a.a}}

+

{{recursive.a.b.recursive.a.b.c}}

+

{{recursive.a.b.recursive.a.b.r}}

+ +

{{recursive.a.b.recursive.a.b.recursive.a.b.c}}

+

{{recursive.a.b.recursive.a.b.recursive.a.b.r}}

+
`, + }).$mount() + expect(vm.$el.querySelector('#recursive_a').textContent).toBe('a') + expect(vm.$el.querySelector('#recursive_b_c').textContent).toBe('c') + expect( + JSON.parse(vm.$el.querySelector('#recursive_b_r').textContent) + ).toMatchObject({ value: 'r' }) + + expect(vm.$el.querySelector('#recursive_b_recursive_a').textContent).toBe( + 'a' + ) + expect(vm.$el.querySelector('#recursive_b_recursive_c').textContent).toBe( + 'c' + ) + expect( + JSON.parse(vm.$el.querySelector('#recursive_b_recursive_r').textContent) + ).toMatchObject({ value: 'r' }) + + expect( + vm.$el.querySelector('#recursive_b_recursive_recursive_c').textContent + ).toBe('c') + + expect( + JSON.parse( + vm.$el.querySelector('#recursive_b_recursive_recursive_r').textContent + ) + ).toMatchObject({ value: 'r' }) + + expect(warn).not.toHaveBeenCalled() + }) + + // #384 + it('not unwrap when is raw', () => { + const vm = new Vue({ + setup() { + const xx = { + ref: ref('r'), + } + const r = markRaw(xx) + return { + r, + } + }, + template: `
+

{{r}}

+
`, + }).$mount() + + expect(JSON.parse(vm.$el.querySelector('#r').textContent)).toMatchObject({ + ref: { + value: 'r', + }, + }) + + expect(warn).not.toHaveBeenCalled() + }) + + // #392 + it('should copy __ob__ and make toRaw work when passing via props', () => { + let propsObj = null + + const Foo = { + template: '

{{obj.bar}}

', + props: { + obj: { + type: Object, + required: true, + }, + }, + setup(props) { + propsObj = toRaw(props.obj) + return {} + }, + } + + const vm = new Vue({ + template: '', + components: { Foo }, + setup() { + return { obj: { bar: ref(1) } } + }, + }).$mount() + + expect(JSON.parse(vm.$el.textContent)).toMatchObject({ value: 1 }) + expect(propsObj).toMatchObject({ bar: { value: 1 } }) + + expect(warn).not.toHaveBeenCalled() + }) + }) + + it('should not unwrap built-in objects on the template', () => { + const date = new Date('2020-01-01') + const regex = /a(b).*/ + const dateString = date.toString() + const regexString = regex.toString() + const mathString = Math.toString() - it('this should be undefined', () => { const vm = new Vue({ - template: '
', setup() { - expect(this).toBe(global); + return { + raw_date: date, + nested_date: { + a: date, + b: date, + }, + raw_regex: regex, + nested_regex: { + a: regex, + b: regex, + }, + math: Math, + } }, - }).$mount(); - }); + template: `
+

{{raw_date}}

+

{{nested_date}}

+

{{raw_regex}}

+

{{nested_regex.a}}

+

{{nested_regex.b}}

+

{{math}}

+
`, + }).$mount() + + expect(vm.$el.querySelector('#raw_date').textContent).toBe(dateString) + expect( + JSON.parse(vm.$el.querySelector('#nested_date').textContent) + ).toMatchObject( + JSON.parse( + JSON.stringify({ + a: date, + b: date, + }) + ) + ) + expect(vm.$el.querySelector('#raw_regex').textContent).toBe(regexString) + expect(vm.$el.querySelector('#nested_regex_a').textContent).toBe( + regexString + ) + expect(vm.$el.querySelector('#nested_regex_b').textContent).toBe( + regexString + ) + expect(vm.$el.querySelector('#math').textContent).toBe(mathString) + }) + + describe('Methods', () => { + it('binds methods when calling with parenthesis', async () => { + let context = null + const contextFunction = vi.fn(function () { + context = this + }) + + const vm = new Vue({ + template: '
', + setup() { + return { + contextFunction, + } + }, + }).$mount() + + await vm.$el.querySelector('button').click() + expect(contextFunction).toBeCalled() + expect(context).toBe(vm) + }) + + it('binds methods when calling without parenthesis', async () => { + let context = null + const contextFunction = vi.fn(function () { + context = this + }) + + const vm = new Vue({ + template: '
', + setup() { + return { + contextFunction, + } + }, + }).$mount() + + await vm.$el.querySelector('button').click() + expect(contextFunction).toBeCalled() + expect(context).toBe(vm) + }) + }) - it('should make returned plain value reactive (value)', done => { + it('should work after extending with an undefined setup', () => { + const opts = { + setup() { + return () => h('div', 'Composition-api') + }, + } + const Constructor = Vue.extend(opts).extend({}) + + const vm = new Vue(Constructor).$mount() + expect(vm.$el.textContent).toBe('Composition-api') + }) + + // #487 + it('should handle updates for directly return a reactive object.', async () => { + const opts = { + template: '
{{ count }}
', + setup() { + const state = reactive({ count: 1 }) + + setTimeout(() => { + state.count = 2 + }, 1) + + return state + }, + } + const Constructor = Vue.extend(opts).extend({}) + + const vm = new Vue(Constructor).$mount() + expect(vm.$el.textContent).toBe('1') + await sleep(10) + await nextTick() + expect(vm.$el.textContent).toBe('2') + }) + + // #679 + it('should work merge with object in development', async () => { + global.__DEV__ = true const vm = new Vue({ + template: '
{{ data.id }}
', setup() { + const data = reactive({ + id: 42, + }) + return { data } + }, + data() { return { - name: null, - nested: { - object: { - msg: 'foo', - }, - }, - }; - }, - template: '
{{ name }}, {{ nested.object.msg }}
', - }).$mount(); - expect(vm.$el.textContent).toBe(', foo'); - vm.name = 'foo'; - vm.nested.object.msg = 'bar'; - waitForUpdate(() => { - expect(vm.$el.textContent).toBe('foo, bar'); - }).then(done); - }); - - it('should make returned plain value reactive (object)', done => { + data: { id: 1 }, + } + }, + }).$mount() + + await nextTick() + expect(vm.$el.textContent).toBe('1') + }) + + // #679 + it('should work merge with object in production', async () => { + global.__DEV__ = false const vm = new Vue({ + template: '
{{ data.id }}
', setup() { + const data = reactive({ + id: 42, + }) + return { data } + }, + data() { return { - form: { - a: 1, - b: 2, + data: { id: 1 }, + } + }, + }).$mount() + + await nextTick() + expect(vm.$el.textContent).toBe('1') + }) + + // #679 html text change + it('should id not change when msg changed in development', async () => { + global.__DEV__ = true + const vm = new Vue({ + template: '
{{ id }} {{ msg }}
', + setup() { + return { id: 42 } + }, + data() { + return { + id: 1, + msg: 'abc', + } + }, + methods: { + change() { + this.msg = this.msg + this.id + }, + }, + }).$mount() + + await nextTick() + expect(vm.$el.textContent).toBe('1 abc') + await vm.$el.querySelector('button').click() + await nextTick() + expect(vm.$el.textContent).toBe('1 abc1') + }) + + // #683 #603 #580 + it('should update directly when adding attributes to a reactive object', async () => { + const vm = new Vue({ + template: '
', + setup() { + const obj = reactive({}) + const add = () => { + set(obj, 'a', 'new property') + } + return { obj, add } + }, + }).$mount() + + expect(vm.$el.textContent).toBe('') + await vm.$el.querySelector('button').click() + expect(vm.$el.textContent).toBe('new property') + }) + + // #683 #603 #580 + it('should update directly when deleting attributes from a reactive object', async () => { + const vm = new Vue({ + template: '
', + setup() { + const obj = reactive({ a: 'hello' }) + const deleting = () => { + del(obj, 'a') + } + return { obj, deleting } + }, + }).$mount() + + expect(vm.$el.textContent).toBe('hello') + await vm.$el.querySelector('button').click() + expect(vm.$el.textContent).toBe('') + }) + + // #524 + it('should work with reactive arrays.', async () => { + const opts = { + template: `
{{items.length}}
`, + setup() { + const items = reactive([]) + + setTimeout(() => { + items.push(2) + }, 1) + + return { + items, + } + }, + } + const Constructor = Vue.extend(opts).extend({}) + + const vm = new Vue(Constructor).$mount() + expect(vm.$el.textContent).toBe('0') + await sleep(10) + await nextTick() + expect(vm.$el.textContent).toBe('1') + }) + + it('should work with reactive array nested', async () => { + const opts = { + template: `
{{a.items.length}}
`, + setup() { + const items = reactive([]) + + setTimeout(() => { + items.push(2) + }, 1) + + return { + a: { + items, + }, + } + }, + } + const Constructor = Vue.extend(opts).extend({}) + + const vm = new Vue(Constructor).$mount() + expect(vm.$el.textContent).toBe('0') + await sleep(10) + await nextTick() + expect(vm.$el.textContent).toBe('1') + }) + + it('should not unwrap reactive array nested', async () => { + const opts = { + template: `
{{a.items}}
`, + setup() { + const items = reactive([]) + + setTimeout(() => { + items.push(ref(1)) + }, 1) + + return { + a: { + items, }, - }; - }, - template: '
{{ form.a }}, {{ form.b }}
', - }).$mount(); - expect(vm.$el.textContent).toBe('1, 2'); - vm.form = { a: 2, b: 3 }; - waitForUpdate(() => { - expect(vm.$el.textContent).toBe('2, 3'); - }).then(done); - }); -}); + } + }, + } + const Constructor = Vue.extend(opts).extend({}) + + const vm = new Vue(Constructor).$mount() + expect(vm.$el.textContent).toBe('[]') + await sleep(10) + await nextTick() + expect(JSON.parse(vm.$el.textContent)).toStrictEqual([{ value: 1 }]) + }) + + // TODO make this pass + // it('should work with computed', async ()=>{ + // const opts = { + // template: `
{{len}}
`, + // setup() { + // const array = reactive([]); + // const len = computed(()=> array.length); + + // setTimeout(() => { + // array.push(2) + // }, 1) + + // return { + // len + // } + // }, + // } + // const Constructor = Vue.extend(opts).extend({}) + + // const vm = new Vue(Constructor).$mount() + // expect(vm.$el.textContent).toBe('0') + // await sleep(10) + // await nextTick() + // expect(vm.$el.textContent).toBe('1') + // }) + + // #448 + it('should not cause infinite loop', async () => { + const A = defineComponent({ + template: `
`, + props: { + pattern: { + type: RegExp, + required: true, + }, + }, + + setup(props) { + return { + props, + } + }, + }) + const B = defineComponent({ + template: `
`, + setup(props, { emit }) { + onMounted(() => { + emit('ev', true) + }) + + return {} + }, + }) + + const vm = new Vue( + defineComponent({ + components: { + A, + B, + }, + + template: ` `, + setup(props, { emit }) { + const o = ref([false]) + + return { + o, + emit, + } + }, + }) + ).$mount() + + await vm.$nextTick() + + expect(warn).not.toBeCalled() + }) + + it('should work with mock objects', async () => { + const originalProxy = new Proxy( + {}, + { + get() { + return vi.fn() + }, + } + ) + + const opts = { + template: `
`, + setup() { + return { + proxy: originalProxy, + } + }, + } + const Constructor = Vue.extend(opts).extend({}) + + const vm = new Vue(Constructor).$mount() + expect(vm.proxy).toBe(originalProxy) + }) + + // test #687 + it('properties of function should not disappear', () => { + Vue.component('todo', { + template: '
', + props: ['testFn'], + setup(props) { + expect(props.testFn.a).toBe(2) + }, + }) + + const vm = new Vue({ + template: ` +
+ +
+ `, + setup() { + const testFn = () => { + console.log(1) + } + testFn.a = 2 + return { testFn } + }, + }).$mount() + }) + + // #794 + it('should not trigger getter w/ object computed nested', () => { + const spy = vi.fn() + new Vue({ + setup() { + new Vue({ + setup() { + const person = { + name: computed(() => { + spy() + return 1 + }), + } + return { + person, + } + }, + }) + }, + }) + expect(spy).toHaveBeenCalledTimes(0) + }) + + // #833 + it('attrs update not correctly mapped to props', async () => { + let propsFromAttrs = null + const Field = defineComponent({ + props: ['firstName', 'lastName'], + setup(props, { attrs }) { + watchEffect(() => { + propsFromAttrs = props + }) + return () => { + return h('div', [props.firstName, props.lastName]) + } + }, + }) + + const WrapperField = defineComponent({ + setup(props, ctx) { + const { attrs } = ctx + return () => { + return h(Field, { + attrs: { + ...attrs, + }, + }) + } + }, + }) + + const App = defineComponent({ + setup() { + let person = ref({ + firstName: 'wang', + }) + onMounted(async () => { + person.value = { + firstName: 'wang', + lastName: 'xiao', + } + }) + return () => { + return h('div', [ + h(WrapperField, { + attrs: { + ...person.value, + }, + }), + ]) + } + }, + }) + const vm = new Vue(App).$mount() + + await sleep(100) + await vm.$nextTick() + expect(vm.$el.outerText === 'wangxiao') + expect(propsFromAttrs).toStrictEqual({ + firstName: 'wang', + lastName: 'xiao', + }) + }) + + // #840 + it('changing prop causes rerender to lose attributes', async () => { + let childAttrs = [] + const Parent = { + computed: { + attrs() { + return { + 'data-type': this.type, + } + }, + }, + props: { + type: { + type: String, + required: true, + }, + }, + mounted() { + childAttrs.push(this.attrs) + }, + updated() { + childAttrs.push(this.attrs) + }, + template: '
Parent
', + } + const Child = { + props: { + type: { + type: String, + required: true, + }, + }, + computed: { + attrs() { + return { + 'data-type': this.type, + } + }, + }, + data() { + return { + update: 0, + } + }, + template: '
Child
', + } + + const App = { + name: 'App', + data() { + return { parentType: 'parent' } + }, + async mounted() { + await sleep(300) + this.parentType = 'parent-click' + }, + + components: { + Parent, + Child, + }, + template: `
+ + + +
`, + } + const vm = new Vue(App).$mount() + + await sleep(1000) + await vm.$nextTick() + expect(childAttrs).toStrictEqual([ + { 'data-type': 'parent' }, + { 'data-type': 'parent-click' }, + ]) + }) +}) diff --git a/test/setupContext.spec.ts b/test/setupContext.spec.ts new file mode 100644 index 00000000..7943d5c2 --- /dev/null +++ b/test/setupContext.spec.ts @@ -0,0 +1,249 @@ +import { + h, + defineComponent, + createApp, + ref, + computed, + nextTick, + SetupContext, + getCurrentInstance, +} from '../src' +import { mockWarn } from './helpers' + +describe('setupContext', () => { + mockWarn(true) + it('should have proper properties', () => { + let context: SetupContext = undefined! + + const vm = createApp( + defineComponent({ + setup(_, ctx) { + context = ctx + }, + template: '
', + }) + ).mount() + + expect(context).toBeDefined() + expect('parent' in context).toBe(true) + expect(context.slots).toBeDefined() + expect(context.attrs).toEqual(vm.$attrs) + + // CAUTION: these will be removed in 3.0 + expect(context.root).toBe(vm.$root) + expect(context.parent).toBe(vm.$parent) + expect(context.listeners).toBe(vm.$listeners) + expect(context.refs).toBe(vm.$refs) + expect(typeof context.emit === 'function').toBe(true) + }) + + it('slots should work in render function', () => { + const vm = createApp( + defineComponent({ + template: ` + + + + + `, + components: { + test: defineComponent({ + setup(_, { slots }) { + return () => { + return h('div', [slots.default?.(), slots.item?.()]) + } + }, + }), + }, + }) + ).mount() + expect(vm.$el.innerHTML).toBe('foomeh') + }) + + it('warn for slots calls outside of the render() function', () => { + let warn = vi.spyOn(global.console, 'error').mockImplementation(() => null) + + createApp( + defineComponent({ + template: ` + + + + `, + components: { + test: { + setup(_, { slots }) { + slots.default?.() + }, + }, + }, + }) + ).mount() + expect(warn.mock.calls[0][0]).toMatch( + 'slots.default() got called outside of the "render()" scope' + ) + warn.mockRestore() + }) + + it('staled slots should be removed', () => { + const Child = { + template: '
', + } + const vm = createApp( + defineComponent({ + components: { Child }, + template: ` + + + + `, + }) + ).mount() + expect(vm.$el.textContent).toMatch(`foo foo`) + }) + + it('slots should be synchronized', async () => { + let slotKeys: string[] = [] + + const Foo = defineComponent({ + setup(_, { slots }) { + slotKeys = Object.keys(slots) + return () => { + slotKeys = Object.keys(slots) + return h('div', [ + slots.default && slots.default('from foo default'), + slots.one && slots.one('from foo one'), + slots.two && slots.two('from foo two'), + slots.three && slots.three('from foo three'), + ]) + } + }, + }) + + const vm = createApp( + defineComponent({ + data() { + return { + a: 'one', + b: 'two', + } + }, + template: ` + + + + + `, + components: { Foo }, + }) + ).mount() + + expect(slotKeys).toEqual(['one', 'two']) + expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch( + `a from foo one b from foo two` + ) + + // @ts-expect-error + vm.a = 'two' + // @ts-expect-error + vm.b = 'three' + + await nextTick() + // expect(slotKeys).toEqual(['one', 'three']); + expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch( + `a from foo two b from foo three ` + ) + }) + + // #264 + it('attrs should be reactive after destructuring', async () => { + let _attrs: SetupContext['attrs'] = undefined! + const foo = ref('bar') + + const ComponentA = defineComponent({ + setup(_, { attrs }) { + _attrs = attrs + }, + template: `
{{$attrs}}
`, + }) + const Root = defineComponent({ + components: { + ComponentA, + }, + setup() { + return { foo } + }, + template: ` + + `, + }) + + createApp(Root).mount() + + expect(_attrs).toBeDefined() + expect(_attrs.foo).toBe('bar') + + foo.value = 'bar2' + + await nextTick() + + expect(_attrs.foo).toBe('bar2') + }) + + // #563 + it('should not RangeError: Maximum call stack size exceeded', async () => { + createApp( + defineComponent({ + template: `
`, + setup() { + // @ts-expect-error + const app = getCurrentInstance().proxy + let mockNT: any = [] + mockNT.__ob__ = {} + const test = { + app, + mockNT, + } + return { + test, + } + }, + }) + ).mount() + + await nextTick() + expect( + `"RangeError: Maximum call stack size exceeded"` + ).not.toHaveBeenWarned() + }) + + // #794 + it('should not trigger getter w/ object computed nested', async () => { + const spy = vi.fn() + createApp( + defineComponent({ + template: `
`, + setup() { + const person = { + name: computed(() => { + spy() + return 1 + }), + } + return { + person, + } + }, + }) + ).mount() + expect(spy).toHaveBeenCalledTimes(0) + }) +}) diff --git a/test/ssr/serverPrefetch.spec.js b/test/ssr/serverPrefetch.spec.js new file mode 100644 index 00000000..14d2dd8c --- /dev/null +++ b/test/ssr/serverPrefetch.spec.js @@ -0,0 +1,139 @@ +import Vue from 'vue/dist/vue.common.js' +import { createRenderer } from 'vue-server-renderer' +import { ref, onServerPrefetch, getCurrentInstance } from '../../src' + +function fetch(result) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(result) + }, 10) + }) +} + +describe('serverPrefetch', () => { + it('should prefetch async operations before rendering', async () => { + const app = new Vue({ + setup() { + const count = ref(0) + + onServerPrefetch(async () => { + count.value = await fetch(42) + }) + + return { + count, + } + }, + render(h) { + return h('div', this.count) + }, + }) + + const serverRenderer = createRenderer() + const html = await serverRenderer.renderToString(app) + expect(html).toBe('
42
') + }) + + it('should prefetch many async operations before rendering', async () => { + const app = new Vue({ + setup() { + const count = ref(0) + const label = ref('') + + onServerPrefetch(async () => { + count.value = await fetch(42) + }) + + onServerPrefetch(async () => { + label.value = await fetch('meow') + }) + + return { + count, + label, + } + }, + render(h) { + return h('div', [this.count, this.label]) + }, + }) + + const serverRenderer = createRenderer() + const html = await serverRenderer.renderToString(app) + expect(html).toBe('
42meow
') + }) + + it('should pass ssrContext', async () => { + const child = { + setup(props, { ssrContext }) { + const content = ref() + + expect(ssrContext.foo).toBe('bar') + + onServerPrefetch(async () => { + content.value = await fetch(ssrContext.foo) + }) + + return { + content, + } + }, + render(h) { + return h('div', this.content) + }, + } + + const app = new Vue({ + components: { + child, + }, + render(h) { + return h('child') + }, + }) + + const serverRenderer = createRenderer() + const html = await serverRenderer.renderToString(app, { foo: 'bar' }) + expect(html).toBe('
bar
') + }) + + it('should not share context', async () => { + const instances = [] + function createApp(context) { + return new Vue({ + setup() { + const count = ref(0) + + onServerPrefetch(async () => { + count.value = await fetch(context.result) + }) + + instances.push(getCurrentInstance()) + + return { + count, + } + }, + render(h) { + return h('div', this.count) + }, + }) + } + + const serverRenderer = createRenderer() + const promises = [] + // Parallel requests + for (let i = 1; i < 3; i++) { + promises.push( + new Promise(async (resolve) => { + const app = createApp({ result: i }) + const html = await serverRenderer.renderToString(app) + expect(html).toBe(`
${i}
`) + resolve() + }) + ) + } + await Promise.all(promises) + expect((instances[0] === instances[1]) === instances[2]).toBe(false) + }) +}) diff --git a/test/ssr/ssrReactive.spec.ts b/test/ssr/ssrReactive.spec.ts new file mode 100644 index 00000000..66c3dc3d --- /dev/null +++ b/test/ssr/ssrReactive.spec.ts @@ -0,0 +1,176 @@ +/** + * @jest-environment node + */ + +import Vue from '../vue' +import { + isReactive, + reactive, + ref, + isRaw, + isRef, + set, + shallowRef, + getCurrentInstance, + nextTick, +} from '../../src' +import { createRenderer } from 'vue-server-renderer' +import { mockWarn } from '../helpers' + +describe('SSR Reactive', () => { + mockWarn(true) + beforeEach(() => { + process.env.VUE_ENV = 'server' + }) + + it('should in SSR context', async () => { + expect(typeof window).toBe('undefined') + expect((Vue.observable({}) as any).__ob__).toBeUndefined() + }) + + it('should render', async () => { + const app = new Vue({ + setup() { + return { + count: ref(42), + } + }, + render(this: any, h) { + return h('div', this.count) + }, + }) + + const serverRenderer = createRenderer() + const html = await serverRenderer.renderToString(app) + expect(html).toBe('
42
') + }) + + it('reactive + isReactive', async () => { + const state = reactive({}) + expect(isReactive(state)).toBe(true) + expect(isRaw(state)).toBe(false) + }) + + it('shallowRef + isRef', async () => { + const state = shallowRef({}) + expect(isRef(state)).toBe(true) + expect(isRaw(state)).toBe(false) + }) + + it('should work on objects sets with set()', () => { + const state = ref({}) + + set(state.value, 'a', {}) + expect(isReactive(state.value.a)).toBe(true) + + set(state.value, 'a', {}) + expect(isReactive(state.value.a)).toBe(true) + }) + + it('should work on arrays sets with set()', () => { + const state = ref([]) + + set(state.value, 1, {}) + expect(isReactive(state.value[1])).toBe(true) + + set(state.value, 1, {}) + expect(isReactive(state.value[1])).toBe(true) + }) + + // #550 + it('props should work with set', () => + new Promise(async (done) => { + let props: any + + const app = new Vue({ + render(this: any, h) { + return h('child', { attrs: { msg: this.msg } }) + }, + setup() { + return { msg: ref('hello') } + }, + components: { + child: { + render(this: any, h: any) { + return h('span', this.data.msg) + }, + props: ['msg'], + setup(_props) { + props = _props + + return { data: _props } + }, + }, + }, + }) + + const serverRenderer = createRenderer() + const html = await serverRenderer.renderToString(app) + + expect(html).toBe('hello') + + expect(props.bar).toBeUndefined() + set(props, 'bar', 'bar') + expect(props.bar).toBe('bar') + + done() + })) + + // #721 + it('should behave correctly', () => { + const state = ref({ old: ref(false) }) + set(state.value, 'new', ref(true)) + // console.log(process.server, 'state.value', JSON.stringify(state.value)) + + expect(state.value).toMatchObject({ + old: false, + new: true, + }) + }) + + // #721 + it('should behave correctly for the nested ref in the object', () => { + const state = { old: ref(false) } + set(state, 'new', ref(true)) + expect(JSON.stringify(state)).toBe( + '{"old":{"value":false},"new":{"value":true}}' + ) + }) + + // #721 + it('should behave correctly for ref of object', () => { + const state = ref({ old: ref(false) }) + set(state.value, 'new', ref(true)) + expect(JSON.stringify(state.value)).toBe('{"old":false,"new":true}') + }) + + // test the input parameter of mockReactivityDeep + it('ssr should not RangeError: Maximum call stack size exceeded', async () => { + new Vue({ + setup() { + // @ts-expect-error + const app = getCurrentInstance().proxy + let mockNt: any = [] + mockNt.__ob__ = {} + const test = reactive({ + app, + mockNt, + }) + return { + test, + } + }, + }) + await nextTick() + expect( + `"RangeError: Maximum call stack size exceeded"` + ).not.toHaveBeenWarned() + }) + + it('should work on objects sets with set()', () => { + const state = ref({}) + set(state.value, 'a', {}) + + expect(isReactive(state.value.a)).toBe(true) + }) +}) diff --git a/test/templateRefs.spec.js b/test/templateRefs.spec.js new file mode 100644 index 00000000..42bfee01 --- /dev/null +++ b/test/templateRefs.spec.js @@ -0,0 +1,168 @@ +import Vue from 'vue/dist/vue.common.js' +import { ref, watchEffect, nextTick } from '../src' + +describe('ref', () => { + it('should work', () => + new Promise((done) => { + let dummy + const vm = new Vue({ + setup() { + const ref1 = ref(null) + watchEffect(() => { + dummy = ref1.value + }) + + return { + bar: ref1, + } + }, + template: `
+ +
`, + components: { + test: { + id: 'test', + template: '
test
', + }, + }, + }).$mount() + vm.$nextTick() + .then(() => { + expect(dummy).toBe(vm.$refs.bar) + }) + .then(done) + })) + + it('should dynamically update refs', () => + new Promise((done, reject) => { + done.fail = reject + + let dummy1 = null + let dummy2 = null + + const vm = new Vue({ + setup() { + const ref1 = ref(null) + const ref2 = ref(null) + watchEffect(() => { + dummy1 = ref1.value + dummy2 = ref2.value + }) + + return { + value: 'bar', + bar: ref1, + foo: ref2, + } + }, + template: '
', + }).$mount() + waitForUpdate(() => {}) + .then(() => { + expect(dummy1).toBe(vm.$refs.bar) + expect(dummy2).toBe(null) + vm.value = 'foo' + }) + .then(() => { + // vm updated. ref update occures after updated; + }) + .then(() => { + // no render cycle, empty tick + }) + .then(() => { + expect(dummy1).toBe(null) + expect(dummy2).toBe(vm.$refs.foo) + }) + .then(done) + })) + + // #296 + it('should dynamically update refs in context', async () => { + const vm = new Vue({ + setup() { + const barRef = ref(null) + return { + barRef, + } + }, + template: `
+ +
`, + components: { + bar: { + setup() { + const name = ref('bar') + return { + name, + } + }, + template: '
{{name}}
', + }, + foo: { + setup() { + const showSlot = ref(false) + const setShow = () => { + showSlot.value = true + } + return { + setShow, + showSlot, + } + }, + template: `
`, + }, + }, + }).$mount() + await nextTick() + //@ts-ignore + vm.$children[0].setShow() + await nextTick() + //@ts-ignore + expect(vm.$refs.barRef).toBe(vm.barRef) + }) + + it('should update deeply nested component refs using scoped slots', async () => { + const vm = new Vue({ + setup() { + const divRef = ref(null) + const showDiv = ref(false) + return { + divRef, + showDiv, + } + }, + template: `
Slot:
`, + components: { + foo: { + components: { + bar: { + template: `
`, + }, + }, + template: '
', + }, + }, + }).$mount() + await nextTick() + //@ts-ignore + vm.showDiv = true + await nextTick() + //@ts-ignore + expect(vm.$refs.divRef).toBe(vm.divRef) + }) + // TODO: how ? + // it('work with createElement', () => { + // let root; + // const vm = new Vue({ + // setup() { + // root = ref(null); + // return () => { + // return h('div', { + // ref: root, + // }); + // }; + // }, + // }).$mount(); + // expect(root.value).toBe(vm.$el); + // }); +}) diff --git a/test/tsTransform.js b/test/tsTransform.js deleted file mode 100644 index 603d94fb..00000000 --- a/test/tsTransform.js +++ /dev/null @@ -1,11 +0,0 @@ -const tsc = require('typescript'); -const tsConfig = require('../tsconfig.json'); - -module.exports = { - process(src, path) { - if (path.endsWith('.ts')) { - return tsc.transpile(src, { ...tsConfig.compilerOptions, module: 'commonjs' }, path, []); - } - return src; - }, -}; diff --git a/test/types/defineComponent.spec.ts b/test/types/defineComponent.spec.ts new file mode 100644 index 00000000..57b58db1 --- /dev/null +++ b/test/types/defineComponent.spec.ts @@ -0,0 +1,221 @@ +import { defineComponent, h, ref, SetupContext, PropType } from '../../src' +import Router from 'vue-router' + +const Vue = require('vue/dist/vue.common.js') + +type Equal = (() => U extends Left ? 1 : 0) extends < + U +>() => U extends Right ? 1 : 0 + ? true + : false + +const isTypeEqual = (shouldBeEqual: Equal) => { + void shouldBeEqual + expect(true).toBe(true) +} +const isSubType = ( + shouldBeEqual: SubType extends SuperType ? true : false +) => { + void shouldBeEqual + expect(true).toBe(true) +} + +describe('defineComponent', () => { + it('should work', () => { + const Child = defineComponent({ + props: { msg: String }, + setup(props) { + return () => h('span', props.msg) + }, + }) + + const App = defineComponent({ + setup() { + const msg = ref('hello') + return () => + h('div', [ + h(Child, { + props: { + msg: msg.value, + }, + }), + ]) + }, + }) + const vm = new Vue(App).$mount() + expect(vm.$el.querySelector('span').textContent).toBe('hello') + }) + + it('should infer props type', () => { + const App = defineComponent({ + props: { + a: { + type: Number, + default: 0, + }, + b: String, + }, + setup(props, ctx) { + type PropsType = typeof props + isTypeEqual(true) + isSubType(true) + isSubType<{ readonly b?: string; readonly a: number }, PropsType>(true) + return () => null + }, + }) + new Vue(App) + expect.assertions(3) + }) + + it('custom props interface', () => { + interface IPropsType { + b: string + } + const App = defineComponent({ + props: { + b: {}, + }, + setup(props, ctx) { + type PropsType = typeof props + isTypeEqual(true) + isSubType(true) + isSubType<{ b: string }, PropsType>(true) + return () => null + }, + }) + new Vue(App) + expect.assertions(3) + }) + + it('custom props type function', () => { + interface IPropsTypeFunction { + fn: (arg: boolean) => void + } + const App = defineComponent({ + props: { + fn: Function as PropType<(arg: boolean) => void>, + }, + setup(props, ctx) { + type PropsType = typeof props + isTypeEqual(true) + isSubType void }>(true) + isSubType<{ fn: (arg: boolean) => void }, PropsType>(true) + return () => null + }, + }) + new Vue(App) + expect.assertions(3) + }) + + it('custom props type inferred from PropType', () => { + interface User { + name: string + } + const App = defineComponent({ + props: { + user: Object as PropType, + func: Function as PropType<() => boolean>, + userFunc: Function as PropType<(u: User) => User>, + }, + setup(props) { + type PropsType = typeof props + isSubType< + { user?: User; func?: () => boolean; userFunc?: (u: User) => User }, + PropsType + >(true) + isSubType< + PropsType, + { user?: User; func?: () => boolean; userFunc?: (u: User) => User } + >(true) + }, + }) + new Vue(App) + expect.assertions(2) + }) + + it('no props', () => { + const App = defineComponent({ + setup(props, ctx) { + isTypeEqual(true) + isTypeEqual<{}, typeof props>(true) + return () => null + }, + }) + new Vue(App) + expect.assertions(2) + }) + + it('should accept tuple props', () => { + const App = defineComponent({ + props: ['p1', 'p2'], + setup(props) { + props.p1 + props.p2 + type PropsType = typeof props + type Expected = { readonly p1?: any; readonly p2?: any } + isSubType(true) + isSubType(true) + }, + }) + new Vue(App) + expect.assertions(2) + }) + + it('should allow any custom options', () => { + const App = defineComponent({ + foo: 'foo', + bar: 'bar', + }) + new Vue(App) + }) + + it('infer the required prop', () => { + const App = defineComponent({ + props: { + foo: { + type: String, + required: true, + }, + bar: { + type: String, + default: 'default', + }, + zoo: { + type: String, + required: false, + }, + }, + propsData: { + foo: 'foo', + }, + setup(props) { + type PropsType = typeof props + isSubType< + { readonly foo: string; readonly bar: string; readonly zoo?: string }, + PropsType + >(true) + isSubType< + PropsType, + { readonly foo: string; readonly bar: string; readonly zoo?: string } + >(true) + return () => null + }, + }) + new Vue(App) + expect.assertions(2) + }) + + describe('compatible with vue router', () => { + it('RouteConfig.component', () => { + new Router({ + routes: [ + { + path: '/', + name: 'root', + component: defineComponent({}), + }, + ], + }) + }) + }) +}) diff --git a/test/use.spec.ts b/test/use.spec.ts new file mode 100644 index 00000000..b375dbfb --- /dev/null +++ b/test/use.spec.ts @@ -0,0 +1,59 @@ +import Vue from 'vue' +import CompositionApi from '../src' +import { createLocalVue } from './helpers/create-local-vue' +import { mockWarn } from './helpers' + +describe('use', () => { + mockWarn(true) + + it('should allow install in multiple vue', () => { + const localVueOne = createLocalVue() + localVueOne.use(CompositionApi) + + const localVueTwo = createLocalVue() + localVueTwo.use(CompositionApi) + + expect( + '[vue-composition-api] another instance of Vue installed' + ).not.toHaveBeenWarned() + }) + + it('should warn install in multiple vue', () => { + try { + const fakeVue = { + version: '2._.x', + config: { + optionMergeStrategies: {}, + }, + mixin: vi.fn(), + } + + // @ts-ignore + CompositionApi.install(fakeVue) + expect( + '[vue-composition-api] another instance of Vue installed' + ).toHaveBeenWarned() + } finally { + Vue.use(CompositionApi) + expect( + '[vue-composition-api] another instance of Vue installed' + ).toHaveBeenWarned() + } + }) + + it('should warn installing multiple times', () => { + const localVueOne = createLocalVue() + localVueOne.use(CompositionApi) + + // vue prevents the same plugin of being installed, this will create a new plugin instance + localVueOne.use({ + install(v) { + CompositionApi.install(v) + }, + }) + + expect( + '[vue-composition-api] already installed. Vue.use(VueCompositionAPI) should be called only once.' + ).toHaveBeenWarned() + }) +}) diff --git a/test/v3/reactivity/computed.spec.ts b/test/v3/reactivity/computed.spec.ts new file mode 100644 index 00000000..f903066b --- /dev/null +++ b/test/v3/reactivity/computed.spec.ts @@ -0,0 +1,194 @@ +import { + computed, + reactive, + ref, + watchEffect, + WritableComputedRef, + nextTick, +} from '../../../src' +import { mockWarn } from '../../helpers' + +describe('reactivity/computed', () => { + mockWarn(true) + + it('should return updated value', async () => { + const value = reactive<{ foo?: number }>({ foo: undefined }) + const cValue = computed(() => value.foo) + expect(cValue.value).toBe(undefined) + value.foo = 1 + await nextTick() + + expect(cValue.value).toBe(1) + }) + + it('should compute lazily', () => { + const value = reactive<{ foo?: number }>({ foo: undefined }) + const getter = vi.fn(() => value.foo) + const cValue = computed(getter) + + // lazy + expect(getter).not.toHaveBeenCalled() + + expect(cValue.value).toBe(undefined) + expect(getter).toHaveBeenCalledTimes(1) + + // should not compute again + cValue.value + expect(getter).toHaveBeenCalledTimes(1) + + // should not compute until needed + value.foo = 1 + expect(getter).toHaveBeenCalledTimes(1) + + // now it should compute + expect(cValue.value).toBe(1) + expect(getter).toHaveBeenCalledTimes(2) + + // should not compute again + cValue.value + expect(getter).toHaveBeenCalledTimes(2) + }) + + it('should trigger effect', () => { + const value = reactive<{ foo?: number }>({ foo: undefined }) + const cValue = computed(() => value.foo) + let dummy + watchEffect( + () => { + dummy = cValue.value + }, + { flush: 'sync' } + ) + expect(dummy).toBe(undefined) + value.foo = 1 + expect(dummy).toBe(1) + }) + + it('should work when chained', () => { + const value = reactive({ foo: 0 }) + const c1 = computed(() => value.foo) + const c2 = computed(() => c1.value + 1) + expect(c2.value).toBe(1) + expect(c1.value).toBe(0) + value.foo++ + expect(c2.value).toBe(2) + expect(c1.value).toBe(1) + }) + + it('should trigger effect when chained', () => { + const value = reactive({ foo: 0 }) + const getter1 = vi.fn(() => value.foo) + const getter2 = vi.fn(() => { + return c1.value + 1 + }) + const c1 = computed(getter1) + const c2 = computed(getter2) + + let dummy + watchEffect( + () => { + dummy = c2.value + }, + { flush: 'sync' } + ) + expect(dummy).toBe(1) + expect(getter1).toHaveBeenCalledTimes(1) + expect(getter2).toHaveBeenCalledTimes(1) + value.foo++ + expect(dummy).toBe(2) + // should not result in duplicate calls + expect(getter1).toHaveBeenCalledTimes(2) + expect(getter2).toHaveBeenCalledTimes(2) + }) + + it('should trigger effect when chained (mixed invocations)', async () => { + const value = reactive({ foo: 0 }) + const getter1 = vi.fn(() => value.foo) + const getter2 = vi.fn(() => { + return c1.value + 1 + }) + const c1 = computed(getter1) + const c2 = computed(getter2) + + let dummy + watchEffect(() => { + dummy = c1.value + c2.value + }) + await nextTick() + expect(dummy).toBe(1) + + expect(getter1).toHaveBeenCalledTimes(1) + expect(getter2).toHaveBeenCalledTimes(1) + value.foo++ + + await nextTick() + + expect(dummy).toBe(3) + // should not result in duplicate calls + expect(getter1).toHaveBeenCalledTimes(2) + expect(getter2).toHaveBeenCalledTimes(2) + }) + + // it('should no longer update when stopped', () => { + // const value = reactive<{ foo?: number }>({}); + // const cValue = computed(() => value.foo); + // let dummy; + // effect(() => { + // dummy = cValue.value; + // }); + // expect(dummy).toBe(undefined); + // value.foo = 1; + // expect(dummy).toBe(1); + // stop(cValue.effect); + // value.foo = 2; + // expect(dummy).toBe(1); + // }); + + it('should support setter', () => { + const n = ref(1) + const plusOne = computed({ + get: () => n.value + 1, + set: (val) => { + n.value = val - 1 + }, + }) + + expect(plusOne.value).toBe(2) + n.value++ + expect(plusOne.value).toBe(3) + + plusOne.value = 0 + expect(n.value).toBe(-1) + }) + + it('should trigger effect w/ setter', async () => { + const n = ref(1) + const plusOne = computed({ + get: () => n.value + 1, + set: (val) => { + n.value = val - 1 + }, + }) + + let dummy + watchEffect(() => { + dummy = n.value + }) + expect(dummy).toBe(1) + + plusOne.value = 0 + await nextTick() + expect(dummy).toBe(-1) + }) + + it('should warn if trying to set a readonly computed', async () => { + const n = ref(1) + const plusOne = computed(() => n.value + 1) + ;(plusOne as WritableComputedRef).value++ // Type cast to prevent TS from preventing the error + await nextTick() + + expect( + 'Write operation failed: computed value is readonly' + ).toHaveBeenWarnedLast() + }) +}) diff --git a/test/v3/reactivity/del.spec.ts b/test/v3/reactivity/del.spec.ts new file mode 100644 index 00000000..490ce00d --- /dev/null +++ b/test/v3/reactivity/del.spec.ts @@ -0,0 +1,61 @@ +import { del, reactive, ref, watch, set, watchEffect } from '../../../src' + +// Vue.delete workaround for triggering view updates on object property/array index deletion +describe('reactivity/del', () => { + it('should not trigger reactivity on native object member deletion', () => { + const obj = reactive<{ a?: object }>({ + a: {}, + }) + const spy = vi.fn() + watch(obj, spy, { deep: true, flush: 'sync' }) + delete obj.a // Vue 2 limitation + expect(spy).not.toHaveBeenCalled() + expect(obj).toStrictEqual({}) + }) + + it('should trigger reactivity when using del on reactive object', () => { + const obj = reactive<{ a?: object }>({ + a: {}, + }) + const spy = vi.fn() + watch(obj, spy, { deep: true, flush: 'sync' }) + del(obj, 'a') + expect(spy).toBeCalledTimes(1) + expect(obj).toStrictEqual({}) + }) + + it('should not remove element on array index and should not trigger reactivity', () => { + const arr = ref([1, 2, 3]) + const spy = vi.fn() + watch(arr, spy, { flush: 'sync' }) + delete arr.value[1] // Vue 2 limitation; workaround with .splice() + expect(spy).not.toHaveBeenCalled() + expect(arr.value).toEqual([1, undefined, 3]) + }) + + it('should trigger reactivity when using del on array', () => { + const arr = ref([1, 2, 3]) + const spy = vi.fn() + watch(arr, spy, { flush: 'sync' }) + del(arr.value, 1) + expect(spy).toBeCalledTimes(1) + expect(arr.value).toEqual([1, 3]) + }) + + it('should trigger reactivity when using del on array to delete index out of valid array length', () => { + const arr = ref([]) + const MAX_VALID_ARRAY_LENGTH = Math.pow(2, 32) - 1 + const NON_VALID_INDEX = MAX_VALID_ARRAY_LENGTH + 1 + set(arr.value, NON_VALID_INDEX, 0) + const spy = vi.fn() + watchEffect( + () => { + spy(arr.value) + }, + { flush: 'sync' } + ) + expect(spy).toBeCalledTimes(1) + del(arr.value, NON_VALID_INDEX) + expect(spy).toBeCalledTimes(2) + }) +}) diff --git a/test/v3/reactivity/effectScope.spec.ts b/test/v3/reactivity/effectScope.spec.ts new file mode 100644 index 00000000..decb8710 --- /dev/null +++ b/test/v3/reactivity/effectScope.spec.ts @@ -0,0 +1,327 @@ +import { + nextTick, + watch, + watchEffect, + reactive, + EffectScope, + onScopeDispose, + computed, + ref, + ComputedRef, + createApp, + getCurrentScope, +} from '../../../src' +import { mockWarn } from '../../helpers' + +describe('reactivity/effect/scope', () => { + mockWarn(true) + + it('should run', () => { + const fnSpy = vi.fn(() => {}) + new EffectScope().run(fnSpy) + expect(fnSpy).toHaveBeenCalledTimes(1) + }) + + it('should accept zero argument', () => { + const scope = new EffectScope() + expect(scope.effects.length).toBe(0) + }) + + it('should return run value', () => { + expect(new EffectScope().run(() => 1)).toBe(1) + }) + + it('should collect the effects', async () => { + const scope = new EffectScope() + let dummy = 0 + scope.run(() => { + const counter = reactive({ num: 0 }) + watchEffect(() => (dummy = counter.num)) + + expect(dummy).toBe(0) + counter.num = 7 + }) + + await nextTick() + + expect(dummy).toBe(7) + // expect(scope.effects.length).toBe(1) + }) + + it('stop', async () => { + let dummy, doubled + const counter = reactive({ num: 0 }) + + const scope = new EffectScope() + scope.run(() => { + watchEffect(() => (dummy = counter.num)) + watchEffect(() => (doubled = counter.num * 2)) + }) + + // expect(scope.effects.length).toBe(2) + + expect(dummy).toBe(0) + counter.num = 7 + await nextTick() + expect(dummy).toBe(7) + expect(doubled).toBe(14) + + scope.stop() + + counter.num = 6 + await nextTick() + expect(dummy).toBe(7) + expect(doubled).toBe(14) + }) + + it('should collect nested scope', async () => { + let dummy, doubled + const counter = reactive({ num: 0 }) + + const scope = new EffectScope() + scope.run(() => { + // nested scope + new EffectScope().run(() => { + watchEffect(() => (doubled = counter.num * 2)) + }) + watchEffect(() => (dummy = counter.num)) + }) + + // expect(scope.effects.length).toBe(2) + expect(scope.effects[0]).toBeInstanceOf(EffectScope) + + expect(dummy).toBe(0) + counter.num = 7 + await nextTick() + expect(dummy).toBe(7) + expect(doubled).toBe(14) + + // stop the nested scope as well + scope.stop() + + counter.num = 6 + await nextTick() + expect(dummy).toBe(7) + expect(doubled).toBe(14) + }) + + it('nested scope can be escaped', async () => { + let dummy, doubled + const counter = reactive({ num: 0 }) + + const scope = new EffectScope() + scope.run(() => { + watchEffect(() => (dummy = counter.num)) + // nested scope + new EffectScope(true).run(() => { + watchEffect(() => (doubled = counter.num * 2)) + }) + }) + + expect(scope.effects.length).toBe(0) + + expect(dummy).toBe(0) + counter.num = 7 + await nextTick() + expect(dummy).toBe(7) + expect(doubled).toBe(14) + + scope.stop() + + counter.num = 6 + await nextTick() + expect(dummy).toBe(7) + + // nested scope should not be stopped + expect(doubled).toBe(12) + }) + + it('able to run the scope', async () => { + let dummy, doubled + const counter = reactive({ num: 0 }) + + const scope = new EffectScope() + scope.run(() => { + watchEffect(() => (dummy = counter.num)) + }) + + // expect(scope.effects.length).toBe(1) + + scope.run(() => { + watchEffect(() => (doubled = counter.num * 2)) + }) + + // expect(scope.effects.length).toBe(2) + + counter.num = 7 + await nextTick() + expect(dummy).toBe(7) + expect(doubled).toBe(14) + + scope.stop() + }) + + it('can not run an inactive scope', async () => { + let dummy, doubled + const counter = reactive({ num: 0 }) + + const scope = new EffectScope() + scope.run(() => { + watchEffect(() => (dummy = counter.num)) + }) + + // expect(scope.effects.length).toBe(1) + + scope.stop() + + scope.run(() => { + watchEffect(() => (doubled = counter.num * 2)) + }) + + expect( + '[Vue warn]: cannot run an inactive effect scope.' + ).toHaveBeenWarned() + + // expect(scope.effects.length).toBe(1) + + counter.num = 7 + await nextTick() + expect(dummy).toBe(0) + expect(doubled).toBe(undefined) + }) + + it('should fire onScopeDispose hook', () => { + let dummy = 0 + + const scope = new EffectScope() + scope.run(() => { + onScopeDispose(() => (dummy += 1)) + onScopeDispose(() => (dummy += 2)) + }) + + scope.run(() => { + onScopeDispose(() => (dummy += 4)) + }) + + expect(dummy).toBe(0) + + scope.stop() + expect(dummy).toBe(7) + }) + + it('should warn onScopeDispose() is called when there is no active effect scope', () => { + const spy = vi.fn() + const scope = new EffectScope() + scope.run(() => { + onScopeDispose(spy) + }) + + expect(spy).toHaveBeenCalledTimes(0) + + onScopeDispose(spy) + + expect( + '[Vue warn]: onScopeDispose() is called when there is no active effect scope to be associated with.' + ).toHaveBeenWarned() + + scope.stop() + expect(spy).toHaveBeenCalledTimes(1) + }) + + it('test with higher level APIs', async () => { + const r = ref(1) + + const computedSpy = vi.fn() + const watchSpy = vi.fn() + const watchEffectSpy = vi.fn() + + let c: ComputedRef + const scope = new EffectScope() + scope.run(() => { + c = computed(() => { + computedSpy() + return r.value + 1 + }) + + watch(r, watchSpy) + watchEffect(() => { + watchEffectSpy() + r.value + }) + }) + + c!.value // computed is lazy so trigger collection + expect(computedSpy).toHaveBeenCalledTimes(1) + expect(watchSpy).toHaveBeenCalledTimes(0) + expect(watchEffectSpy).toHaveBeenCalledTimes(1) + + r.value++ + c!.value + await nextTick() + expect(computedSpy).toHaveBeenCalledTimes(2) + expect(watchSpy).toHaveBeenCalledTimes(1) + expect(watchEffectSpy).toHaveBeenCalledTimes(2) + + scope.stop() + + r.value++ + c!.value + await nextTick() + // should not trigger anymore + expect(computedSpy).toHaveBeenCalledTimes(2) + expect(watchSpy).toHaveBeenCalledTimes(1) + expect(watchEffectSpy).toHaveBeenCalledTimes(2) + }) + + it('should stop along with parent component', async () => { + let dummy, doubled + const counter = reactive({ num: 0 }) + + const root = document.createElement('div') + const vm = createApp({ + setup() { + const scope = new EffectScope() + scope.run(() => { + watchEffect(() => (dummy = counter.num)) + watchEffect(() => (doubled = counter.num * 2)) + }) + }, + }) + vm.mount(root) + + expect(dummy).toBe(0) + counter.num = 7 + await nextTick() + expect(dummy).toBe(7) + expect(doubled).toBe(14) + + vm.unmount() + + counter.num = 6 + await nextTick() + expect(dummy).toBe(7) + expect(doubled).toBe(14) + }) + + it('component should be a valid scope', async () => { + let dummy = 0 + let scope + + const root = document.createElement('div') + const vm = createApp({ + setup() { + scope = getCurrentScope() + onScopeDispose(() => (dummy += 1)) + scope?.cleanups.push(() => (dummy += 1)) + }, + }) + + vm.mount(root) + expect(dummy).toBe(0) + expect(scope).not.toBeFalsy() + + vm.unmount() + await nextTick() + expect(dummy).toBe(2) + }) +}) diff --git a/test/v3/reactivity/reactive.spec.ts b/test/v3/reactivity/reactive.spec.ts new file mode 100644 index 00000000..af18d2d7 --- /dev/null +++ b/test/v3/reactivity/reactive.spec.ts @@ -0,0 +1,275 @@ +import { + ref, + isRef, + reactive, + isReactive, + computed, + toRaw, + shallowReactive, + set, + markRaw, + isRaw, +} from '../../../src' + +describe('reactivity/reactive', () => { + let warn: vi.SpyInstance + beforeEach(() => { + warn = vi.spyOn(global.console, 'error').mockImplementation(() => null) + warn.mockReset() + }) + afterEach(() => { + expect(warn).not.toBeCalled() + warn.mockRestore() + }) + + test('Object', () => { + const original = { foo: 1 } + const observed = reactive(original) + expect(observed).toBe(original) + expect(isReactive(observed)).toBe(true) + expect(isReactive(original)).toBe(true) // this is false in v3 but true in v2 + // get + expect(observed.foo).toBe(1) + // has + expect('foo' in observed).toBe(true) + // ownKeys + expect(Object.keys(observed)).toEqual(['foo']) + }) + + //#693 + test('the hasOwn should be used to determine whether an attribute exists.', () => { + const obj = {} + expect(isReactive(obj)).toBe(false) + expect(isRaw(obj)).toBe(false) + const mockObj = new Proxy(obj, { + get: (target, key) => { + if (!(key in Object.keys(target))) { + throw new Error(`the ${key.toString()} is not found in the target.`) + } + }, + }) + expect(isReactive(mockObj)).toBe(false) + expect(isRaw(obj)).toBe(false) + }) + + test('proto', () => { + const obj = {} + const reactiveObj = reactive(obj) + expect(isReactive(reactiveObj)).toBe(true) + // read prop of reactiveObject will cause reactiveObj[prop] to be reactive + // @ts-ignore + const prototype = reactiveObj['__proto__'] + const otherObj = { data: ['a'] } + expect(isReactive(otherObj)).toBe(false) + const reactiveOther = reactive(otherObj) + expect(isReactive(reactiveOther)).toBe(true) + expect(reactiveOther.data[0]).toBe('a') + }) + test('nested reactives', () => { + const original = { + nested: { + foo: 1, + }, + array: [{ bar: 2 }], + } + const observed = reactive(original) + expect(isReactive(observed.nested)).toBe(true) + expect(isReactive(observed.array)).toBe(true) + expect(isReactive(observed.array[0])).toBe(true) + }) + + test('observed value should proxy mutations to original (Object)', () => { + const original: any = { foo: 1 } + const observed = reactive(original) + // set + observed.bar = 1 + expect(observed.bar).toBe(1) + expect(original.bar).toBe(1) + // delete + delete observed.foo + expect('foo' in observed).toBe(false) + expect('foo' in original).toBe(false) + }) + + test('setting a property with an unobserved value should wrap with reactive', () => { + const observed = reactive<{ foo?: object }>({}) + const raw = {} + set(observed, 'foo', raw) // v2 limitation + + expect(observed.foo).toBe(raw) // v2 limitation + expect(isReactive(observed.foo)).toBe(true) + }) + + test('observing already observed value should return same Proxy', () => { + const original = { foo: 1 } + const observed = reactive(original) + const observed2 = reactive(observed) + expect(observed2).toBe(observed) + }) + + test('observing the same value multiple times should return same Proxy', () => { + const original = { foo: 1 } + const observed = reactive(original) + const observed2 = reactive(original) + expect(observed2).toBe(observed) + }) + + test('should not pollute original object with Proxies', () => { + const original: any = { foo: 1 } + const original2 = { bar: 2 } + const observed = reactive(original) + const observed2 = reactive(original2) + observed.bar = observed2 + expect(observed.bar).toBe(observed2) + expect(original.bar).toBe(original2) + }) + + test('unwrap', () => { + // vue2 mutates the original object + const original = { foo: 1 } + const observed = reactive(original) + expect(toRaw(observed)).toBe(original) + expect(toRaw(original)).toBe(original) + }) + + test('should not unwrap Ref', () => { + const observedNumberRef = reactive(ref(1)) + const observedObjectRef = reactive(ref({ foo: 1 })) + + expect(isRef(observedNumberRef)).toBe(true) + expect(isRef(observedObjectRef)).toBe(true) + }) + + test('should unwrap computed refs', () => { + // readonly + const a = computed(() => 1) + // writable + const b = computed({ + get: () => 1, + set: () => {}, + }) + const obj = reactive({ a, b }) + // check type + obj.a + 1 + obj.b + 1 + expect(typeof obj.a).toBe(`number`) + expect(typeof obj.b).toBe(`number`) + }) + + test('non-observable values', () => { + const assertValue = (value: any) => { + expect(isReactive(reactive(value))).toBe(false) + expect(reactive(value)).toBe(value) + // expect(warnSpy).toHaveBeenLastCalledWith(`value cannot be made reactive: ${String(value)}`); + } + + // number + assertValue(1) + // string + assertValue('foo') + // boolean + assertValue(false) + // null + assertValue(null) + // undefined + assertValue(undefined) + // symbol + const s = Symbol() + assertValue(s) + + // built-ins should work and return same value + const p = Promise.resolve() + expect(reactive(p)).toBe(p) + const r = new RegExp('') + expect(reactive(r)).toBe(r) + const d = new Date() + expect(reactive(d)).toBe(d) + + expect(warn).toBeCalledTimes(12) + expect( + warn.mock.calls.map((call) => { + expect(call[0]).toBe( + '[Vue warn]: "reactive()" must be called on an object.' + ) + }) + ) + warn.mockReset() + }) + + test('markRaw', () => { + const obj = reactive({ + foo: { a: 1 }, + bar: markRaw({ b: 2 }), + }) + expect(isReactive(obj.foo)).toBe(true) + expect(isReactive(obj.bar)).toBe(false) + }) + + test('should not observe frozen objects', () => { + const obj = reactive({ + foo: Object.freeze({ a: 1 }), + }) + expect(isReactive(obj.foo)).toBe(false) + }) + + describe('shallowReactive', () => { + test('should not make non-reactive properties reactive', () => { + const props = shallowReactive({ n: { foo: 1 } }) + expect(isReactive(props.n)).toBe(false) + }) + + test('should keep reactive properties reactive', () => { + const props: any = shallowReactive({ n: reactive({ foo: 1 }) }) + props.n = reactive({ foo: 2 }) + expect(isReactive(props.n)).toBe(true) + }) + + test('should keep array as array', () => { + const arr = [1, 2, 3] + const shallowReactiveArr = shallowReactive(arr) + expect(Array.isArray(shallowReactiveArr)).toBe(true) + expect(shallowReactiveArr.join(' ')).toBe(arr.join(' ')) + }) + + test('should trigger computed when changed', () => { + const arr = Array(10).fill(0) + const shallowReactiveArr = shallowReactive(arr) + const sum = computed(() => + shallowReactiveArr.reduce((acc, cur) => acc + cur, 0) + ) + expect(sum.value).toBe(0) + shallowReactiveArr[0] = 1 + expect(sum.value).toBe(1) + }) + }) + + test('should shallowReactive non-observable values', () => { + const assertValue = (value: any) => { + expect(shallowReactive(value)).toBe(value) + } + + // number + assertValue(1) + // string + assertValue('foo') + // boolean + assertValue(false) + // null + assertValue(null) + // undefined + assertValue(undefined) + // symbol + const s = Symbol() + assertValue(s) + + expect(warn).toBeCalledTimes(6) + expect( + warn.mock.calls.map((call) => { + expect(call[0]).toBe( + '[Vue warn]: "shallowReactive()" must be called on an object.' + ) + }) + ) + warn.mockReset() + }) +}) diff --git a/test/v3/reactivity/readonly.spec.ts b/test/v3/reactivity/readonly.spec.ts new file mode 100644 index 00000000..2f371884 --- /dev/null +++ b/test/v3/reactivity/readonly.spec.ts @@ -0,0 +1,498 @@ +import { mockWarn } from '../../helpers/mockWarn' +import { + shallowReadonly, + isReactive, + ref, + reactive, + watch, + nextTick, + readonly, + isReadonly, + toRaw, + computed, +} from '../../../src' + +const Vue = require('vue/dist/vue.common.js') + +// /** +// * @see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html +// */ +// type Writable = { -readonly [P in keyof T]: T[P] } + +describe('reactivity/readonly', () => { + mockWarn(true) + + // describe('Object', () => { + // it('should make nested values readonly', () => { + // const original = { foo: 1, bar: { baz: 2 } } + // const wrapped = readonly(original) + // expect(wrapped).not.toBe(original) + // expect(isProxy(wrapped)).toBe(true) + // expect(isReactive(wrapped)).toBe(false) + // expect(isReadonly(wrapped)).toBe(true) + // expect(isReactive(original)).toBe(false) + // expect(isReadonly(original)).toBe(false) + // expect(isReactive(wrapped.bar)).toBe(false) + // expect(isReadonly(wrapped.bar)).toBe(true) + // expect(isReactive(original.bar)).toBe(false) + // expect(isReadonly(original.bar)).toBe(false) + // // get + // expect(wrapped.foo).toBe(1) + // // has + // expect('foo' in wrapped).toBe(true) + // // ownKeys + // expect(Object.keys(wrapped)).toEqual(['foo', 'bar']) + // }) + + // it('should not allow mutation', () => { + // const qux = Symbol('qux') + // const original = { + // foo: 1, + // bar: { + // baz: 2, + // }, + // [qux]: 3, + // } + // const wrapped: Writable = readonly(original) + + // wrapped.foo = 2 + // expect(wrapped.foo).toBe(1) + // expect( + // `Set operation on key "foo" failed: target is readonly.` + // ).toHaveBeenWarnedLast() + + // wrapped.bar.baz = 3 + // expect(wrapped.bar.baz).toBe(2) + // expect( + // `Set operation on key "baz" failed: target is readonly.` + // ).toHaveBeenWarnedLast() + + // wrapped[qux] = 4 + // expect(wrapped[qux]).toBe(3) + // expect( + // `Set operation on key "Symbol(qux)" failed: target is readonly.` + // ).toHaveBeenWarnedLast() + + // delete wrapped.foo + // expect(wrapped.foo).toBe(1) + // expect( + // `Delete operation on key "foo" failed: target is readonly.` + // ).toHaveBeenWarnedLast() + + // delete wrapped.bar.baz + // expect(wrapped.bar.baz).toBe(2) + // expect( + // `Delete operation on key "baz" failed: target is readonly.` + // ).toHaveBeenWarnedLast() + + // delete wrapped[qux] + // expect(wrapped[qux]).toBe(3) + // expect( + // `Delete operation on key "Symbol(qux)" failed: target is readonly.` + // ).toHaveBeenWarnedLast() + // }) + + // it('should not trigger effects', () => { + // const wrapped: any = readonly({ a: 1 }) + // let dummy + // effect(() => { + // dummy = wrapped.a + // }) + // expect(dummy).toBe(1) + // wrapped.a = 2 + // expect(wrapped.a).toBe(1) + // expect(dummy).toBe(1) + // expect(`target is readonly`).toHaveBeenWarned() + // }) + // }) + + // describe('Array', () => { + // it('should make nested values readonly', () => { + // const original = [{ foo: 1 }] + // const wrapped = readonly(original) + // expect(wrapped).not.toBe(original) + // expect(isProxy(wrapped)).toBe(true) + // expect(isReactive(wrapped)).toBe(false) + // expect(isReadonly(wrapped)).toBe(true) + // expect(isReactive(original)).toBe(false) + // expect(isReadonly(original)).toBe(false) + // expect(isReactive(wrapped[0])).toBe(false) + // expect(isReadonly(wrapped[0])).toBe(true) + // expect(isReactive(original[0])).toBe(false) + // expect(isReadonly(original[0])).toBe(false) + // // get + // expect(wrapped[0].foo).toBe(1) + // // has + // expect(0 in wrapped).toBe(true) + // // ownKeys + // expect(Object.keys(wrapped)).toEqual(['0']) + // }) + + // it('should not allow mutation', () => { + // const wrapped: any = readonly([{ foo: 1 }]) + // wrapped[0] = 1 + // expect(wrapped[0]).not.toBe(1) + // expect( + // `Set operation on key "0" failed: target is readonly.` + // ).toHaveBeenWarned() + // wrapped[0].foo = 2 + // expect(wrapped[0].foo).toBe(1) + // expect( + // `Set operation on key "foo" failed: target is readonly.` + // ).toHaveBeenWarned() + + // // should block length mutation + // wrapped.length = 0 + // expect(wrapped.length).toBe(1) + // expect(wrapped[0].foo).toBe(1) + // expect( + // `Set operation on key "length" failed: target is readonly.` + // ).toHaveBeenWarned() + + // // mutation methods invoke set/length internally and thus are blocked as well + // wrapped.push(2) + // expect(wrapped.length).toBe(1) + // // push triggers two warnings on [1] and .length + // expect(`target is readonly.`).toHaveBeenWarnedTimes(5) + // }) + + // it('should not trigger effects', () => { + // const wrapped: any = readonly([{ a: 1 }]) + // let dummy + // effect(() => { + // dummy = wrapped[0].a + // }) + // expect(dummy).toBe(1) + // wrapped[0].a = 2 + // expect(wrapped[0].a).toBe(1) + // expect(dummy).toBe(1) + // expect(`target is readonly`).toHaveBeenWarnedTimes(1) + // wrapped[0] = { a: 2 } + // expect(wrapped[0].a).toBe(1) + // expect(dummy).toBe(1) + // expect(`target is readonly`).toHaveBeenWarnedTimes(2) + // }) + // }) + + // const maps = [Map, WeakMap] + // maps.forEach((Collection: any) => { + // describe(Collection.name, () => { + // test('should make nested values readonly', () => { + // const key1 = {} + // const key2 = {} + // const original = new Collection([ + // [key1, {}], + // [key2, {}], + // ]) + // const wrapped = readonly(original) + // expect(wrapped).not.toBe(original) + // expect(isProxy(wrapped)).toBe(true) + // expect(isReactive(wrapped)).toBe(false) + // expect(isReadonly(wrapped)).toBe(true) + // expect(isReactive(original)).toBe(false) + // expect(isReadonly(original)).toBe(false) + // expect(isReactive(wrapped.get(key1))).toBe(false) + // expect(isReadonly(wrapped.get(key1))).toBe(true) + // expect(isReactive(original.get(key1))).toBe(false) + // expect(isReadonly(original.get(key1))).toBe(false) + // }) + + // test('should not allow mutation & not trigger effect', () => { + // const map = readonly(new Collection()) + // const key = {} + // let dummy + // effect(() => { + // dummy = map.get(key) + // }) + // expect(dummy).toBeUndefined() + // map.set(key, 1) + // expect(dummy).toBeUndefined() + // expect(map.has(key)).toBe(false) + // expect( + // `Set operation on key "${key}" failed: target is readonly.` + // ).toHaveBeenWarned() + // }) + + // if (Collection === Map) { + // test('should retrieve readonly values on iteration', () => { + // const key1 = {} + // const key2 = {} + // const original = new Collection([ + // [key1, {}], + // [key2, {}], + // ]) + // const wrapped: any = readonly(original) + // expect(wrapped.size).toBe(2) + // for (const [key, value] of wrapped) { + // expect(isReadonly(key)).toBe(true) + // expect(isReadonly(value)).toBe(true) + // } + // wrapped.forEach((value: any) => { + // expect(isReadonly(value)).toBe(true) + // }) + // for (const value of wrapped.values()) { + // expect(isReadonly(value)).toBe(true) + // } + // }) + // } + // }) + // }) + + // const sets = [Set, WeakSet] + // sets.forEach((Collection: any) => { + // describe(Collection.name, () => { + // test('should make nested values readonly', () => { + // const key1 = {} + // const key2 = {} + // const original = new Collection([key1, key2]) + // const wrapped = readonly(original) + // expect(wrapped).not.toBe(original) + // expect(isProxy(wrapped)).toBe(true) + // expect(isReactive(wrapped)).toBe(false) + // expect(isReadonly(wrapped)).toBe(true) + // expect(isReactive(original)).toBe(false) + // expect(isReadonly(original)).toBe(false) + // expect(wrapped.has(reactive(key1))).toBe(true) + // expect(original.has(reactive(key1))).toBe(false) + // }) + + // test('should not allow mutation & not trigger effect', () => { + // const set = readonly(new Collection()) + // const key = {} + // let dummy + // effect(() => { + // dummy = set.has(key) + // }) + // expect(dummy).toBe(false) + // set.add(key) + // expect(dummy).toBe(false) + // expect(set.has(key)).toBe(false) + // expect( + // `Add operation on key "${key}" failed: target is readonly.` + // ).toHaveBeenWarned() + // }) + + // if (Collection === Set) { + // test('should retrieve readonly values on iteration', () => { + // const original = new Collection([{}, {}]) + // const wrapped: any = readonly(original) + // expect(wrapped.size).toBe(2) + // for (const value of wrapped) { + // expect(isReadonly(value)).toBe(true) + // } + // wrapped.forEach((value: any) => { + // expect(isReadonly(value)).toBe(true) + // }) + // for (const value of wrapped.values()) { + // expect(isReadonly(value)).toBe(true) + // } + // for (const [v1, v2] of wrapped.entries()) { + // expect(isReadonly(v1)).toBe(true) + // expect(isReadonly(v2)).toBe(true) + // } + // }) + // } + // }) + // }) + + test('calling reactive on an readonly should return readonly', () => { + const a = readonly({}) + const b = reactive(a) + expect(isReadonly(b)).toBe(true) + // should point to same original + expect(toRaw(a)).toBe(toRaw(b)) + }) + + test('calling readonly on a reactive object should return readonly', () => { + const a = reactive({}) + const b = readonly(a) + expect(isReadonly(b)).toBe(true) + // should point to same original + expect(toRaw(a)).toBe(toRaw(b)) + }) + + // test('readonly should track and trigger if wrapping reactive original', () => { + // const a = reactive({ n: 1 }) + // const b = readonly(a) + // // should return true since it's wrapping a reactive source + // expect(isReactive(b)).toBe(true) + + // let dummy + // effect(() => { + // dummy = b.n + // }) + // expect(dummy).toBe(1) + // a.n++ + // expect(b.n).toBe(2) + // expect(dummy).toBe(2) + // }) + + test('wrapping already wrapped value should return same Proxy', () => { + const original = { foo: 1 } + const wrapped = readonly(original) + const wrapped2 = readonly(wrapped) + expect(wrapped2).toBe(wrapped) + }) + + test('wrapping the same value multiple times should return same Proxy', () => { + const original = { foo: 1 } + const wrapped = readonly(original) + const wrapped2 = readonly(original) + expect(wrapped2).toBe(wrapped) + }) + + // test('markRaw', () => { + // const obj = readonly({ + // foo: { a: 1 }, + // bar: markRaw({ b: 2 }), + // }) + // expect(isReadonly(obj.foo)).toBe(true) + // expect(isReactive(obj.bar)).toBe(false) + // }) + + // test('should make ref readonly', () => { + // const n: any = readonly(ref(1)) + // n.value = 2 + // expect(n.value).toBe(1) + // expect( + // `Set operation on key "value" failed: target is readonly.` + // ).toHaveBeenWarned() + // }) + + describe('shallowReadonly', () => { + test('should not make non-reactive properties reactive', () => { + const props = shallowReadonly({ n: { foo: 1 } }) + expect(isReactive(props.n)).toBe(false) + }) + + test('should make root level properties readonly', () => { + const props = shallowReadonly({ n: 1 }) + // @ts-ignore + props.n = 2 + expect(props.n).toBe(1) + expect( + `Set operation on key "n" failed: target is readonly.` + ).toHaveBeenWarned() + }) + + // to retain 2.x behavior. + test('should NOT make nested properties readonly', () => { + const props = shallowReadonly({ n: { foo: 1 } }) + // @ts-ignore + props.n.foo = 2 + expect(props.n.foo).toBe(2) + expect( + `Set operation on key "foo" failed: target is readonly.` + ).not.toHaveBeenWarned() + }) + + test('should not process non-object data', () => { + // @ts-ignore + shallowReadonly(25) + expect(`value cannot be made reactive: 25`).toHaveBeenWarned() + // @ts-ignore + readonly(25) + expect(`value cannot be made reactive: 25`).toHaveBeenWarned() + }) + + // #669 + test('shallowReadonly should work for refs', () => { + const vm = new Vue({ + template: '
{{ count }} {{ countRef }}
', + setup() { + const count = reactive({ number: 0 }) + const countRef = ref(0) + + const countReadonly = shallowReadonly(count) + const countRefReadonly = shallowReadonly(countRef) + + setTimeout(() => { + // @ts-expect-error + countReadonly.number++ + // @ts-expect-error + countRefReadonly.value++ + }, 1000) + return { + count, + countRef, + } + }, + }).$mount() + + expect(vm.$el.textContent).toBe(`{\n "number": 0\n} 0`) + }) + + // #712 + test('watch should work for ref with shallowReadonly', async () => { + const cb = vi.fn() + const vm = new Vue({ + template: '
{{ countRef }}
', + setup() { + const countRef = ref(0) + const countRefReadonly = shallowReadonly(countRef) + watch(countRefReadonly, cb, { deep: true }) + return { + countRef, + countRefReadonly, + } + }, + }).$mount() + + vm.countRef++ + await nextTick() + expect(cb).toHaveBeenCalled() + + vm.countRefReadonly++ + await nextTick() + expect( + `Set operation on key "value" failed: target is readonly.` + ).toHaveBeenWarned() + expect(vm.$el.textContent).toBe(`1`) + }) + + // #712 + test('watch should work for reactive with shallowReadonly', async () => { + const cb = vi.fn() + const vm = new Vue({ + template: '
{{ count.number }}
', + setup() { + const count = reactive({ number: 0 }) + const countReadonly = shallowReadonly(count) + watch(countReadonly, cb, { deep: true }) + return { + count, + countReadonly, + } + }, + }).$mount() + + vm.count.number++ + await nextTick() + expect(cb).toHaveBeenCalled() + + vm.countReadonly.number++ + await nextTick() + expect( + `Set operation on key "number" failed: target is readonly.` + ).toHaveBeenWarned() + expect(vm.$el.textContent).toBe(`1`) + }) + + it('should mark computed as readonly', () => { + expect(isReadonly(computed(() => {}))).toBe(true) + expect( + isReadonly( + computed({ + get: () => {}, + set: () => {}, + }) + ) + ).toBe(false) + }) + + // #811 + it('should not mark ref as readonly', () => { + expect(isReadonly(ref([]))).toBe(false) + }) + }) +}) diff --git a/test/v3/reactivity/ref.spec.ts b/test/v3/reactivity/ref.spec.ts new file mode 100644 index 00000000..1443f922 --- /dev/null +++ b/test/v3/reactivity/ref.spec.ts @@ -0,0 +1,427 @@ +import { + ref, + customRef, + reactive, + isRef, + toRef, + toRefs, + Ref, + computed, + triggerRef, + watchEffect, + unref, + isReactive, + shallowRef, + proxyRefs, + ShallowUnwrapRef, +} from '../../../src' +import { RefKey } from '../../../src/utils/symbols' + +describe('reactivity/ref', () => { + it('should hold a value', () => { + const a = ref(1) + expect(a.value).toBe(1) + a.value = 2 + expect(a.value).toBe(2) + }) + + it('should be reactive', () => { + const a = ref(1) + let dummy + let calls = 0 + watchEffect( + () => { + calls++ + dummy = a.value + }, + { flush: 'sync' } + ) + expect(calls).toBe(1) + expect(dummy).toBe(1) + + a.value = 2 + expect(calls).toBe(2) + expect(dummy).toBe(2) + // same value should not trigger + a.value = 2 + expect(calls).toBe(2) + expect(dummy).toBe(2) + }) + + it('should make nested properties reactive', () => { + const a = ref({ + count: 1, + }) + let dummy + watchEffect( + () => { + dummy = a.value.count + }, + { flush: 'sync' } + ) + expect(dummy).toBe(1) + a.value.count = 2 + expect(dummy).toBe(2) + }) + + it('should work without initial value', () => { + const a = ref() + let dummy + watchEffect( + () => { + dummy = a.value + }, + { flush: 'sync' } + ) + expect(dummy).toBe(undefined) + a.value = 2 + expect(dummy).toBe(2) + }) + + it('should work like a normal property when nested in a reactive object', () => { + const a = ref(1) + const obj = reactive({ + a, + b: { + c: a, + }, + }) + + let dummy1: number + let dummy2: number + + watchEffect( + () => { + dummy1 = obj.a + dummy2 = obj.b.c + }, + { flush: 'sync' } + ) + + const assertDummiesEqualTo = (val: number) => + [dummy1, dummy2].forEach((dummy) => expect(dummy).toBe(val)) + + assertDummiesEqualTo(1) + a.value++ + assertDummiesEqualTo(2) + obj.a++ + assertDummiesEqualTo(3) + obj.b.c++ + assertDummiesEqualTo(4) + }) + + it('should unwrap nested ref in types', () => { + const a = ref(0) + const b = ref(a) + + expect(typeof (b.value + 1)).toBe('number') + }) + + it('should unwrap nested values in types', () => { + const a = { + b: ref(0), + } + + const c = ref(a) + + expect(typeof (c.value.b + 1)).toBe('number') + }) + + it('should NOT unwrap ref types nested inside arrays', () => { + const arr = ref([1, ref(1)]).value + ;(arr[0] as number)++ + ;(arr[1] as Ref).value++ + + const arr2 = ref([1, new Map(), ref('1')]).value + const value = arr2[0] + if (isRef(value)) { + value + 'foo' + } else if (typeof value === 'number') { + value + 1 + } else { + // should narrow down to Map type + // and not contain any Ref type + value.has('foo') + } + }) + + it('should keep tuple types', () => { + const tuple: [number, string, { a: number }, () => number, Ref] = [ + 0, + '1', + { a: 1 }, + () => 0, + ref(0), + ] + const tupleRef = ref(tuple) + + tupleRef.value[0]++ + expect(tupleRef.value[0]).toBe(1) + tupleRef.value[1] += '1' + expect(tupleRef.value[1]).toBe('11') + tupleRef.value[2].a++ + expect(tupleRef.value[2].a).toBe(2) + expect(tupleRef.value[3]()).toBe(0) + tupleRef.value[4].value++ + expect(tupleRef.value[4].value).toBe(1) + }) + + it('should keep symbols', () => { + const customSymbol = Symbol() + const obj = { + [Symbol.asyncIterator]: ref(1), + [Symbol.hasInstance]: { a: ref('a') }, + [Symbol.isConcatSpreadable]: { b: ref(true) }, + [Symbol.iterator]: [ref(1)], + [Symbol.match]: new Set>(), + [Symbol.matchAll]: new Map>(), + [Symbol.replace]: { arr: [ref('a')] }, + [Symbol.search]: { set: new Set>() }, + [Symbol.species]: { map: new Map>() }, + [Symbol.split]: new WeakSet>(), + [Symbol.toPrimitive]: new WeakMap, string>(), + [Symbol.toStringTag]: { weakSet: new WeakSet>() }, + [Symbol.unscopables]: { weakMap: new WeakMap, string>() }, + [customSymbol]: { arr: [ref(1)] }, + } + + const objRef = ref(obj) + + const keys: (keyof typeof obj)[] = [ + Symbol.asyncIterator, + Symbol.hasInstance, + Symbol.isConcatSpreadable, + Symbol.iterator, + Symbol.match, + Symbol.matchAll, + Symbol.replace, + Symbol.search, + Symbol.species, + Symbol.split, + Symbol.toPrimitive, + Symbol.toStringTag, + Symbol.unscopables, + customSymbol, + ] + + keys.forEach((key) => { + expect(objRef.value[key]).toStrictEqual(obj[key]) + }) + }) + + test('unref', () => { + expect(unref(1)).toBe(1) + expect(unref(ref(1))).toBe(1) + }) + + test('shallowRef', () => { + const sref = shallowRef({ a: 1 }) + expect(isReactive(sref.value)).toBe(false) + + let dummy + watchEffect( + () => { + dummy = sref.value.a + }, + { flush: 'sync' } + ) + expect(dummy).toBe(1) + + sref.value = { a: 2 } + expect(isReactive(sref.value)).toBe(false) + expect(dummy).toBe(2) + + sref.value.a = 3 + expect(dummy).toBe(2) + }) + + test('shallowRef force trigger', () => { + const sref = shallowRef({ a: 1 }) + let dummy + watchEffect( + () => { + dummy = sref.value.a + }, + { flush: 'sync' } + ) + expect(dummy).toBe(1) + + sref.value.a = 2 + expect(dummy).toBe(1) // should not trigger yet + + // force trigger + triggerRef(sref) + expect(dummy).toBe(2) + }) + + test('shallowRef noop when assignment is the same', () => { + const sref = shallowRef(1) + let calls = 0 + watchEffect( + () => { + sref.value + calls++ + }, + { flush: 'sync' } + ) + expect(calls).toBe(1) + + sref.value = 2 + expect(calls).toBe(2) + + sref.value = 2 + expect(calls).toBe(2) + }) + + test('isRef', () => { + expect(isRef(ref(1))).toBe(true) + expect(isRef(computed(() => 1))).toBe(true) + + expect(isRef(0)).toBe(false) + expect(isRef(1)).toBe(false) + // an object that looks like a ref isn't necessarily a ref + expect(isRef({ value: 0 })).toBe(false) + }) + + test('toRef', () => { + const a = reactive({ + x: 1, + }) + const x = toRef(a, 'x') + expect(isRef(x)).toBe(true) + expect(x.value).toBe(1) + + // source -> proxy + a.x = 2 + expect(x.value).toBe(2) + + // proxy -> source + x.value = 3 + expect(a.x).toBe(3) + + // reactivity + let dummyX + watchEffect( + () => { + dummyX = x.value + }, + { flush: 'sync' } + ) + expect(dummyX).toBe(x.value) + + // mutating source should trigger watchEffect using the proxy refs + a.x = 4 + expect(dummyX).toBe(4) + }) + + test('toRefs', () => { + const a = reactive({ + x: 1, + y: 2, + }) + + const { x, y } = toRefs(a) + + expect(isRef(x)).toBe(true) + expect(isRef(y)).toBe(true) + expect(x.value).toBe(1) + expect(y.value).toBe(2) + + // source -> proxy + a.x = 2 + a.y = 3 + expect(x.value).toBe(2) + expect(y.value).toBe(3) + + // proxy -> source + x.value = 3 + y.value = 4 + expect(a.x).toBe(3) + expect(a.y).toBe(4) + + // reactivity + let dummyX, dummyY + watchEffect( + () => { + dummyX = x.value + dummyY = y.value + }, + { flush: 'sync' } + ) + expect(dummyX).toBe(x.value) + expect(dummyY).toBe(y.value) + + // mutating source should trigger watchEffect using the proxy refs + a.x = 4 + a.y = 5 + expect(dummyX).toBe(4) + expect(dummyY).toBe(5) + }) + + test('customRef', () => { + let value = 1 + let _trigger: () => void + + const custom = customRef((track, trigger) => ({ + get() { + track() + return value + }, + set(newValue: number) { + value = newValue + _trigger = trigger + }, + })) + + expect(isRef(custom)).toBe(true) + + let dummy + watchEffect( + () => { + dummy = custom.value + }, + { flush: 'sync' } + ) + expect(dummy).toBe(1) + + custom.value = 2 + // should not trigger yet + expect(dummy).toBe(1) + + _trigger!() + expect(dummy).toBe(2) + }) + + test('proxyRefs', () => { + const a = { + x: ref(1), + obj: { + y: ref('foo'), + }, + } + const reactiveA = { + [RefKey]: a, + ...a, + } + const p = proxyRefs(a) as ShallowUnwrapRef + expect(p.x).toBe(1) + expect(p.obj.y).toBe('foo') + expect(p[RefKey]).toBe(a) + expect(Object.keys(p)).toStrictEqual(['x', 'obj']) + + // @ts-expect-error + p.obj.y = 'bar' + p.x = 2 + expect(a.x).toBe(2) + expect(a.obj.y).toBe('bar') + expect(p[RefKey]).toBe(a) + expect(Object.keys(p)).toStrictEqual(['x', 'obj']) + + const r = reactive({ k: 'v' }) + const s = proxyRefs(r) + expect(s.k).toBe('v') + + r.k = 'k' + expect(s.k).toBe('k') + }) +}) diff --git a/test/v3/reactivity/set.spec.ts b/test/v3/reactivity/set.spec.ts new file mode 100644 index 00000000..c6522c08 --- /dev/null +++ b/test/v3/reactivity/set.spec.ts @@ -0,0 +1,37 @@ +import { set, reactive, ref, watch, watchEffect } from '../../../src' + +describe('reactivity/set', () => { + it('should not trigger reactivity on native object member assignment', () => { + const obj = reactive<{ a?: number }>({}) + const spy = vi.fn() + watch(obj, spy, { deep: true, flush: 'sync' }) + obj.a = 1 + expect(spy).not.toHaveBeenCalled() + expect(obj).toStrictEqual({ a: 1 }) + }) + + it('should trigger reactivity when using set on reactive object', () => { + const obj = reactive<{ a?: number }>({}) + const spy = vi.fn() + watch(obj, spy, { deep: true, flush: 'sync' }) + set(obj, 'a', 1) + expect(spy).toBeCalledTimes(1) + expect(obj).toStrictEqual({ a: 1 }) + }) + + it('should trigger watchEffect when using set to change value of array length', () => { + const arr = ref([1, 2, 3]) + const spy = vi.fn() + watchEffect( + () => { + spy(arr.value) + }, + { flush: 'sync' } + ) + + expect(spy).toHaveBeenCalledTimes(1) + set(arr.value, 'length', 1) + expect(arr.value.length).toBe(1) + expect(spy).toHaveBeenCalledTimes(2) + }) +}) diff --git a/test/v3/runtime-core/apiAsyncComponent.spec.ts b/test/v3/runtime-core/apiAsyncComponent.spec.ts new file mode 100644 index 00000000..dbc54d04 --- /dev/null +++ b/test/v3/runtime-core/apiAsyncComponent.spec.ts @@ -0,0 +1,655 @@ +import { + h, + createApp, + defineAsyncComponent, + nextTick, + ref, + defineComponent, +} from '../../../src' + +const resolveComponent = { + render() { + return h('p', 'resolved') + }, +} + +const loadingComponent = { + render() { + return h('p', 'loading') + }, +} + +const errorComponent = defineComponent({ + render() { + return h('p', 'errored out') + }, +}) + +const timeout = (n: number = 0) => new Promise((r) => setTimeout(r, n)) + +describe('api: defineAsyncComponent', () => { + test('simple usage', async () => { + const Foo = defineAsyncComponent(() => + Promise.resolve().then(() => resolveComponent) + ) + + const toggle = ref(true) + + const app = createApp({ + render: () => (toggle.value ? h(Foo) : null), + }) + + const vm = app.mount() + + expect(vm.$el.textContent).toBe('') + + // first time resolve, wait for macro task since there are multiple + // microtasks / .then() calls + await timeout() + expect(vm.$el.textContent).toBe('resolved') + + toggle.value = false + await nextTick() + expect(vm.$el.textContent).toBe('') + + // already resolved component should update on nextTick + toggle.value = true + await nextTick() + expect(vm.$el.textContent).toBe('resolved') + }) + + test('with loading component', async () => { + let resolve: (cmp: any) => void + const Foo = defineAsyncComponent({ + loader: () => + new Promise((res) => { + resolve = res + }), + loadingComponent, + delay: 1, // defaults to 200 + }) + + const toggle = ref(true) + + const app = createApp({ + render: () => (toggle.value ? h(Foo) : null), + }) + + const vm = app.mount() + + // due to the delay, initial mount should be empty + expect(vm.$el.textContent).toBe('') + + // loading show up after delay + await timeout(2) + expect(vm.$el.textContent).toBe('loading') + + resolve!(resolveComponent) + await timeout() + expect(vm.$el.textContent).toBe('resolved') + + toggle.value = false + await nextTick() + expect(vm.$el.textContent).toBe('') + + // already resolved component should update on nextTick without loading + // state + toggle.value = true + await nextTick() + expect(vm.$el.textContent).toBe('resolved') + }) + + test('with loading component + explicit delay (0)', async () => { + let resolve: (comp: any) => void + const Foo = defineAsyncComponent({ + loader: () => + new Promise((r) => { + resolve = r as any + }), + loadingComponent, + delay: 0, + }) + + const toggle = ref(true) + + const app = createApp({ + render: () => (toggle.value ? h(Foo) : null), + }) + const vm = app.mount() + + // with delay: 0, should show loading immediately + expect(vm.$el.textContent).toBe('loading') + + resolve!(resolveComponent) + await timeout() + expect(vm.$el.textContent).toBe('resolved') + + toggle.value = false + await nextTick() + expect(vm.$el.textContent).toBe('') + + // already resolved component should update on nextTick without loading + // state + toggle.value = true + await nextTick() + expect(vm.$el.textContent).toBe('resolved') + }) + + test('error without error component', async () => { + // let resolve: (comp: any) => void + let reject: (e: Error) => void + const Foo = defineAsyncComponent( + () => + new Promise((_resolve, _reject) => { + // resolve = _resolve as any + reject = _reject + }) + ) + + const toggle = ref(true) + const app = createApp({ + render: () => (toggle.value ? h(Foo) : null), + }) + + const handler = vi + .spyOn(global.console, 'error') + .mockImplementation(() => null) + + const vm = app.mount() + expect(vm.$el.textContent).toBe('') + + const err = new Error('foo') + reject!(err) + await timeout() + expect(handler).toHaveBeenCalled() + expect(handler.mock.calls[0][0]).toContain(err.message) + expect(vm.$el.textContent).toBe('') + + toggle.value = false + await nextTick() + expect(vm.$el.textContent).toBe('') + + // This retry method doesn't work in Vue2 + + // errored out on previous load, toggle and mock success this time + // toggle.value = true + // await nextTick() + // expect(vm.$el.textContent).toBe('') + + // // should render this time + // resolve!(resolveComponent) + // await timeout() + // expect(vm.$el.textContent).toBe('resolved') + }) + + test('error with error component', async () => { + // let resolve: (comp: any) => void + let reject: (e: Error) => void + const Foo = defineAsyncComponent({ + loader: () => + new Promise((_resolve, _reject) => { + // resolve = _resolve as any + reject = _reject + }), + errorComponent, + }) + + const toggle = ref(true) + + const app = createApp({ + render: () => (toggle.value ? h(Foo) : null), + }) + + const handler = vi + .spyOn(global.console, 'error') + .mockImplementation(() => null) + + const vm = app.mount() + expect(vm.$el.textContent).toBe('') + + const err = new Error('errored out') + reject!(err) + await timeout(1) + expect(handler).toHaveBeenCalled() + expect(vm.$el.textContent).toBe('errored out') + + toggle.value = false + await nextTick() + expect(vm.$el.textContent).toBe('') + + // This doesn't work in vue2 + // // errored out on previous load, toggle and mock success this time + // toggle.value = true + // await nextTick() + // expect(vm.$el.textContent).toBe('') + + // // should render this time + // resolve!(resolveComponent) + // await timeout() + // expect(vm.$el.textContent).toBe('resolved') + }) + + // #2129 + test('error with error component, without global handler', async () => { + // let resolve: (comp: any) => void + let reject: (e: Error) => void + const Foo = defineAsyncComponent({ + loader: () => + new Promise((_resolve, _reject) => { + // resolve = _resolve as any + reject = _reject + }), + errorComponent, + }) + + const toggle = ref(true) + const app = createApp({ + render: () => (toggle.value ? h(Foo) : null), + }) + + vi.spyOn(global.console, 'error').mockImplementation(() => null) + + const vm = app.mount() + expect(vm.$el.textContent).toBe('') + + const err = new Error('errored out') + reject!(err) + await timeout() + expect(vm.$el.textContent).toBe('errored out') + // TODO: Warning check? + // expect( + // 'Unhandled error during execution of async component loader' + // ).toHaveBeenWarned() + + toggle.value = false + await nextTick() + expect(vm.$el.textContent).toBe('') + + // this doesn't work in vue2 + // // errored out on previous load, toggle and mock success this time + // toggle.value = true + // await nextTick() + // expect(vm.$el.textContent).toBe('') + + // // should render this time + // resolve!(resolveComponent) + // await timeout() + // expect(vm.$el.textContent).toBe('resolved') + }) + + test('error with error + loading components', async () => { + // let resolve: (comp: any) => void + let reject: (e: Error) => void + const Foo = defineAsyncComponent({ + loader: () => + new Promise((_resolve, _reject) => { + // resolve = _resolve as any + reject = _reject + }), + errorComponent, + loadingComponent, + delay: 1, + }) + + const toggle = ref(true) + const app = createApp({ + render: () => (toggle.value ? h(Foo) : null), + }) + + const handler = vi + .spyOn(global.console, 'error') + .mockImplementation(() => null) + + const vm = app.mount() + + // due to the delay, initial mount should be empty + expect(vm.$el.textContent).toBe('') + + // loading show up after delay + await timeout(1) + expect(vm.$el.textContent).toBe('loading') + + const err = new Error('errored out') + reject!(err) + await timeout() + expect(handler).toHaveBeenCalled() + expect(vm.$el.textContent).toBe('errored out') + + toggle.value = false + await nextTick() + expect(vm.$el.textContent).toBe('') + + // Not in vue2 + // // errored out on previous load, toggle and mock success this time + // toggle.value = true + // await nextTick() + // expect(vm.$el.textContent).toBe('') + + // // loading show up after delay + // await timeout(1) + // expect(vm.$el.textContent).toBe('loading') + + // // should render this time + // resolve!(resolveComponent) + // await timeout() + // expect(vm.$el.textContent).toBe('resolved') + }) + + test('timeout without error component', async () => { + let resolve: (comp: any) => void + const Foo = defineAsyncComponent({ + loader: () => + new Promise((_resolve) => { + resolve = _resolve as any + }), + timeout: 1, + }) + + const app = createApp({ + render: () => h(Foo), + }) + + const handler = vi + .spyOn(global.console, 'error') + .mockImplementation(() => null) + + const vm = app.mount() + expect(vm.$el.textContent).toBe('') + + await timeout(1) + expect(handler).toHaveBeenCalled() + // expect(handler.mock.calls[0][0].message).toContain( + // `Async component timed out after 1ms.` + // ) + expect(vm.$el.textContent).toBe('') + + // if it resolved after timeout, should still work + resolve!(resolveComponent) + await timeout() + expect(vm.$el.textContent).toBe('resolved') + }) + + test('timeout with error component', async () => { + // let resolve: (comp: any) => void + const Foo = defineAsyncComponent({ + loader: () => + new Promise((_resolve) => { + // resolve = _resolve as any + }), + timeout: 1, + errorComponent, + }) + + const app = createApp({ + render: () => h(Foo), + }) + + const handler = vi + .spyOn(global.console, 'error') + .mockImplementation(() => null) + + const vm = app.mount() + expect(vm.$el.textContent).toBe('') + + await timeout(1) + expect(handler).toHaveBeenCalled() + expect(vm.$el.textContent).toBe('errored out') + + // Not in vue2 + // // if it resolved after timeout, should still work + // resolve!(resolveComponent) + // await timeout() + // expect(vm.$el.textContent).toBe('resolved') + }) + + test('timeout with error + loading components', async () => { + // let resolve: (comp: any) => void + const Foo = defineAsyncComponent({ + loader: () => + new Promise((_resolve) => { + // resolve = _resolve as any + }), + delay: 1, + timeout: 16, + errorComponent, + loadingComponent, + }) + + const app = createApp({ + render: () => h(Foo), + }) + const handler = vi + .spyOn(global.console, 'error') + .mockImplementation(() => null) + const vm = app.mount() + + expect(vm.$el.textContent).toBe('') + await timeout(1) + expect(vm.$el.textContent).toBe('loading') + + await timeout(16) + expect(vm.$el.textContent).toBe('errored out') + expect(handler).toHaveBeenCalled() + + // Not in Vue2 + // resolve!(resolveComponent) + // await timeout() + // expect(vm.$el.textContent).toBe('resolved') + }) + + test('timeout without error component, but with loading component', async () => { + let resolve: (comp: any) => void + const Foo = defineAsyncComponent({ + loader: () => + new Promise((_resolve) => { + resolve = _resolve as any + }), + delay: 1, + timeout: 16, + loadingComponent, + }) + + const app = createApp({ + render: () => h(Foo), + }) + const handler = vi + .spyOn(global.console, 'error') + .mockImplementation(() => null) + const vm = app.mount() + expect(vm.$el.textContent).toBe('') + await timeout(1) + expect(vm.$el.textContent).toBe('loading') + + await timeout(16) + expect(handler).toHaveBeenCalled() + // expect(handler.mock.calls[0][0].message).toContain( + // `Async component timed out after 16ms.` + // ) + // should still display loading + expect(vm.$el.textContent).toBe('loading') + + resolve!(resolveComponent) + await timeout() + expect(vm.$el.textContent).toBe('resolved') + }) + + test('retry (success)', async () => { + let loaderCallCount = 0 + let resolve: (comp: any) => void + let reject: (e: Error) => void + + const Foo = defineAsyncComponent({ + loader: () => { + loaderCallCount++ + return new Promise((_resolve, _reject) => { + resolve = _resolve as any + reject = _reject + }) + }, + onError(error, retry, fail) { + if (error.message.match(/foo/)) { + retry() + } else { + fail() + } + }, + }) + + const app = createApp({ + render: () => h(Foo), + }) + + vi.spyOn(global.console, 'error').mockImplementation(() => null) + + const vm = app.mount() + expect(vm.$el.textContent).toBe('') + expect(loaderCallCount).toBe(1) + + const err = new Error('foo') + reject!(err) + await timeout() + // expect(handler).toHaveBeenCalled() + expect(loaderCallCount).toBe(2) + expect(vm.$el.textContent).toBe('') + + // should render this time + resolve!(resolveComponent) + await timeout() + // expect(handler).not.toHaveBeenCalled() + expect(vm.$el.textContent).toBe('resolved') + }) + + test('retry (skipped)', async () => { + let loaderCallCount = 0 + let reject: (e: Error) => void + + const Foo = defineAsyncComponent({ + loader: () => { + loaderCallCount++ + return new Promise((_resolve, _reject) => { + reject = _reject + }) + }, + onError(error, retry, fail) { + if (error.message.match(/bar/)) { + retry() + } else { + fail() + } + }, + }) + + const app = createApp({ + render: () => h(Foo), + }) + + const handler = vi + .spyOn(global.console, 'error') + .mockImplementation(() => null) + const vm = app.mount() + expect(vm.$el.textContent).toBe('') + expect(loaderCallCount).toBe(1) + + const err = new Error('foo') + reject!(err) + await timeout() + // should fail because retryWhen returns false + expect(handler).toHaveBeenCalled() + expect(handler.mock.calls[0][0]).toContain(err.message) + expect(loaderCallCount).toBe(1) + expect(vm.$el.textContent).toBe('') + }) + + test('retry (fail w/ max retry attempts)', async () => { + let loaderCallCount = 0 + let reject: (e: Error) => void + + const Foo = defineAsyncComponent({ + loader: () => { + loaderCallCount++ + return new Promise((_resolve, _reject) => { + reject = _reject + }) + }, + onError(error, retry, fail, attempts) { + if (error.message.match(/foo/) && attempts <= 1) { + retry() + } else { + fail() + } + }, + }) + + const app = createApp({ + render: () => h(Foo), + }) + + const handler = vi + .spyOn(global.console, 'error') + .mockImplementation(() => null) + const vm = app.mount() + expect(vm.$el.textContent).toBe('') + expect(loaderCallCount).toBe(1) + + // first retry + const err = new Error('foo') + reject!(err) + await timeout() + // expect(handler).not.toHaveBeenCalled() + expect(loaderCallCount).toBe(2) + expect(vm.$el.textContent).toBe('') + + // 2nd retry, should fail due to reaching maxRetries + reject!(err) + await timeout() + // expect(handler).toHaveBeenCalled() + expect(handler.mock.calls[0][0]).toContain(err.message) + expect(loaderCallCount).toBe(2) + expect(vm.$el.textContent).toBe('') + }) + + // test('template ref forwarding', async () => { + // let resolve: (comp: Component) => void + // const Foo = defineAsyncComponent( + // () => + // new Promise((r) => { + // resolve = r as any + // }) + // ) + + // const fooRef = ref() + // const toggle = ref(true) + // const root = nodeOps.createElement('div') + // createApp({ + // render: () => (toggle.value ? h(Foo, { ref: fooRef } as any) : null), + // }).mount(root) + + // expect(vm.$el.textContent).toBe('') + // expect(fooRef.value).toBe(null) + + // resolve!({ + // data() { + // return { + // id: 'foo', + // } + // }, + // render: resolveComponent.render, + // }) + // // first time resolve, wait for macro task since there are multiple + // // microtasks / .then() calls + // await timeout() + // expect(vm.$el.textContent).toBe('resolved') + // expect(fooRef.value.id).toBe('foo') + + // toggle.value = false + // await nextTick() + // expect(vm.$el.textContent).toBe('') + // expect(fooRef.value).toBe(null) + + // // already resolved component should update on nextTick + // toggle.value = true + // await nextTick() + // expect(vm.$el.textContent).toBe('resolved') + // expect(fooRef.value.id).toBe('foo') + // }) +}) diff --git a/test/v3/runtime-core/apiInject.spec.ts b/test/v3/runtime-core/apiInject.spec.ts new file mode 100644 index 00000000..99b7cc85 --- /dev/null +++ b/test/v3/runtime-core/apiInject.spec.ts @@ -0,0 +1,307 @@ +import { + h, + provide, + inject, + InjectionKey, + ref, + nextTick, + Ref, + reactive, + defineComponent, + createApp, +} from '../../../src' +import { mockWarn } from '../../helpers' + +describe('api: provide/inject', () => { + mockWarn(true) + it('string keys', async () => { + const Provider = { + setup() { + provide('foo', 1) + return () => h(Middle) + }, + } + + const Middle = { + setup() { + return () => h(Consumer) + }, + } + + const Consumer = { + setup() { + const foo = inject('foo') + return () => h('div', foo as unknown as string) + }, + } + + const root = document.createElement('div') + const vm = createApp(Provider).mount(root) + expect(vm.$el.outerHTML).toBe(`
1
`) + }) + + it('symbol keys', () => { + // also verifies InjectionKey type sync + const key: InjectionKey = Symbol() + + const Provider = { + setup() { + provide(key, 1) + return () => h(Middle) + }, + } + + const Middle = { + setup() { + return () => h(Consumer) + }, + } + + const Consumer = { + setup() { + const foo = inject(key) || 1 + return () => h('div', (foo + 1) as unknown as string) + }, + } + + const root = document.createElement('div') + const vm = createApp(Provider).mount(root) + expect(vm.$el.outerHTML).toBe(`
2
`) + }) + + it('default values', () => { + const Provider = { + setup() { + provide('foo', 'foo') + return () => h(Middle) + }, + } + + const Middle = { + setup() { + return () => h(Consumer) + }, + } + + const Consumer = { + setup() { + // default value should be ignored if value is provided + const foo = inject('foo', 'fooDefault') + // default value should be used if value is not provided + const bar = inject('bar', 'bar') + return () => h('div', (foo + bar) as unknown as string) + }, + } + + const root = document.createElement('div') + const vm = createApp(Provider).mount(root) + expect(vm.$el.outerHTML).toBe(`
foobar
`) + }) + + it('bound to instance', () => { + const Provider = { + setup() { + return () => h(Consumer) + }, + } + + const Consumer = defineComponent({ + name: 'Consumer', + inject: { + foo: { + from: 'foo', + default() { + return this!.$options.name + }, + }, + }, + render() { + return h('div', this.foo as unknown as string) + }, + }) + + const root = document.createElement('div') + const vm = createApp(Provider).mount(root) + expect(vm.$el.outerHTML).toBe(`
Consumer
`) + }) + + it('nested providers', () => { + const ProviderOne = { + setup() { + provide('foo', 'foo') + provide('bar', 'bar') + return () => h(ProviderTwo) + }, + } + + const ProviderTwo = { + setup() { + // override parent value + provide('foo', 'fooOverride') + provide('baz', 'baz') + return () => h(Consumer) + }, + } + + const Consumer = { + setup() { + const foo = inject('foo') + const bar = inject('bar') + const baz = inject('baz') + return () => h('div', [foo, bar, baz].join(',') as unknown as string) + }, + } + + const root = document.createElement('div') + const vm = createApp(ProviderOne).mount(root) + expect(vm.$el.outerHTML).toBe(`
fooOverride,bar,baz
`) + }) + + it('reactivity with refs', async () => { + const count = ref(1) + + const Provider = { + setup() { + provide('count', count) + return () => h(Middle) + }, + } + + const Middle = { + setup() { + return () => h(Consumer) + }, + } + + const Consumer = { + setup() { + const count = inject>('count')! + return () => h('div', count.value as unknown as string) + }, + } + + const root = document.createElement('div') + const vm = createApp(Provider).mount(root) + expect(vm.$el.outerHTML).toBe(`
1
`) + + count.value++ + await nextTick() + expect(vm.$el.outerHTML).toBe(`
2
`) + }) + + it('reactivity with objects', async () => { + const rootState = reactive({ count: 1 }) + + const Provider = { + setup() { + provide('state', rootState) + return () => h(Middle) + }, + } + + const Middle = { + setup() { + return () => h(Consumer) + }, + } + + const Consumer = { + setup() { + const state = inject('state')! + return () => h('div', state.count as unknown as string) + }, + } + + const root = document.createElement('div') + const vm = createApp(Provider).mount(root) + expect(vm.$el.outerHTML).toBe(`
1
`) + + rootState.count++ + await nextTick() + expect(vm.$el.outerHTML).toBe(`
2
`) + }) + + it('should warn unfound', () => { + const Provider = { + setup() { + return () => h(Consumer) + }, + } + + const Consumer = { + setup() { + const foo = inject('foo') + expect(foo).toBeUndefined() + return () => h('div', foo as unknown as string) + }, + } + + const root = document.createElement('div') + const vm = createApp(Provider).mount(root) + expect(vm.$el.outerHTML).toBe(`
`) + expect(`[Vue warn]: Injection "foo" not found.`).toHaveBeenWarned() + }) + + it('should warn unfound w/ injectionKey is undefined', () => { + const Provider = { + setup() { + return () => h(Consumer) + }, + } + + const Consumer = { + setup() { + // The emulation does not use TypeScript + const foo = inject(undefined as unknown as string) + expect(foo).toBeUndefined() + return () => h('div', foo as unknown as string) + }, + } + + const root = document.createElement('div') + const vm = createApp(Provider).mount(root) + expect(vm.$el.outerHTML).toBe(`
`) + expect(`[Vue warn]: injection "undefined" not found.`).toHaveBeenWarned() + }) + + it('should not self-inject', () => { + const Comp = { + setup() { + provide('foo', 'foo') + const injection = inject('foo', null) + return () => h('div', injection as unknown as string) + }, + } + + const root = document.createElement('div') + const vm = createApp(Comp).mount(root) + expect(vm.$el.outerHTML).toBe(`
foo
`) + }) + + it('should not warn when default value is undefined', () => { + const Provider = { + setup() { + provide('foo', undefined) + return () => h(Middle) + }, + } + + const Middle = { + setup() { + return () => h(Consumer) + }, + } + + const Consumer = { + setup() { + const foo = inject('foo') + return () => h('div', foo as unknown as string) + }, + } + + const root = document.createElement('div') + const vm = createApp(Provider).mount(root) + expect(vm.$el.outerHTML).toBe(`
`) + expect(`injection "foo" not found.`).not.toHaveBeenWarned() + }) +}) diff --git a/test/v3/runtime-core/apiLifecycle.spec.ts b/test/v3/runtime-core/apiLifecycle.spec.ts new file mode 100644 index 00000000..ac22a676 --- /dev/null +++ b/test/v3/runtime-core/apiLifecycle.spec.ts @@ -0,0 +1,353 @@ +import { + onBeforeMount, + onMounted, + ref, + h, + onBeforeUpdate, + onUpdated, + onBeforeUnmount, + onUnmounted, + nextTick, + createApp, +} from '../../../src' + +// reference: https://vue-composition-api-rfc.netlify.com/api.html#lifecycle-hooks + +describe('api: lifecycle hooks', () => { + it('onBeforeMount', () => { + const root = document.createElement('div') + const fn = vi.fn(() => { + // should be called before inner div is rendered + expect(root.innerHTML).toBe(``) + }) + + const Comp = { + setup() { + onBeforeMount(fn) + return () => h('div') + }, + } + createApp(Comp).mount(root) + //render(h(Comp), root); + expect(fn).toHaveBeenCalledTimes(1) + }) + + it('onMounted', () => { + const root = document.createElement('div') + const fn = vi.fn(() => { + // should be called after inner div is rendered + expect(root.outerHTML).toBe(`
`) + }) + + const Comp = { + setup() { + onMounted(fn) + return () => h('div') + }, + } + createApp(Comp).mount(root) + //render(h(Comp), root); + expect(fn).toHaveBeenCalledTimes(1) + }) + + it('onBeforeUpdate', async () => { + const count = ref(0) + // const root = document.createElement('div'); + const fn = vi.fn(() => { + // should be called before inner div is updated + expect(vm.$el.outerHTML).toBe(`
0
`) + }) + + const Comp = { + setup() { + onBeforeUpdate(fn) + return () => h('div', count.value as unknown as string) + }, + } + const vm = createApp(Comp).mount() + //render(h(Comp), root); + + count.value = 1 + await nextTick() + expect(fn).toHaveBeenCalledTimes(1) + }) + + it('onUpdated', async () => { + const count = ref(0) + // const root = document.createElement('div'); + const fn = vi.fn(() => { + // should be called after inner div is updated + expect(vm.$el.outerHTML).toBe(`
1
`) + }) + + const Comp = { + setup() { + onUpdated(fn) + return () => h('div', count.value as unknown as string) + }, + } + const vm = createApp(Comp).mount() + //render(h(Comp), root); + + count.value++ + await nextTick() + expect(fn).toHaveBeenCalledTimes(1) + }) + + // it('onBeforeUnmount', async () => { + // const toggle = ref(true); + // const root = document.createElement('div'); + // const fn = vi.fn(() => { + // // should be called before inner div is removed + // expect(root.outerHTML).toBe(`
`); + // }); + + // const Comp = { + // setup() { + // return () => (toggle.value ? h(Child) : null); + // }, + // }; + + // const Child = { + // setup() { + // onBeforeUnmount(fn); + // return () => h('div'); + // }, + // }; + + // createApp(Comp).mount(root); + // //render(h(Comp), root); + + // toggle.value = false; + // await nextTick(); + // expect(fn).toHaveBeenCalledTimes(1); + // }); + + // it('onUnmounted', async () => { + // const toggle = ref(true); + // const root = document.createElement('div'); + // const fn = vi.fn(() => { + // // should be called after inner div is removed + // expect(root.outerHTML).toBe(``); + // }); + + // const Comp = { + // setup() { + // return () => (toggle.value ? h(Child) : null); + // }, + // }; + + // const Child = { + // setup() { + // onUnmounted(fn); + // return () => h('div'); + // }, + // }; + + // createApp(Comp).mount(root); + // //render(h(Comp), root); + + // toggle.value = false; + // await nextTick(); + // expect(fn).toHaveBeenCalledTimes(1); + // }); + + it('onBeforeUnmount in onMounted', async () => { + const toggle = ref(true) + const root = document.createElement('div') + const fn = vi.fn(() => { + // should be called before inner div is removed + expect(root.outerHTML).toBe(`
`) + }) + + const Comp = { + setup() { + return () => (toggle.value ? h(Child) : null) + }, + } + + const Child = { + setup() { + onMounted(() => { + onBeforeUnmount(fn) + }) + return () => h('div') + }, + } + + createApp(Comp).mount(root) + //render(h(Comp), root); + + toggle.value = false + await nextTick() + expect(fn).toHaveBeenCalledTimes(1) + }) + + it('lifecycle call order', async () => { + const count = ref(0) + const calls: string[] = [] + + const Root = { + setup() { + onBeforeMount(() => calls.push('root onBeforeMount')) + onMounted(() => calls.push('root onMounted')) + onBeforeUpdate(() => calls.push('root onBeforeUpdate')) + onUpdated(() => calls.push('root onUpdated')) + onBeforeUnmount(() => calls.push('root onBeforeUnmount')) + onUnmounted(() => calls.push('root onUnmounted')) + return () => h(Mid, { props: { count: count.value } }) + }, + } + const Mid = { + props: { + count: Number, + }, + setup(props: any) { + onBeforeMount(() => calls.push('mid onBeforeMount')) + onMounted(() => calls.push('mid onMounted')) + onBeforeUpdate(() => calls.push('mid onBeforeUpdate')) + onUpdated(() => calls.push('mid onUpdated')) + onBeforeUnmount(() => calls.push('mid onBeforeUnmount')) + onUnmounted(() => calls.push('mid onUnmounted')) + return () => h(Child, { props: { count: props.count } }) + }, + } + + const Child = { + props: { + count: Number, + }, + setup(props: any) { + onBeforeMount(() => calls.push('child onBeforeMount')) + onMounted(() => calls.push('child onMounted')) + onBeforeUpdate(() => calls.push('child onBeforeUpdate')) + onUpdated(() => calls.push('child onUpdated')) + onBeforeUnmount(() => calls.push('child onBeforeUnmount')) + onUnmounted(() => calls.push('child onUnmounted')) + return () => h('div', props.count) + }, + } + + // mount + // render(h(Root), root); + const vm = createApp(Root) + vm.mount() + expect(calls).toEqual([ + 'root onBeforeMount', + 'mid onBeforeMount', + 'child onBeforeMount', + 'child onMounted', + 'mid onMounted', + 'root onMounted', + ]) + + calls.length = 0 + + // update + count.value++ + await nextTick() + await nextTick() + await nextTick() + expect(calls).toEqual([ + 'root onBeforeUpdate', + 'mid onBeforeUpdate', + 'child onBeforeUpdate', + 'child onUpdated', + 'mid onUpdated', + 'root onUpdated', + ]) + + calls.length = 0 + + // unmount + // render(null, root); + vm.unmount() + expect(calls).toEqual([ + 'root onBeforeUnmount', + 'mid onBeforeUnmount', + 'child onBeforeUnmount', + 'child onUnmounted', + 'mid onUnmounted', + 'root onUnmounted', + ]) + }) + + // it('onRenderTracked', () => { + // const events: DebuggerEvent[] = []; + // const onTrack = vi.fn((e: DebuggerEvent) => { + // events.push(e); + // }); + // const obj = reactive({ foo: 1, bar: 2 }); + + // const Comp = { + // setup() { + // onRenderTracked(onTrack); + // return () => h('div', [obj.foo, 'bar' in obj, Object.keys(obj).join('')]); + // }, + // }; + + // render(h(Comp), document.createElement('div')); + // expect(onTrack).toHaveBeenCalledTimes(3); + // expect(events).toMatchObject([ + // { + // target: obj, + // type: TrackOpTypes.GET, + // key: 'foo', + // }, + // { + // target: obj, + // type: TrackOpTypes.HAS, + // key: 'bar', + // }, + // { + // target: obj, + // type: TrackOpTypes.ITERATE, + // key: ITERATE_KEY, + // }, + // ]); + // }); + + // it('onRenderTriggered', async () => { + // const events: DebuggerEvent[] = []; + // const onTrigger = vi.fn((e: DebuggerEvent) => { + // events.push(e); + // }); + // const obj = reactive({ foo: 1, bar: 2 }); + + // const Comp = { + // setup() { + // onRenderTriggered(onTrigger); + // return () => h('div', [obj.foo, 'bar' in obj, Object.keys(obj).join('')]); + // }, + // }; + + // render(h(Comp), document.createElement('div')); + + // obj.foo++; + // await nextTick(); + // expect(onTrigger).toHaveBeenCalledTimes(1); + // expect(events[0]).toMatchObject({ + // type: TriggerOpTypes.SET, + // key: 'foo', + // oldValue: 1, + // newValue: 2, + // }); + + // delete obj.bar; + // await nextTick(); + // expect(onTrigger).toHaveBeenCalledTimes(2); + // expect(events[1]).toMatchObject({ + // type: TriggerOpTypes.DELETE, + // key: 'bar', + // oldValue: 2, + // }); + // (obj as any).baz = 3; + // await nextTick(); + // expect(onTrigger).toHaveBeenCalledTimes(3); + // expect(events[2]).toMatchObject({ + // type: TriggerOpTypes.ADD, + // key: 'baz', + // newValue: 3, + // }); + // }); +}) diff --git a/test/v3/runtime-core/apiSetupHelpers.spec.ts b/test/v3/runtime-core/apiSetupHelpers.spec.ts new file mode 100644 index 00000000..78b8ae43 --- /dev/null +++ b/test/v3/runtime-core/apiSetupHelpers.spec.ts @@ -0,0 +1,49 @@ +import { + createApp, + defineComponent, + SetupContext, + useAttrs, + useSlots, +} from '../../../src' + +describe('SFC