├── .editorconfig ├── .eslintrc.cjs ├── .github └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .nvmrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── package.json ├── pnpm-lock.yaml ├── src ├── __tests__ │ ├── compile.test.ts │ ├── fixtures.test.ts │ ├── fixtures │ │ ├── $attrs.input.vue │ │ ├── $attrs.output.2.7.vue │ │ ├── $attrs.output.3.4.vue │ │ ├── $attrs.output.3.5.vue │ │ ├── $options.input.vue │ │ ├── $options.output.2.7.vue │ │ ├── $options.output.3.4.vue │ │ ├── $options.output.3.5.vue │ │ ├── $slots.input.vue │ │ ├── $slots.output.2.7.vue │ │ ├── $slots.output.3.4.vue │ │ ├── $slots.output.3.5.vue │ │ ├── $store.input.vue │ │ ├── $store.output.2.7.vue │ │ ├── $store.output.3.4.vue │ │ ├── $store.output.3.5.vue │ │ ├── .gitattributes │ │ ├── async-created.input.vue │ │ ├── async-created.output.2.7.vue │ │ ├── async-created.output.3.4.vue │ │ ├── async-created.output.3.5.vue │ │ ├── builtins.input.vue │ │ ├── builtins.output.2.7.vue │ │ ├── builtins.output.3.4.vue │ │ ├── builtins.output.3.5.vue │ │ ├── computed.input.vue │ │ ├── computed.output.2.7.vue │ │ ├── computed.output.3.4.vue │ │ ├── computed.output.3.5.vue │ │ ├── created.input.vue │ │ ├── created.output.2.7.vue │ │ ├── created.output.3.4.vue │ │ ├── created.output.3.5.vue │ │ ├── cssmodule.input.vue │ │ ├── cssmodule.output.2.7.vue │ │ ├── cssmodule.output.3.4.vue │ │ ├── cssmodule.output.3.5.vue │ │ ├── cycle.input.vue │ │ ├── cycle.output.2.7.vue │ │ ├── cycle.output.3.4.vue │ │ ├── cycle.output.3.5.vue │ │ ├── data.input.vue │ │ ├── data.output.2.7.vue │ │ ├── data.output.3.4.vue │ │ ├── data.output.3.5.vue │ │ ├── destructure.input.vue │ │ ├── destructure.output.2.7.vue │ │ ├── destructure.output.3.4.vue │ │ ├── destructure.output.3.5.vue │ │ ├── directive.input.vue │ │ ├── directive.output.2.7.vue │ │ ├── directive.output.3.4.vue │ │ ├── directive.output.3.5.vue │ │ ├── emits-object.input.vue │ │ ├── emits-object.output.2.7.vue │ │ ├── emits-object.output.3.4.vue │ │ ├── emits-object.output.3.5.vue │ │ ├── emits-option.input.vue │ │ ├── emits-option.output.2.7.vue │ │ ├── emits-option.output.3.4.vue │ │ ├── emits-option.output.3.5.vue │ │ ├── emits.input.vue │ │ ├── emits.output.2.7.vue │ │ ├── emits.output.3.4.vue │ │ ├── emits.output.3.5.vue │ │ ├── incompatible.input.vue │ │ ├── incompatible.output.2.7.vue │ │ ├── incompatible.output.3.4.vue │ │ ├── incompatible.output.3.5.vue │ │ ├── inject-array.input.vue │ │ ├── inject-array.output.2.7.vue │ │ ├── inject-array.output.3.4.vue │ │ ├── inject-array.output.3.5.vue │ │ ├── inject-object.input.vue │ │ ├── inject-object.output.2.7.vue │ │ ├── inject-object.output.3.4.vue │ │ ├── inject-object.output.3.5.vue │ │ ├── lifecycle.input.vue │ │ ├── lifecycle.output.2.7.vue │ │ ├── lifecycle.output.3.4.vue │ │ ├── lifecycle.output.3.5.vue │ │ ├── methods.input.vue │ │ ├── methods.output.2.7.vue │ │ ├── methods.output.3.4.vue │ │ ├── methods.output.3.5.vue │ │ ├── pinia-mapactions.input.vue │ │ ├── pinia-mapactions.output.2.7.vue │ │ ├── pinia-mapactions.output.3.4.vue │ │ ├── pinia-mapactions.output.3.5.vue │ │ ├── pinia-mapstate.input.vue │ │ ├── pinia-mapstate.output.2.7.vue │ │ ├── pinia-mapstate.output.3.4.vue │ │ ├── pinia-mapstate.output.3.5.vue │ │ ├── pinia-mapstores.input.vue │ │ ├── pinia-mapstores.output.2.7.vue │ │ ├── pinia-mapstores.output.3.4.vue │ │ ├── pinia-mapstores.output.3.5.vue │ │ ├── pinia-vuex.input.vue │ │ ├── pinia-vuex.output.2.7.vue │ │ ├── pinia-vuex.output.3.4.vue │ │ ├── pinia-vuex.output.3.5.vue │ │ ├── pinia-writablestate.input.vue │ │ ├── pinia-writablestate.output.2.7.vue │ │ ├── pinia-writablestate.output.3.4.vue │ │ ├── pinia-writablestate.output.3.5.vue │ │ ├── props-2.input.vue │ │ ├── props-2.output.2.7.vue │ │ ├── props-2.output.3.4.vue │ │ ├── props-2.output.3.5.vue │ │ ├── props-no-var.input.vue │ │ ├── props-no-var.output.2.7.vue │ │ ├── props-no-var.output.3.4.vue │ │ ├── props-no-var.output.3.5.vue │ │ ├── props.input.vue │ │ ├── props.output.2.7.vue │ │ ├── props.output.3.4.vue │ │ ├── props.output.3.5.vue │ │ ├── provide-fn.input.vue │ │ ├── provide-fn.output.2.7.vue │ │ ├── provide-fn.output.3.4.vue │ │ ├── provide-fn.output.3.5.vue │ │ ├── provide.input.vue │ │ ├── provide.output.2.7.vue │ │ ├── provide.output.3.4.vue │ │ ├── provide.output.3.5.vue │ │ ├── raw-data.input.vue │ │ ├── raw-data.output.2.7.vue │ │ ├── raw-data.output.3.4.vue │ │ ├── raw-data.output.3.5.vue │ │ ├── refs.input.vue │ │ ├── refs.output.2.7.vue │ │ ├── refs.output.3.4.vue │ │ ├── refs.output.3.5.vue │ │ ├── router.input.vue │ │ ├── router.output.2.7.vue │ │ ├── router.output.3.4.vue │ │ ├── router.output.3.5.vue │ │ ├── setup.input.vue │ │ ├── setup.output.2.7.vue │ │ ├── setup.output.3.4.vue │ │ ├── setup.output.3.5.vue │ │ ├── typescript.input.vue │ │ ├── typescript.output.2.7.vue │ │ ├── typescript.output.3.4.vue │ │ ├── typescript.output.3.5.vue │ │ ├── unknown.input.vue │ │ ├── unknown.output.2.7.vue │ │ ├── unknown.output.3.4.vue │ │ ├── unknown.output.3.5.vue │ │ ├── vuex-getter.input.vue │ │ ├── vuex-getter.output.2.7.vue │ │ ├── vuex-getter.output.3.4.vue │ │ ├── vuex-getter.output.3.5.vue │ │ ├── vuex-state-array.input.vue │ │ ├── vuex-state-array.output.2.7.vue │ │ ├── vuex-state-array.output.3.4.vue │ │ ├── vuex-state-array.output.3.5.vue │ │ ├── vuex-state-object.input.vue │ │ ├── vuex-state-object.output.2.7.vue │ │ ├── vuex-state-object.output.3.4.vue │ │ ├── vuex-state-object.output.3.5.vue │ │ ├── vuex.input.vue │ │ ├── vuex.output.2.7.vue │ │ ├── vuex.output.3.4.vue │ │ ├── vuex.output.3.5.vue │ │ ├── watch.input.vue │ │ ├── watch.output.2.7.vue │ │ ├── watch.output.3.4.vue │ │ └── watch.output.3.5.vue │ └── version.test.ts ├── analyze │ ├── computed.ts │ ├── created.ts │ ├── data.ts │ ├── dependencies.ts │ ├── directives.ts │ ├── emits.ts │ ├── incompatible.ts │ ├── index.ts │ ├── inject.ts │ ├── lifecycle.ts │ ├── methods.ts │ ├── options.ts │ ├── pinia-actions.ts │ ├── pinia-mapstate.ts │ ├── pinia-mapstores.ts │ ├── pinia-mapwritablestate.ts │ ├── props.ts │ ├── provide.ts │ ├── refs.ts │ ├── setup.ts │ ├── utils.ts │ ├── vuex.ts │ ├── watcher-sources.ts │ └── watchers.ts ├── ast.ts ├── codegen │ ├── $options.ts │ ├── $refs.ts │ ├── computed.ts │ ├── data.ts │ ├── directive.ts │ ├── emits.ts │ ├── graph.ts │ ├── index.ts │ ├── lifecycle.ts │ ├── method.ts │ ├── pinia-state.ts │ ├── pinia-store.ts │ ├── props.ts │ ├── unknown.ts │ ├── vuex-action.ts │ ├── vuex-getter.ts │ ├── vuex-mutation.ts │ ├── vuex-state.ts │ └── watcher.ts ├── compile.ts ├── main.ts ├── options.ts ├── transform │ ├── directives.ts │ ├── index.ts │ ├── provide-inject.ts │ ├── refs.ts │ ├── this.ts │ ├── utils.ts │ └── vuex.ts └── version.ts ├── tsconfig.eslint.json ├── tsconfig.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | /** @type {import("eslint").ESLint.ConfigData} */ 4 | module.exports = { 5 | root: true, 6 | parser: '@typescript-eslint/parser', 7 | parserOptions: { 8 | sourceType: 'module', 9 | ecmaVersion: 2020, 10 | project: path.join(__dirname, 'tsconfig.eslint.json'), 11 | }, 12 | plugins: [ 13 | '@typescript-eslint', 14 | ], 15 | 16 | ignorePatterns: [ 17 | 'node_modules', 18 | 'dist', 19 | ], 20 | extends: [ 21 | 'plugin:@typescript-eslint/recommended', 22 | 'airbnb-base', 23 | 'airbnb-typescript/base', 24 | ], 25 | rules: { 26 | 'import/prefer-default-export': 0, 27 | 'no-param-reassign': 0, 28 | 'no-plusplus': 0, 29 | 'no-restricted-syntax': 0, 30 | 'import/extensions': 0, 31 | 'no-await-in-loop': 0, 32 | 'no-continue': 0, 33 | '@typescript-eslint/no-loop-func': 0, 34 | 'max-len': 0, 35 | '@typescript-eslint/member-delimiter-style': [2, { 36 | multiline: { 37 | delimiter: 'semi', 38 | requireLast: true, 39 | }, 40 | singleline: { 41 | delimiter: 'semi', 42 | requireLast: true, 43 | }, 44 | }], 45 | }, 46 | overrides: [ 47 | { 48 | files: ['*.cjs'], 49 | rules: { 50 | '@typescript-eslint/no-var-requires': 0, 51 | }, 52 | }, 53 | { 54 | files: ['vite.config.ts', 'docs/**'], 55 | rules: { 56 | 'import/no-extraneous-dependencies': 0, 57 | }, 58 | }, 59 | ], 60 | }; 61 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | 4 | concurrency: 5 | group: ${{ github.ref }}-${{ github.workflow }} 6 | cancel-in-progress: true 7 | 8 | jobs: 9 | ci: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version-file: .nvmrc 16 | - uses: pnpm/action-setup@v3.0.0 17 | with: 18 | version: ^9.0.0 19 | run_install: false 20 | - run: pnpm install 21 | - run: pnpm lint 22 | - run: pnpm test 23 | - run: pnpm build 24 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | release_type: 6 | type: choice 7 | required: true 8 | options: 9 | - patch 10 | - minor 11 | - major 12 | 13 | 14 | permissions: 15 | contents: write 16 | id-token: write 17 | actions: write 18 | 19 | jobs: 20 | publish: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-node@v4 25 | with: 26 | node-version-file: .nvmrc 27 | registry-url: 'https://registry.npmjs.org' 28 | - uses: pnpm/action-setup@v3.0.0 29 | with: 30 | version: ^9.9.0 31 | run_install: false 32 | - name: Install dependencies 33 | run: pnpm install 34 | - name: Bump version 35 | run: | 36 | git config user.name 'vue-scriptshifter release bot' 37 | git config user.email 'unrefinedbrain@users.noreply.github.com' 38 | 39 | npm version ${{ inputs.release_type }} 40 | git push --follow-tags origin master 41 | - name: Create release 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.RELEASE_PAT }} 44 | run: | 45 | VERSION=v$(cat package.json | jq --raw-output .version) 46 | gh api \ 47 | --method POST \ 48 | -H "Accept: application/vnd.github+json" \ 49 | -H "X-GitHub-Api-Version: 2022-11-28" \ 50 | /repos/UnrefinedBrain/vue-scriptshifter/releases \ 51 | -f tag_name="$VERSION" \ 52 | -F draft=false \ 53 | -F prerelease=false \ 54 | -F generate_release_notes=true 55 | 56 | - name: Build 57 | run: pnpm build 58 | - name: Publish 59 | run: npm publish --access=public --provenance 60 | env: 61 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 62 | 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.11.0 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2025 UnrefinedBrain 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ScriptShifter 2 | 3 | 4 | [![NPM Version](https://img.shields.io/npm/v/scriptshifter)](https://npmjs.com/package/scriptshifter) [![NPM License](https://img.shields.io/npm/l/scriptshifter)](https://github.com/UnrefinedBrain/scriptshifter/blob/master/LICENSE) [![GitHub Repo stars](https://img.shields.io/github/stars/UnrefinedBrain/scriptshifter)](https://github.com/UnrefinedBrain/scriptshifter) [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/UnrefinedBrain/scriptshifter/ci.yml)](https://github.com/UnrefinedBrain/scriptshifter/actions) 5 | 6 | Vue ScriptShifter is a tool that converts Vue components from Options API to ` 93 | ``` 94 | 95 | Output: 96 | ```vue 97 | 105 | ``` 106 | 107 | ## License 108 | 109 | ScriptShifter is released under the MIT license. See the [LICENSE](./LICENSE) file for the full license. 110 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scriptshifter", 3 | "description": "Automatically convert Vue components from Options API to 36 | `; 37 | 38 | const result = transform(source, 'file.vue', [scriptshifter]); 39 | 40 | expect(normalizeLinebreaks(result.code)).toBe(source); 41 | }); 42 | -------------------------------------------------------------------------------- /src/__tests__/fixtures.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import { 4 | it, 5 | expect, 6 | describe, 7 | } from 'vitest'; 8 | import { transform } from 'vue-metamorph'; 9 | import { scriptshifter } from '../compile'; 10 | import { vueVersions } from '../options'; 11 | import { normalizeLinebreaks } from './compile.test'; 12 | 13 | const fixtures = fs 14 | .readdirSync(path.resolve(__dirname, 'fixtures')) 15 | .filter((file) => file.endsWith('.input.vue')) 16 | .map((file) => path.join('fixtures', file)); 17 | 18 | describe.each(vueVersions)('Vue %s mode', (mode) => { 19 | it.each(fixtures)('snapshot %s', async (filename) => { 20 | const code = fs.readFileSync(path.resolve(__dirname, filename), { encoding: 'utf-8' }); 21 | await expect( 22 | normalizeLinebreaks(transform(code, filename, [scriptshifter], { vue: mode }).code), 23 | ).toMatchFileSnapshot(path.resolve(__dirname, filename.replace('.input.vue', `.output.${mode}.vue`))); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/$attrs.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/$attrs.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/$attrs.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/$attrs.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/$options.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 19 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/$options.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/$options.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/$options.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/$slots.input.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/$slots.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/$slots.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/$slots.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/$store.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/$store.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/$store.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/$store.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/.gitattributes: -------------------------------------------------------------------------------- 1 | *.vue eol=lf -------------------------------------------------------------------------------- /src/__tests__/fixtures/async-created.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/async-created.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/async-created.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/async-created.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/builtins.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 19 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/builtins.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/builtins.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/builtins.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/computed.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 35 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/computed.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 31 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/computed.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 31 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/computed.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 31 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/created.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/created.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/created.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/created.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/cssmodule.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/cssmodule.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/cssmodule.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/cssmodule.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/cycle.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 33 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/cycle.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 29 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/cycle.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 29 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/cycle.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 29 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/data.input.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 44 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/data.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 39 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/data.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 39 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/data.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 39 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/destructure.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 29 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/destructure.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/destructure.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/destructure.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/directive.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 29 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/directive.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/directive.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/directive.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/emits-object.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/emits-object.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/emits-object.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/emits-object.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/emits-option.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/emits-option.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/emits-option.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/emits-option.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/emits.input.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/emits.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/emits.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/emits.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/incompatible.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 31 | 32 | 37 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/incompatible.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | 21 | 33 | 34 | 39 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/incompatible.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | 21 | 33 | 34 | 39 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/incompatible.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | 21 | 33 | 34 | 39 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/inject-array.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/inject-array.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/inject-array.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/inject-array.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/inject-object.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 22 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/inject-object.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/inject-object.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/inject-object.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/lifecycle.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 32 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/lifecycle.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 32 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/lifecycle.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 32 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/lifecycle.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 32 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/methods.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 30 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/methods.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/methods.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/methods.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/pinia-mapactions.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 27 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/pinia-mapactions.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/pinia-mapactions.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/pinia-mapactions.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/pinia-mapstate.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 37 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/pinia-mapstate.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 27 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/pinia-mapstate.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 27 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/pinia-mapstate.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 27 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/pinia-mapstores.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 22 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/pinia-mapstores.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/pinia-mapstores.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/pinia-mapstores.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/pinia-vuex.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 25 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/pinia-vuex.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/pinia-vuex.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/pinia-vuex.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/pinia-writablestate.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 29 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/pinia-writablestate.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 22 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/pinia-writablestate.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 22 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/pinia-writablestate.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 22 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/props-2.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/props-2.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/props-2.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/props-2.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/props-no-var.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/props-no-var.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/props-no-var.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/props-no-var.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/props.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 26 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/props.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 23 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/props.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 23 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/props.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 23 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/provide-fn.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/provide-fn.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/provide-fn.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/provide-fn.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/provide.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/provide.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/provide.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/provide.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/raw-data.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/raw-data.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/raw-data.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/raw-data.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/refs.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/refs.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/refs.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/refs.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/router.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/router.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/router.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/router.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/setup.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 26 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/setup.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/setup.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/setup.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/typescript.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 22 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/typescript.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/typescript.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/typescript.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/unknown.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/unknown.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/unknown.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/unknown.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/vuex-getter.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 22 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/vuex-getter.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/vuex-getter.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | 19 | 24 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/vuex-getter.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | 19 | 24 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/vuex-state-array.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 21 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/vuex-state-array.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 26 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/vuex-state-array.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 29 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/vuex-state-array.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 29 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/vuex-state-object.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 28 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/vuex-state-object.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 41 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/vuex-state-object.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 44 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/vuex-state-object.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 44 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/vuex.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 50 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/vuex.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 38 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/vuex.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 40 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/vuex.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 40 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/watch.input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 61 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/watch.output.2.7.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 53 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/watch.output.3.4.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 53 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/watch.output.3.5.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 53 | -------------------------------------------------------------------------------- /src/__tests__/version.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { isVersionGtEq, isVersionLtEq } from '../version'; 3 | 4 | describe('isVersionGreaterThanOrEqualTo', () => { 5 | it.each([ 6 | [true, '2.7', '2.7'], 7 | [false, '2.7', '3.4'], 8 | [true, '3.4', '2.7'], 9 | ] as const)('should return %s when comparing %s against %s', (expected, first, second) => { 10 | expect(isVersionGtEq(first, second)).toBe(expected); 11 | }); 12 | }); 13 | 14 | describe('isVersionLessThanOrEqualTo', () => { 15 | it.each([ 16 | [true, '2.7', '2.7'], 17 | [true, '2.7', '3.4'], 18 | [false, '3.4', '2.7'], 19 | ] as const)('should return %s when comparing %s against %s', (expected, first, second) => { 20 | expect(isVersionLtEq(first, second)).toBe(expected); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/analyze/computed.ts: -------------------------------------------------------------------------------- 1 | import { namedTypes as n } from 'vue-metamorph'; 2 | import type { ComputedNode } from '../ast'; 3 | import { analyzeDependencies } from './dependencies'; 4 | import { 5 | getStringKey, 6 | isStringKey, 7 | toArrowFunctionExpression, 8 | } from './utils'; 9 | 10 | export function analyzeComputed(computedBlock: n.ObjectExpression): ComputedNode[] { 11 | const nodes: ComputedNode[] = []; 12 | 13 | for (const computed of computedBlock.properties) { 14 | if (computed.type === 'Property' 15 | && isStringKey(computed.key) 16 | && (computed.value.type === 'ArrowFunctionExpression' 17 | || computed.value.type === 'ObjectExpression' 18 | || computed.value.type === 'FunctionExpression')) { 19 | nodes.push({ 20 | dependencies: [], 21 | name: getStringKey(computed.key), 22 | node: computed.value.type === 'ObjectExpression' 23 | ? computed.value 24 | : toArrowFunctionExpression(computed.value), 25 | type: 'computed', 26 | comments: computed.comments, 27 | }); 28 | } 29 | } 30 | 31 | nodes.forEach((node) => { 32 | node.dependencies = analyzeDependencies(node); 33 | }); 34 | 35 | return nodes; 36 | } 37 | -------------------------------------------------------------------------------- /src/analyze/created.ts: -------------------------------------------------------------------------------- 1 | import { 2 | namedTypes as n, 3 | builders as b, 4 | } from 'vue-metamorph'; 5 | import type { CreatedHookNode } from '../ast'; 6 | import { analyzeDependencies } from './dependencies'; 7 | import { 8 | getStringKey, 9 | isStringKey, 10 | } from './utils'; 11 | 12 | export function analyzeCreatedHook(optionsBlock: n.ObjectExpression): CreatedHookNode | null { 13 | // created hooks should be turned into an iife to preserve block scoping 14 | for (const option of optionsBlock.properties) { 15 | if (option.type === 'Property' 16 | && isStringKey(option.key) 17 | && getStringKey(option.key) === 'created' 18 | && option.value.type === 'FunctionExpression') { 19 | const fn = b.arrowFunctionExpression( 20 | [], 21 | option.value.body, 22 | ); 23 | 24 | if (option.value.async) { 25 | fn.async = true; 26 | } 27 | 28 | const stmt = b.expressionStatement( 29 | b.callExpression( 30 | fn, 31 | [], 32 | ), 33 | ); 34 | 35 | const node: CreatedHookNode = { 36 | type: 'created', 37 | dependencies: [], 38 | name: '', 39 | node: stmt, 40 | }; 41 | 42 | node.dependencies.push(...analyzeDependencies(node)); 43 | 44 | return node; 45 | } 46 | } 47 | 48 | return null; 49 | } 50 | -------------------------------------------------------------------------------- /src/analyze/data.ts: -------------------------------------------------------------------------------- 1 | import { namedTypes as n } from 'vue-metamorph'; 2 | import type { DataNode } from '../ast'; 3 | import { analyzeDependencies } from './dependencies'; 4 | import { 5 | getStringKey, 6 | isStringKey, 7 | } from './utils'; 8 | 9 | export function analyzeData(dataFn: n.ArrowFunctionExpression | n.FunctionExpression): DataNode[] { 10 | const nodes: DataNode[] = []; 11 | // case 1: data() { return { prop1: 0 } } 12 | // case 2: data: () => ({ prop1: 0 }) 13 | 14 | const visitObjectExpression = (expr: n.ObjectExpression) => { 15 | for (const prop of expr.properties) { 16 | if (prop.type === 'Property' 17 | && isStringKey(prop.key) 18 | && (prop.value.type.includes('Expression') 19 | || prop.value.type === 'Identifier' 20 | || prop.value.type === 'Literal')) { 21 | nodes.push({ 22 | type: prop.value.type === 'Identifier' && getStringKey(prop.key) === prop.value.name 23 | ? 'rawData' 24 | : 'data', 25 | name: getStringKey(prop.key), 26 | dependencies: [], 27 | node: prop.value as never, 28 | comments: prop.comments, 29 | }); 30 | } 31 | } 32 | }; 33 | 34 | if (dataFn.body.type === 'ObjectExpression') { 35 | visitObjectExpression(dataFn.body); 36 | } else if (dataFn.body.type === 'BlockStatement') { 37 | for (const statement of dataFn.body.body) { 38 | if (statement.type === 'ReturnStatement' 39 | && statement.argument?.type === 'ObjectExpression') { 40 | visitObjectExpression(statement.argument); 41 | } 42 | } 43 | } 44 | 45 | nodes.forEach((node) => { 46 | node.dependencies = analyzeDependencies(node); 47 | }); 48 | 49 | return nodes; 50 | } 51 | -------------------------------------------------------------------------------- /src/analyze/dependencies.ts: -------------------------------------------------------------------------------- 1 | import { 2 | traverseScriptAST, 3 | type Kinds, 4 | } from 'vue-metamorph'; 5 | import type { ScriptSetupNode } from '../ast'; 6 | import { CompoundWatcherRegex, isThisDotRefs } from './utils'; 7 | import { refName } from './refs'; 8 | 9 | export function analyzeDependencies(node: ScriptSetupNode) { 10 | const dependencies = new Set(); 11 | 12 | const visitExpression = (expr: Kinds.ExpressionKind) => { 13 | traverseScriptAST(expr, { 14 | visitVariableDeclaration(path) { 15 | for (const declarator of path.node.declarations) { 16 | if ( 17 | declarator.type !== 'VariableDeclarator' 18 | || declarator.id.type !== 'ObjectPattern' 19 | ) { 20 | continue; 21 | } 22 | 23 | // `const { foo } = this` depends on foo 24 | // `const { foo } = this.$refs` depends on foo 25 | if ( 26 | declarator.init 27 | && (declarator.init?.type === 'ThisExpression' 28 | || isThisDotRefs(declarator.init))) { 29 | for (const prop of declarator.id.properties) { 30 | if (prop.type !== 'Property' 31 | || prop.value.type !== 'Identifier' 32 | ) { 33 | continue; 34 | } 35 | 36 | dependencies.add(refName(prop.value.name)); 37 | } 38 | } 39 | } 40 | 41 | return this.traverse(path); 42 | }, 43 | visitMemberExpression(path) { 44 | if (path.node.object.type === 'ThisExpression') { 45 | // this['foo'] depends on foo 46 | if (path.node.property.type === 'Literal' 47 | && typeof path.node.property.value === 'string') { 48 | dependencies.add(path.node.property.value); 49 | } 50 | 51 | // this.foo depends on foo 52 | if (path.node.property.type === 'Identifier') { 53 | dependencies.add(path.node.property.name); 54 | } 55 | } 56 | 57 | // this.$refs.foo depends on foo 58 | if (path.node.object.type === 'MemberExpression' 59 | && path.node.property.type === 'Identifier' 60 | && path.node.object.object.type === 'ThisExpression' 61 | && path.node.object.property.type === 'Identifier' 62 | && path.node.object.property.name === '$refs' 63 | ) { 64 | dependencies.add(refName(path.node.property.name)); 65 | } 66 | 67 | return this.traverse(path); 68 | }, 69 | }); 70 | }; 71 | 72 | // normal computed / methods / watchers 73 | if (node.node?.type === 'ArrowFunctionExpression') { 74 | visitExpression(node.node); 75 | } 76 | 77 | if (node.type === 'data') { 78 | visitExpression(node.node); 79 | } 80 | 81 | if (node.type === 'created') { 82 | visitExpression(node.node.expression); 83 | } 84 | 85 | if (node.type === 'provide') { 86 | visitExpression(node.node.expression); 87 | } 88 | 89 | // settable computed properties 90 | if (node.type === 'computed' && node.node.type === 'ObjectExpression') { 91 | for (const prop of node.node.properties) { 92 | if (prop.type === 'Property' 93 | && prop.key.type === 'Identifier' 94 | && ['get', 'set'].includes(prop.key.name) 95 | && (prop.value.type === 'ArrowFunctionExpression' || prop.value.type === 'FunctionExpression')) { 96 | visitExpression(prop.value); 97 | } 98 | } 99 | } 100 | 101 | if (node.type === 'watcher') { 102 | if (CompoundWatcherRegex.test(node.watchName)) { 103 | // if the watch key is something like 'person.firstName', we have a dependency on `person` 104 | const [name] = node.watchName.split('.'); 105 | dependencies.add(name!); 106 | } else { 107 | dependencies.add(node.watchName); 108 | } 109 | } 110 | 111 | return Array.from(dependencies); 112 | } 113 | -------------------------------------------------------------------------------- /src/analyze/directives.ts: -------------------------------------------------------------------------------- 1 | import { namedTypes as n } from 'vue-metamorph'; 2 | import type { DirectiveNode } from '../ast'; 3 | import { 4 | getStringKey, 5 | isStringKey, 6 | } from './utils'; 7 | 8 | export function analyzeDirectives(directivesBlock: n.ObjectExpression): DirectiveNode[] { 9 | const nodes: DirectiveNode[] = []; 10 | 11 | for (const directive of directivesBlock.properties) { 12 | if (directive.type === 'Property' 13 | && isStringKey(directive.key)) { 14 | nodes.push({ 15 | dependencies: [], 16 | name: getStringKey(directive.key), 17 | node: directive.value.type === 'ObjectExpression' 18 | ? directive.value 19 | : null, 20 | type: 'directive', 21 | comments: directive.comments, 22 | }); 23 | } 24 | } 25 | 26 | return nodes; 27 | } 28 | -------------------------------------------------------------------------------- /src/analyze/emits.ts: -------------------------------------------------------------------------------- 1 | import { 2 | namedTypes as n, 3 | traverseScriptAST, 4 | astHelpers, 5 | builders as b, 6 | AST, 7 | } from 'vue-metamorph'; 8 | import type { EmitsNode } from '../ast'; 9 | import { getStringKey, isStringKey } from './utils'; 10 | 11 | export function analyzeEmits(optionsBlock: n.ObjectExpression, sfcAST: AST.VDocumentFragment): EmitsNode | null { 12 | for (const option of optionsBlock.properties) { 13 | if (option.type === 'Property' 14 | && isStringKey(option.key) 15 | && getStringKey(option.key) === 'emits' 16 | && (option.value.type === 'ArrayExpression' || option.value.type === 'ObjectExpression')) { 17 | return { 18 | type: 'emit', 19 | node: option.value, 20 | dependencies: [], 21 | name: 'defineEmits', 22 | comments: option.comments, 23 | }; 24 | } 25 | } 26 | 27 | const emits = b.arrayExpression([]); 28 | const events = new Set(); 29 | traverseScriptAST(optionsBlock, { 30 | visitCallExpression(path) { 31 | if (path.node.callee.type === 'MemberExpression' 32 | && path.node.callee.object.type === 'ThisExpression' 33 | && path.node.callee.property.type === 'Identifier' 34 | && path.node.callee.property.name === '$emit' 35 | && path.node.arguments[0]?.type === 'Literal' 36 | && typeof path.node.arguments[0].value === 'string') { 37 | events.add(path.node.arguments[0].value); 38 | } 39 | return this.traverse(path); 40 | }, 41 | }); 42 | 43 | astHelpers 44 | .findAll(sfcAST, { 45 | type: 'CallExpression', 46 | callee: { 47 | type: 'Identifier', 48 | name: '$emit', 49 | }, 50 | }) 51 | .forEach((node) => { 52 | if (node.callee.type === 'Identifier' 53 | && node.arguments[0]?.type === 'Literal' 54 | && typeof node.arguments[0].value === 'string' 55 | ) { 56 | events.add(node.arguments[0].value); 57 | node.callee.name = 'emit'; 58 | } 59 | }); 60 | 61 | Array 62 | .from(events) 63 | .forEach((event) => { 64 | emits.elements.push(b.literal(event)); 65 | }); 66 | 67 | return emits.elements.length > 0 68 | ? { 69 | dependencies: [], 70 | name: 'defineEmits', 71 | type: 'emit', 72 | node: emits, 73 | comments: [], 74 | } 75 | : null; 76 | } 77 | -------------------------------------------------------------------------------- /src/analyze/incompatible.ts: -------------------------------------------------------------------------------- 1 | import { 2 | namedTypes as n, 3 | builders as b, 4 | } from 'vue-metamorph'; 5 | import { 6 | getStringKey, 7 | isStringKey, 8 | } from './utils'; 9 | 10 | export function analyzeIncompatibleOptions(optionsBlock: n.ObjectExpression): n.Program | null { 11 | const program = b.program([]); 12 | const properties: n.Property[] = []; 13 | 14 | for (const option of optionsBlock.properties) { 15 | if (option.type !== 'Property' 16 | || !isStringKey(option.key) 17 | ) { 18 | continue; 19 | } 20 | 21 | if ([ 22 | 'mixins', 23 | 'beforeCreate', 24 | 'beforeRouteEnter', 25 | 'name', 26 | ].includes(getStringKey(option.key))) { 27 | properties.push(option); 28 | } 29 | } 30 | 31 | if (properties.length > 0) { 32 | program.body.push( 33 | b.exportDefaultDeclaration( 34 | b.objectExpression(properties), 35 | ), 36 | ); 37 | } 38 | 39 | return program.body.length > 0 40 | ? program 41 | : null; 42 | } 43 | -------------------------------------------------------------------------------- /src/analyze/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AST, 3 | astHelpers, 4 | namedTypes as n, 5 | } from 'vue-metamorph'; 6 | import type { ScriptSetupAst } from '../ast'; 7 | import { analyzeComputed } from './computed'; 8 | import { analyzeCreatedHook } from './created'; 9 | import { analyzeData } from './data'; 10 | import { analyzeDirectives } from './directives'; 11 | import { analyzeEmits } from './emits'; 12 | import { analyzeLifecycleHooks } from './lifecycle'; 13 | import { analyzeMethods } from './methods'; 14 | import { 15 | analyzeIfPropsReferenced, 16 | analyzeProps, 17 | } from './props'; 18 | import { 19 | analyzeVuexActions, 20 | analyzeVuexGetters, 21 | analyzeVuexMutations, 22 | analyzeVuexState, 23 | } from './vuex'; 24 | import { analyzeWatcherSourceTypes } from './watcher-sources'; 25 | import { analyzeWatchers } from './watchers'; 26 | import { analyzeSetup, analyzeSetupPropsReferenced } from './setup'; 27 | import { toArrowFunctionExpression } from './utils'; 28 | import { analyzeProvide } from './provide'; 29 | import { analyzeInject } from './inject'; 30 | import { analyzeOptions } from './options'; 31 | import { analyzeRefs } from './refs'; 32 | import { analyzePiniaMapStores } from './pinia-mapstores'; 33 | import { analyzePiniaActions } from './pinia-actions'; 34 | import { analyzePiniaMapState } from './pinia-mapstate'; 35 | import { analyzePiniaMapWriteableState } from './pinia-mapwritablestate'; 36 | 37 | function findSfcOptionsBlock(program: n.Program): [n.ObjectExpression | null, number] { 38 | let i = 0; 39 | for (const statement of program.body) { 40 | if (statement.type === 'ExportDefaultDeclaration') { 41 | if (statement.declaration.type === 'ObjectExpression') { 42 | return [statement.declaration, i]; 43 | } if (statement.declaration.type === 'CallExpression' 44 | && statement.declaration.arguments[0]?.type === 'ObjectExpression') { 45 | return [statement.declaration.arguments[0], i]; 46 | } 47 | } 48 | 49 | i++; 50 | } 51 | 52 | return [null, -1]; 53 | } 54 | 55 | function findSfcOption< 56 | T extends n.Property['value']['type'][], 57 | >( 58 | optionsBlock: n.ObjectExpression, 59 | name: string, 60 | types: T, 61 | ): (n.Property['value'] & { type: T[number]; }) | null { 62 | for (const property of optionsBlock.properties) { 63 | if (property.type === 'Property' 64 | && property.key.type === 'Identifier' 65 | && property.key.name === name 66 | && types.includes(property.value.type)) { 67 | return property.value; 68 | } 69 | } 70 | 71 | return null; 72 | } 73 | 74 | function findLocalImportName(program: n.Program, source: string, importedName: string) { 75 | const importDecls = astHelpers 76 | .findAll(program, { 77 | type: 'ImportDeclaration', 78 | source: { 79 | type: 'Literal', 80 | value: source, 81 | }, 82 | }); 83 | 84 | for (const decl of importDecls) { 85 | if (!decl.specifiers) { 86 | continue; 87 | } 88 | 89 | for (const specifier of decl.specifiers) { 90 | if (specifier.type === 'ImportSpecifier' 91 | && specifier.imported?.name === importedName) { 92 | // eslint-disable-next-line no-nested-ternary 93 | return specifier.local?.type === 'Identifier' 94 | ? specifier.local.name 95 | : specifier.imported.type === 'Identifier' 96 | ? specifier.imported.name 97 | : null; 98 | } 99 | } 100 | } 101 | 102 | return null; 103 | } 104 | 105 | export function analyze(program: n.Program, sfcAST: AST.VDocumentFragment): ScriptSetupAst { 106 | const [optionsBlock, optionsBlockIndex] = findSfcOptionsBlock(program); 107 | 108 | if (!optionsBlock) { 109 | throw new Error('Could not find SFC options block'); 110 | } 111 | 112 | const computedBlock = findSfcOption(optionsBlock, 'computed', ['ObjectExpression']); 113 | const propsBlock = findSfcOption(optionsBlock, 'props', ['ObjectExpression']); 114 | const dataBlock = findSfcOption(optionsBlock, 'data', ['ArrowFunctionExpression', 'FunctionExpression']); 115 | const methodsBlock = findSfcOption(optionsBlock, 'methods', ['ObjectExpression']); 116 | const watchBlock = findSfcOption(optionsBlock, 'watch', ['ObjectExpression']); 117 | const directivesBlock = findSfcOption(optionsBlock, 'directives', ['ObjectExpression']); 118 | const setupBlock = findSfcOption(optionsBlock, 'setup', ['ArrowFunctionExpression', 'FunctionExpression']); 119 | const provideBlock = findSfcOption(optionsBlock, 'provide', ['ArrowFunctionExpression', 'ObjectExpression', 'FunctionExpression']); 120 | const injectBlock = findSfcOption(optionsBlock, 'inject', ['ArrayExpression', 'ObjectExpression']); 121 | 122 | const localVuexMapActionsName = findLocalImportName(program, 'vuex', 'mapActions'); 123 | const localVuexMapGettersName = findLocalImportName(program, 'vuex', 'mapGetters'); 124 | const localVuexMapMutationsName = findLocalImportName(program, 'vuex', 'mapMutations'); 125 | const localVuexMapStateName = findLocalImportName(program, 'vuex', 'mapState'); 126 | 127 | const localPiniaMapActionsName = findLocalImportName(program, 'pinia', 'mapActions'); 128 | const localPiniaMapStateName = findLocalImportName(program, 'pinia', 'mapState'); 129 | const localPiniaMapGettersName = findLocalImportName(program, 'pinia', 'mapGetters'); 130 | const localPiniaMapWritableStateName = findLocalImportName(program, 'pinia', 'mapWritableState'); 131 | const localPiniaMapStoresName = findLocalImportName(program, 'pinia', 'mapStores'); 132 | 133 | const ast: ScriptSetupAst = { 134 | computed: computedBlock 135 | ? analyzeComputed(computedBlock) 136 | : [], 137 | data: dataBlock 138 | ? analyzeData(dataBlock) 139 | : [], 140 | emits: analyzeEmits(optionsBlock, sfcAST), 141 | lifecycleHooks: analyzeLifecycleHooks(optionsBlock), 142 | methods: methodsBlock 143 | ? analyzeMethods(methodsBlock) 144 | : [], 145 | props: propsBlock 146 | ? analyzeProps(propsBlock) 147 | : null, 148 | watchers: watchBlock 149 | ? analyzeWatchers(watchBlock) 150 | : [], 151 | unknowns: [], 152 | vuexActions: methodsBlock && localVuexMapActionsName 153 | ? analyzeVuexActions(methodsBlock, localVuexMapActionsName) 154 | : [], 155 | vuexGetters: computedBlock && localVuexMapGettersName 156 | ? analyzeVuexGetters(computedBlock, localVuexMapGettersName) 157 | : [], 158 | vuexMutations: methodsBlock && localVuexMapMutationsName 159 | ? analyzeVuexMutations(methodsBlock, localVuexMapMutationsName) 160 | : [], 161 | vuexState: computedBlock && localVuexMapStateName 162 | ? analyzeVuexState(computedBlock, localVuexMapStateName) 163 | : [], 164 | afterOptionsStatements: program.body.slice(optionsBlockIndex + 1), 165 | beforeOptionsStatements: program.body.slice(0, optionsBlockIndex), 166 | piniaStores: {}, 167 | piniaActions: {}, 168 | piniaStates: {}, 169 | piniaWritableStates: [], 170 | directives: directivesBlock 171 | ? analyzeDirectives(directivesBlock) 172 | : [], 173 | createdHook: analyzeCreatedHook(optionsBlock), 174 | afterPropsStatements: [], 175 | provides: provideBlock 176 | ? analyzeProvide(provideBlock) 177 | : [], 178 | injects: injectBlock 179 | ? analyzeInject(injectBlock) 180 | : [], 181 | $options: analyzeOptions(optionsBlock), 182 | $refs: analyzeRefs(optionsBlock), 183 | wasEmitted: { 184 | cssModule: false, 185 | router: false, 186 | route: false, 187 | store: false, 188 | vuexStateAccessHelper: false, 189 | attrs: false, 190 | slots: false, 191 | }, 192 | areThereDependenciesOn: { 193 | props: false, 194 | }, 195 | setupVarNames: {}, 196 | }; 197 | 198 | ast.areThereDependenciesOn.props = analyzeIfPropsReferenced(ast); 199 | 200 | if (setupBlock) { 201 | const normalizedSetup = toArrowFunctionExpression(setupBlock); 202 | ast.areThereDependenciesOn.props = ast.areThereDependenciesOn.props || analyzeSetupPropsReferenced(normalizedSetup); 203 | const { statements, names } = analyzeSetup(normalizedSetup); 204 | ast.afterPropsStatements.push( 205 | ...statements, 206 | ); 207 | ast.setupVarNames = names; 208 | } 209 | 210 | if (computedBlock && localPiniaMapStoresName) { 211 | for (const node of analyzePiniaMapStores(computedBlock, localPiniaMapStoresName)) { 212 | ast.piniaStores[node.storeFunctionName] = node; 213 | } 214 | } 215 | 216 | if (methodsBlock && localPiniaMapActionsName) { 217 | const res = analyzePiniaActions(methodsBlock, localPiniaMapActionsName); 218 | for (const node of res.stores) { 219 | ast.piniaStores[node.storeFunctionName] = node; 220 | } 221 | 222 | for (const action of res.actions) { 223 | ast.piniaActions[action.name] = action; 224 | } 225 | } 226 | 227 | if (computedBlock && localPiniaMapStateName) { 228 | const res = analyzePiniaMapState(computedBlock, localPiniaMapStateName); 229 | for (const node of res.stores) { 230 | ast.piniaStores[node.storeFunctionName] = node; 231 | } 232 | 233 | for (const state of res.state) { 234 | ast.piniaStates[state.name] = state; 235 | } 236 | } 237 | 238 | if (computedBlock && localPiniaMapWritableStateName) { 239 | const res = analyzePiniaMapWriteableState(computedBlock, localPiniaMapWritableStateName); 240 | for (const node of res.stores) { 241 | ast.piniaStores[node.storeFunctionName] = node; 242 | } 243 | 244 | ast.piniaWritableStates.push(...res.state); 245 | } 246 | 247 | if (computedBlock && localPiniaMapGettersName) { 248 | // mapGetters is a subset of mapState 249 | const res = analyzePiniaMapState(computedBlock, localPiniaMapGettersName); 250 | for (const node of res.stores) { 251 | ast.piniaStores[node.storeFunctionName] = node; 252 | } 253 | 254 | for (const state of res.state) { 255 | ast.piniaStates[state.name] = state; 256 | } 257 | } 258 | 259 | // now that refs/props have been analyzed, detect watcher source types 260 | analyzeWatcherSourceTypes(ast); 261 | 262 | return ast; 263 | } 264 | -------------------------------------------------------------------------------- /src/analyze/inject.ts: -------------------------------------------------------------------------------- 1 | import { 2 | namedTypes as n, 3 | builders as b, 4 | } from 'vue-metamorph'; 5 | import type { InjectNode } from '../ast'; 6 | import { 7 | getStringKey, 8 | isPattern, 9 | isStringKey, 10 | } from './utils'; 11 | 12 | export function analyzeInject(injectBlock: n.ArrayExpression | n.ObjectExpression) { 13 | const nodes: InjectNode[] = []; 14 | 15 | if (injectBlock.type === 'ArrayExpression') { 16 | for (const inject of injectBlock.elements) { 17 | if ( 18 | inject?.type !== 'Literal' 19 | || typeof inject.value !== 'string' 20 | ) { 21 | continue; 22 | } 23 | nodes.push({ 24 | type: 'inject', 25 | dependencies: [], 26 | name: inject.value, 27 | injectionKey: inject, 28 | node: null, 29 | defaultValue: null, 30 | }); 31 | } 32 | } else { 33 | for (const inject of injectBlock.properties) { 34 | if (inject.type !== 'Property' 35 | || !isStringKey(inject.key) 36 | ) { 37 | continue; 38 | } 39 | 40 | if (inject.value.type === 'Literal' || inject.value.type === 'Identifier') { 41 | nodes.push({ 42 | type: 'inject', 43 | defaultValue: null, 44 | dependencies: [], 45 | injectionKey: inject.value, 46 | name: getStringKey(inject.key), 47 | node: null, 48 | }); 49 | } else if (inject.value.type === 'ObjectExpression') { 50 | let injectionKey: InjectNode['injectionKey'] = inject.key; 51 | let defaultValue: InjectNode['defaultValue'] = null; 52 | 53 | let hasFrom = false; 54 | 55 | for (const prop of inject.value.properties) { 56 | if (prop.type !== 'Property' 57 | || isPattern(prop.value) 58 | ) { 59 | continue; 60 | } 61 | 62 | if (prop.key.type === 'Identifier') { 63 | switch (prop.key.name) { 64 | case 'from': { 65 | if (prop.value.type === 'Literal' || prop.value.type === 'Identifier') { 66 | injectionKey = prop.value; 67 | } 68 | 69 | hasFrom = true; 70 | break; 71 | } 72 | 73 | case 'default': { 74 | defaultValue = prop.value; 75 | break; 76 | } 77 | 78 | default: 79 | } 80 | } 81 | } 82 | 83 | nodes.push({ 84 | defaultValue, 85 | dependencies: [], 86 | injectionKey: !hasFrom && injectionKey.type === 'Identifier' 87 | ? b.literal(injectionKey.name) 88 | : injectionKey, 89 | name: getStringKey(inject.key), 90 | node: null, 91 | type: 'inject', 92 | }); 93 | } 94 | } 95 | } 96 | 97 | return nodes; 98 | } 99 | -------------------------------------------------------------------------------- /src/analyze/lifecycle.ts: -------------------------------------------------------------------------------- 1 | import { namedTypes as n } from 'vue-metamorph'; 2 | import type { LifecycleHookNode } from '../ast'; 3 | import { analyzeDependencies } from './dependencies'; 4 | import { 5 | getStringKey, 6 | isStringKey, 7 | normalizeLifecycleHookName, 8 | toArrowFunctionExpression, 9 | } from './utils'; 10 | 11 | export function analyzeLifecycleHooks(optionsBlock: n.ObjectExpression): LifecycleHookNode[] { 12 | // created hook is not handled here 13 | const nodes: LifecycleHookNode[] = []; 14 | 15 | for (const option of optionsBlock.properties) { 16 | if (option.type === 'Property' 17 | && isStringKey(option.key) 18 | && (option.value.type === 'ArrowFunctionExpression' || option.value.type === 'FunctionExpression') 19 | && normalizeLifecycleHookName(getStringKey(option.key))) { 20 | nodes.push({ 21 | dependencies: [], 22 | name: normalizeLifecycleHookName(getStringKey(option.key))!, 23 | node: toArrowFunctionExpression(option.value), 24 | type: 'lifecycle', 25 | }); 26 | } 27 | } 28 | 29 | nodes.forEach((node) => { 30 | node.dependencies = analyzeDependencies(node); 31 | }); 32 | 33 | return nodes; 34 | } 35 | -------------------------------------------------------------------------------- /src/analyze/methods.ts: -------------------------------------------------------------------------------- 1 | import { namedTypes as n } from 'vue-metamorph'; 2 | import type { MethodNode } from '../ast'; 3 | import { analyzeDependencies } from './dependencies'; 4 | import { 5 | getStringKey, 6 | isPattern, 7 | isStringKey, 8 | toArrowFunctionExpression, 9 | } from './utils'; 10 | 11 | export function analyzeMethods(methods: n.ObjectExpression): MethodNode[] { 12 | const nodes: MethodNode[] = []; 13 | 14 | methods.properties 15 | .filter( 16 | (prop): prop is n.Property & { key: n.Identifier | (n.Literal & { value: string; }); } => prop.type === 'Property' 17 | && isStringKey(prop.key), 18 | ) 19 | .forEach((prop) => { 20 | if (isPattern(prop.value)) { 21 | return; 22 | } 23 | 24 | nodes.push({ 25 | dependencies: [], 26 | name: getStringKey(prop.key), 27 | node: prop.value.type === 'FunctionExpression' 28 | ? toArrowFunctionExpression(prop.value) 29 | : prop.value, 30 | type: 'method', 31 | comments: prop.comments, 32 | }); 33 | }); 34 | 35 | nodes.forEach((node) => { 36 | node.dependencies = analyzeDependencies(node); 37 | }); 38 | 39 | return nodes; 40 | } 41 | -------------------------------------------------------------------------------- /src/analyze/options.ts: -------------------------------------------------------------------------------- 1 | import { 2 | namedTypes as n, 3 | builders as b, 4 | } from 'vue-metamorph'; 5 | import type { OptionsNode } from '../ast'; 6 | import { 7 | getStringKey, 8 | isStringKey, 9 | } from './utils'; 10 | 11 | const vueOptions = [ 12 | 'compatConfig', 13 | 'data', 14 | 'computed', 15 | 'methods', 16 | 'watch', 17 | 'provide', 18 | 'inject', 19 | 'filters', 20 | 'mixins', 21 | 'extends', 22 | 'beforeCreate', 23 | 'created', 24 | 'beforeMount', 25 | 'mounted', 26 | 'beforeUpdate', 27 | 'updated', 28 | 'activated', 29 | 'deactivated', 30 | 'beforeDestroy', 31 | 'beforeUnmount', 32 | 'destroyed', 33 | 'unmounted', 34 | 'renderTracked', 35 | 'renderTriggered', 36 | 'errorCaptured', 37 | 'name', 38 | 'setup', 39 | 'template', 40 | 'components', 41 | 'directives', 42 | 'inheritAttrs', 43 | 'emits', 44 | 'slots', 45 | 'expose', 46 | 'props', 47 | ].reduce>((prev, cur) => { 48 | prev[cur] = true; 49 | return prev; 50 | }, {}); 51 | 52 | export function analyzeOptions(options: n.ObjectExpression): OptionsNode | null { 53 | const node: OptionsNode = { 54 | type: 'options', 55 | dependencies: [], 56 | name: '$options', 57 | node: b.objectExpression([]), 58 | }; 59 | 60 | for (const prop of options.properties) { 61 | if (prop.type === 'Property' 62 | && isStringKey(prop.key) 63 | && vueOptions[getStringKey(prop.key)] 64 | ) { 65 | continue; 66 | } 67 | 68 | node.node.properties.push(prop); 69 | } 70 | 71 | if (node.node.properties.length > 0) { 72 | return node; 73 | } 74 | 75 | return null; 76 | } 77 | -------------------------------------------------------------------------------- /src/analyze/pinia-actions.ts: -------------------------------------------------------------------------------- 1 | import { namedTypes as n } from 'vue-metamorph'; 2 | import type { 3 | PiniaActionNode, 4 | PiniaStoreNode, 5 | } from '../ast'; 6 | import { buildStoreNode } from './pinia-mapstores'; 7 | import { 8 | getStringKey, 9 | isStringKey, 10 | } from './utils'; 11 | 12 | export function analyzePiniaActions( 13 | methodsBlock: n.ObjectExpression, 14 | mapActionsLocalName: string, 15 | ) { 16 | const actions: PiniaActionNode[] = []; 17 | const stores: PiniaStoreNode[] = []; 18 | 19 | for (const method of methodsBlock.properties) { 20 | if (method.type === 'SpreadElement' 21 | && method.argument.type === 'CallExpression' 22 | && method.argument.callee.type === 'Identifier' 23 | && method.argument.callee.name === mapActionsLocalName 24 | && method.argument.arguments[0]?.type === 'Identifier' 25 | && (method.argument.arguments[1]?.type === 'ArrayExpression' 26 | || method.argument.arguments[1]?.type === 'ObjectExpression' 27 | )) { 28 | const { name } = method.argument.arguments[0]; 29 | const store = buildStoreNode(name); 30 | stores.push(store); 31 | 32 | const definition = method.argument.arguments[1]; 33 | 34 | if (definition.type === 'ArrayExpression') { 35 | for (const item of definition.elements) { 36 | if (item?.type === 'Literal' && typeof item.value === 'string') { 37 | actions.push({ 38 | actionName: item.value, 39 | dependencies: [store.name], 40 | name: item.value, 41 | node: null, 42 | type: 'piniaAction', 43 | storeName: store.name, 44 | }); 45 | } 46 | } 47 | } else { 48 | for (const prop of definition.properties) { 49 | if (prop.type !== 'Property' 50 | || !isStringKey(prop.key) 51 | || prop.value.type !== 'Literal' 52 | || typeof prop.value.value !== 'string' 53 | ) { 54 | continue; 55 | } 56 | 57 | actions.push({ 58 | actionName: prop.value.value, 59 | dependencies: [store.name], 60 | name: getStringKey(prop.key), 61 | node: null, 62 | storeName: store.name, 63 | type: 'piniaAction', 64 | }); 65 | } 66 | } 67 | } 68 | } 69 | 70 | return { 71 | actions, 72 | stores, 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /src/analyze/pinia-mapstate.ts: -------------------------------------------------------------------------------- 1 | import { 2 | namedTypes as n, 3 | traverseScriptAST, 4 | } from 'vue-metamorph'; 5 | import type { 6 | PiniaStateNode, 7 | PiniaStoreNode, 8 | } from '../ast'; 9 | import { buildStoreNode } from './pinia-mapstores'; 10 | import { analyzeDependencies } from './dependencies'; 11 | import { 12 | getStringKey, 13 | isStringKey, 14 | toArrowFunctionExpression, 15 | } from './utils'; 16 | 17 | export function analyzePiniaMapState(computed: n.ObjectExpression, localMapStateName: string) { 18 | const state: PiniaStateNode[] = []; 19 | const stores: PiniaStoreNode[] = []; 20 | 21 | for (const computedProp of computed.properties) { 22 | if (computedProp.type === 'SpreadElement' 23 | && computedProp.argument.type === 'CallExpression' 24 | && computedProp.argument.callee.type === 'Identifier' 25 | && computedProp.argument.callee.name === localMapStateName 26 | && computedProp.argument.arguments[0]?.type === 'Identifier' 27 | && (computedProp.argument.arguments[1]?.type === 'ArrayExpression' || computedProp.argument.arguments[1]?.type === 'ObjectExpression') 28 | ) { 29 | const storeName = computedProp.argument.arguments[0].name; 30 | const store = buildStoreNode(storeName); 31 | stores.push(store); 32 | 33 | const definition = computedProp.argument.arguments[1]; 34 | 35 | if (definition.type === 'ArrayExpression') { 36 | for (const el of definition.elements) { 37 | if (!el) { 38 | continue; 39 | } 40 | 41 | if (el.type === 'Literal' && typeof el.value === 'string') { 42 | state.push({ 43 | dependencies: [store.name], 44 | name: el.value, 45 | node: el, 46 | storeName: store.name, 47 | type: 'piniaState', 48 | }); 49 | } 50 | } 51 | } else { 52 | for (const prop of definition.properties) { 53 | if (prop.type !== 'Property' 54 | || !isStringKey(prop.key) 55 | ) { 56 | continue; 57 | } 58 | 59 | if (prop.value.type === 'Literal' 60 | && typeof prop.value.value === 'string') { 61 | state.push({ 62 | dependencies: [store.name], 63 | name: getStringKey(prop.key), 64 | node: prop.value, 65 | storeName: store.name, 66 | type: 'piniaState', 67 | }); 68 | } 69 | 70 | if (prop.value.type === 'FunctionExpression' || prop.value.type === 'ArrowFunctionExpression') { 71 | if (prop.value.params[0]?.type === 'Identifier') { 72 | const paramName = prop.value.params[0].name; 73 | traverseScriptAST(prop.value.body, { 74 | visitMemberExpression(path) { 75 | if (path.node.object.type === 'Identifier' 76 | && path.node.object.name === paramName 77 | ) { 78 | path.node.object.name = store.name; 79 | return false; 80 | } 81 | 82 | return this.traverse(path); 83 | }, 84 | }); 85 | 86 | prop.value.params = []; 87 | 88 | state.push({ 89 | dependencies: [store.name], 90 | name: getStringKey(prop.key), 91 | node: toArrowFunctionExpression(prop.value), 92 | storeName: store.name, 93 | type: 'piniaState', 94 | }); 95 | } 96 | } 97 | } 98 | } 99 | } 100 | } 101 | 102 | state.forEach((node) => { 103 | node.dependencies.push(...analyzeDependencies(node)); 104 | }); 105 | 106 | return { 107 | state, 108 | stores, 109 | }; 110 | } 111 | -------------------------------------------------------------------------------- /src/analyze/pinia-mapstores.ts: -------------------------------------------------------------------------------- 1 | import { namedTypes } from 'vue-metamorph'; 2 | import { camelCase } from 'change-case'; 3 | import type { PiniaStoreNode } from '../ast'; 4 | 5 | export function buildStoreNode(name: string): PiniaStoreNode { 6 | return { 7 | dependencies: [], 8 | name: camelCase(name.replace(/^use/, '')), 9 | storeFunctionName: name, 10 | type: 'piniaStore', 11 | node: null, 12 | }; 13 | } 14 | 15 | export function analyzePiniaMapStores(computed: namedTypes.ObjectExpression, mapStoresName: string) { 16 | const nodes: PiniaStoreNode[] = []; 17 | 18 | for (const prop of computed.properties) { 19 | if (prop.type === 'SpreadElement' 20 | && prop.argument.type === 'CallExpression' 21 | && prop.argument.callee.type === 'Identifier' 22 | && prop.argument.callee.name === mapStoresName 23 | ) { 24 | for (const arg of prop.argument.arguments) { 25 | if (arg.type === 'Identifier') { 26 | nodes.push(buildStoreNode(arg.name)); 27 | } 28 | } 29 | } 30 | } 31 | 32 | return nodes; 33 | } 34 | -------------------------------------------------------------------------------- /src/analyze/pinia-mapwritablestate.ts: -------------------------------------------------------------------------------- 1 | import { namedTypes as n } from 'vue-metamorph'; 2 | import type { 3 | PiniaWritableStateNode, 4 | PiniaStoreNode, 5 | } from '../ast'; 6 | import { buildStoreNode } from './pinia-mapstores'; 7 | import { 8 | getStringKey, 9 | isStringKey, 10 | } from './utils'; 11 | 12 | export function analyzePiniaMapWriteableState(computed: n.ObjectExpression, localMapWriteableStateName: string) { 13 | const state: PiniaWritableStateNode[] = []; 14 | const stores: PiniaStoreNode[] = []; 15 | 16 | for (const computedProp of computed.properties) { 17 | if (computedProp.type === 'SpreadElement' 18 | && computedProp.argument.type === 'CallExpression' 19 | && computedProp.argument.callee.type === 'Identifier' 20 | && computedProp.argument.callee.name === localMapWriteableStateName 21 | && computedProp.argument.arguments[0]?.type === 'Identifier' 22 | && (computedProp.argument.arguments[1]?.type === 'ArrayExpression' || computedProp.argument.arguments[1]?.type === 'ObjectExpression') 23 | ) { 24 | const storeName = computedProp.argument.arguments[0].name; 25 | const store = buildStoreNode(storeName); 26 | stores.push(store); 27 | 28 | const definition = computedProp.argument.arguments[1]; 29 | 30 | if (definition.type === 'ArrayExpression') { 31 | for (const el of definition.elements) { 32 | if (!el) { 33 | continue; 34 | } 35 | 36 | if (el.type === 'Literal' && typeof el.value === 'string') { 37 | state.push({ 38 | dependencies: [store.name], 39 | name: el.value, 40 | node: null, 41 | storeName: store.name, 42 | type: 'piniaWritableState', 43 | stateName: el.value, 44 | }); 45 | } 46 | } 47 | } else { 48 | for (const prop of definition.properties) { 49 | if (prop.type !== 'Property' 50 | || !isStringKey(prop.key) 51 | || prop.value.type !== 'Literal' 52 | || typeof prop.value.value !== 'string' 53 | ) { 54 | continue; 55 | } 56 | 57 | state.push({ 58 | dependencies: [store.name], 59 | name: getStringKey(prop.key), 60 | node: null, 61 | stateName: prop.value.value, 62 | storeName: store.name, 63 | type: 'piniaWritableState', 64 | }); 65 | } 66 | } 67 | } 68 | } 69 | 70 | return { 71 | state, 72 | stores, 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /src/analyze/props.ts: -------------------------------------------------------------------------------- 1 | import { namedTypes as n } from 'vue-metamorph'; 2 | import type { 3 | PropsNode, 4 | ScriptSetupAst, 5 | } from '../ast'; 6 | import { analyzeDependencies } from './dependencies'; 7 | import { 8 | getStringKey, 9 | isStringKey, 10 | } from './utils'; 11 | 12 | export function analyzeProps(props: n.ObjectExpression): PropsNode[] { 13 | const nodes: PropsNode[] = []; 14 | 15 | props.properties.forEach((prop) => { 16 | if (prop.type === 'Property' && isStringKey(prop.key)) { 17 | nodes.push({ 18 | name: getStringKey(prop.key), 19 | dependencies: [], 20 | node: prop, 21 | type: 'prop', 22 | comments: prop.comments, 23 | }); 24 | } 25 | }); 26 | 27 | nodes.forEach((node) => { 28 | node.dependencies = analyzeDependencies(node); 29 | }); 30 | 31 | return nodes; 32 | } 33 | 34 | /** 35 | * Checks if props are referenced by other nodes. 36 | * 37 | * Used to determine whether to emit defineProps() as a variable declaration or a top-level function call 38 | */ 39 | export function analyzeIfPropsReferenced(ast: ScriptSetupAst): boolean { 40 | if (!ast.props) { 41 | return false; 42 | } 43 | 44 | const dependencyNodes = [ 45 | ...ast.data, 46 | ...ast.lifecycleHooks, 47 | ...ast.watchers, 48 | ...ast.createdHook ? [ast.createdHook] : [], 49 | ...ast.methods, 50 | ...ast.computed, 51 | ...ast.provides, 52 | ]; 53 | 54 | const propsNames = ast.props?.reduce>((acc, cur) => { 55 | acc[cur.name] = true; 56 | return acc; 57 | }, {}); 58 | 59 | for (const node of dependencyNodes) { 60 | for (const dep of node.dependencies) { 61 | if (propsNames[dep]) { 62 | return true; 63 | } 64 | } 65 | } 66 | 67 | return false; 68 | } 69 | -------------------------------------------------------------------------------- /src/analyze/provide.ts: -------------------------------------------------------------------------------- 1 | import { 2 | namedTypes as n, 3 | builders as b, 4 | traverseScriptAST, 5 | } from 'vue-metamorph'; 6 | import type { ProvideNode } from '../ast'; 7 | import { 8 | getStringKey, 9 | isPattern, 10 | isStringKey, 11 | } from './utils'; 12 | import { analyzeDependencies } from './dependencies'; 13 | 14 | export function analyzeProvide(provide: n.ObjectExpression | n.ArrowFunctionExpression | n.FunctionExpression) { 15 | const nodes: ProvideNode[] = []; 16 | 17 | let properties: n.ObjectExpression['properties'] | undefined; 18 | 19 | if (provide.type === 'ObjectExpression' 20 | || provide.body.type === 'ObjectExpression' 21 | ) { 22 | properties = provide.type === 'ObjectExpression' 23 | ? provide.properties 24 | : (provide.body as n.ObjectExpression).properties; 25 | } else { 26 | traverseScriptAST(provide.body, { 27 | visitReturnStatement(path) { 28 | if (path.node.argument?.type === 'ObjectExpression') { 29 | properties = path.node.argument.properties; 30 | return false; 31 | } 32 | 33 | return this.traverse(path); 34 | }, 35 | }); 36 | } 37 | 38 | if (!properties) { 39 | throw new Error('Could not determine properties of provide block'); 40 | } 41 | 42 | for (const prop of properties) { 43 | if (prop.type === 'Property' 44 | && isStringKey(prop.key) 45 | && !isPattern(prop.value) 46 | ) { 47 | nodes.push({ 48 | type: 'provide', 49 | dependencies: [], 50 | name: `provide-${getStringKey(prop.key)}`, 51 | key: prop.key, 52 | node: b.expressionStatement(prop.value), 53 | computed: prop.computed ?? false, 54 | }); 55 | } 56 | } 57 | 58 | nodes.forEach((node) => { 59 | node.dependencies = analyzeDependencies(node); 60 | }); 61 | 62 | return nodes; 63 | } 64 | -------------------------------------------------------------------------------- /src/analyze/refs.ts: -------------------------------------------------------------------------------- 1 | import { 2 | namedTypes as n, 3 | traverseScriptAST, 4 | } from 'vue-metamorph'; 5 | import type { RefsNode } from '../ast'; 6 | import { 7 | getStringKey, 8 | isStringKey, 9 | isThisDotRefs, 10 | } from './utils'; 11 | 12 | export const refName = (s: string) => `${s}$`; 13 | 14 | export function analyzeRefs(options: n.ObjectExpression) { 15 | const nodes: RefsNode[] = []; 16 | 17 | const refs: Record = {}; 18 | 19 | traverseScriptAST(options, { 20 | visitVariableDeclaration(path) { 21 | for (const declarator of path.node.declarations) { 22 | if ( 23 | declarator.type !== 'VariableDeclarator' 24 | || declarator.id.type !== 'ObjectPattern' 25 | || !declarator.init 26 | || !isThisDotRefs(declarator.init) 27 | ) { 28 | continue; 29 | } 30 | 31 | for (const prop of declarator.id.properties) { 32 | if (prop.type !== 'Property' 33 | || prop.key.type !== 'Identifier' 34 | || prop.value.type !== 'Identifier' 35 | ) { 36 | continue; 37 | } 38 | 39 | const name = refName(prop.value.name); 40 | 41 | if (!refs[name]) { 42 | nodes.push({ 43 | dependencies: [], 44 | name, 45 | node: null, 46 | type: 'refs', 47 | }); 48 | 49 | refs[name] = true; 50 | } 51 | } 52 | } 53 | this.traverse(path); 54 | }, 55 | visitMemberExpression(path) { 56 | if ( 57 | path.node.object.type === 'MemberExpression' 58 | && path.node.object.object.type === 'ThisExpression' 59 | && path.node.object.property.type === 'Identifier' 60 | && path.node.object.property.name === '$refs' 61 | && isStringKey(path.node.property) 62 | && !refs[refName(getStringKey(path.node.property))] 63 | ) { 64 | const name = refName(getStringKey(path.node.property)); 65 | nodes.push({ 66 | dependencies: [], 67 | name, 68 | node: null, 69 | type: 'refs', 70 | }); 71 | refs[name] = true; 72 | } 73 | this.traverse(path); 74 | }, 75 | }); 76 | 77 | return nodes; 78 | } 79 | -------------------------------------------------------------------------------- /src/analyze/setup.ts: -------------------------------------------------------------------------------- 1 | import { 2 | namedTypes as n, 3 | builders as b, 4 | type Kinds, 5 | traverseScriptAST, 6 | } from 'vue-metamorph'; 7 | import { isPattern } from './utils'; 8 | 9 | export function analyzeSetup(setup: n.ArrowFunctionExpression): { 10 | statements: Kinds.StatementKind[]; 11 | names: Record; 12 | } { 13 | if (setup.body.type !== 'BlockStatement') { 14 | return { 15 | statements: [], 16 | names: {}, 17 | }; 18 | } 19 | const statements: Kinds.StatementKind[] = setup.body.body 20 | .filter((stmt) => stmt.type !== 'ReturnStatement'); 21 | 22 | const returnStatement = setup.body.body.find((stmt) => stmt.type === 'ReturnStatement'); 23 | 24 | if (!returnStatement || returnStatement.argument?.type !== 'ObjectExpression') { 25 | return { 26 | statements: [], 27 | names: {}, 28 | }; 29 | } 30 | 31 | const names: Record = {}; 32 | 33 | for (const prop of returnStatement.argument.properties) { 34 | if (prop.type === 'Property') { 35 | if (prop.key.type === 'Identifier' 36 | && prop.value.type === 'Identifier' 37 | && prop.key.name === prop.value.name 38 | ) { 39 | names[prop.key.name] = true; 40 | // case 1: not creating a new variable in the return body 41 | continue; 42 | } 43 | 44 | if (prop.key.type === 'Identifier' 45 | && !isPattern(prop.value)) { 46 | // case 2: setup return defines a property that isn't a variable itself 47 | statements.push( 48 | b.variableDeclaration( 49 | 'const', 50 | [ 51 | b.variableDeclarator( 52 | prop.key, 53 | prop.value, 54 | ), 55 | ], 56 | ), 57 | ); 58 | 59 | names[prop.key.name] = true; 60 | } 61 | } 62 | 63 | if (prop.type === 'SpreadElement' && prop.argument.type === 'Identifier') { 64 | // case 3: spread in return value. we cannot spread variable declarations so this requires manual fix 65 | 66 | const decl = b.variableDeclaration( 67 | 'const', 68 | [ 69 | b.variableDeclarator( 70 | b.identifier(`FIX_ME_SPREAD_${prop.argument.name}`), 71 | prop.argument, 72 | ), 73 | ], 74 | ); 75 | 76 | decl.comments = [ 77 | b.commentLine(` ⚠️ scriptshifter: Could not analyze which variables were created from spreading '${prop.argument.name}' in the setup() return statement`), 78 | ]; 79 | statements.push( 80 | decl, 81 | ); 82 | } 83 | } 84 | 85 | return { 86 | statements, 87 | names, 88 | }; 89 | } 90 | 91 | export function analyzeSetupPropsReferenced(setup: n.ArrowFunctionExpression) { 92 | if (setup.params[0]?.type !== 'Identifier') { 93 | return false; 94 | } 95 | 96 | const propsName = setup.params[0].name; 97 | 98 | let ret = false; 99 | traverseScriptAST(setup.body, { 100 | visitIdentifier(path) { 101 | if (path.node.name === propsName 102 | && !(n.MemberExpression.check(path.parent.node) 103 | && path.parent.node.property.type === 'Identifier' 104 | && path.parent.node.property.name === propsName)) { 105 | ret = true; 106 | } 107 | 108 | this.traverse(path); 109 | }, 110 | }); 111 | 112 | return ret; 113 | } 114 | -------------------------------------------------------------------------------- /src/analyze/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | builders as b, 3 | Kinds, 4 | namedTypes as n, 5 | } from 'vue-metamorph'; 6 | 7 | export function normalizeLifecycleHookName(name: string) { 8 | // created hook is not handled here 9 | const map = { 10 | destroyed: 'onUnmounted', 11 | unmounted: 'onUnmounted', 12 | beforeDestroy: 'onBeforeUnmount', 13 | beforeUnmount: 'onBeforeUnmount', 14 | mounted: 'onMounted', 15 | beforeMount: 'onBeforeMount', 16 | beforeUpdate: 'onBeforeUpdate', 17 | updated: 'onUpdated', 18 | } as const; 19 | 20 | if (!(name in map)) { 21 | return null; 22 | } 23 | 24 | return map[name as keyof typeof map]; 25 | } 26 | 27 | export function isStringKey(p: n.Property['key']): p is n.Identifier | (n.Literal & { value: string; }) { 28 | return (p.type === 'Identifier') 29 | || (p.type === 'Literal' && typeof p.value === 'string'); 30 | } 31 | 32 | export function getStringKey(p: n.Property['key']) { 33 | if (!isStringKey(p)) { 34 | throw new Error('not a string key'); 35 | } 36 | 37 | return p.type === 'Identifier' 38 | ? p.name 39 | : p.value; 40 | } 41 | 42 | export function toArrowFunctionExpression(fn: n.FunctionExpression | n.ArrowFunctionExpression) { 43 | if (fn.type === 'ArrowFunctionExpression') { 44 | return fn; 45 | } 46 | 47 | const expr = b.arrowFunctionExpression( 48 | fn.params, 49 | fn.body, 50 | fn.expression, 51 | ); 52 | 53 | expr.async = fn.async; 54 | expr.returnType = fn.returnType; 55 | expr.typeParameters = fn.typeParameters; 56 | expr.comments = fn.comments; 57 | 58 | return expr; 59 | } 60 | 61 | export const CompoundWatcherRegex = /^\w+\.\w+/; 62 | 63 | export function isPattern(v: n.Property['value']) { 64 | return v.type === 'TSTypeAssertion' 65 | || v.type === 'RestElement' 66 | || v.type === 'SpreadElementPattern' 67 | || v.type === 'PropertyPattern' 68 | || v.type === 'ObjectPattern' 69 | || v.type === 'ArrayPattern' 70 | || v.type === 'AssignmentPattern' 71 | || v.type === 'SpreadPropertyPattern' 72 | || v.type === 'TSParameterProperty'; 73 | } 74 | 75 | export function isThisDotRefs(node: Kinds.ExpressionKind) { 76 | let nn = node; 77 | 78 | if (nn.type === 'TSAsExpression') { 79 | nn = nn.expression; 80 | } 81 | 82 | if (nn.type === 'MemberExpression' 83 | && nn.object.type === 'ThisExpression' 84 | && nn.property.type === 'Identifier' 85 | && nn.property.name === '$refs' 86 | ) { 87 | return true; 88 | } 89 | 90 | return false; 91 | } 92 | -------------------------------------------------------------------------------- /src/analyze/vuex.ts: -------------------------------------------------------------------------------- 1 | import { namedTypes as n } from 'vue-metamorph'; 2 | import type { 3 | VuexActionNode, 4 | VuexGetterNode, 5 | VuexMutationNode, 6 | VuexStateNode, 7 | } from '../ast'; 8 | import { analyzeDependencies } from './dependencies'; 9 | import { 10 | getStringKey, 11 | isStringKey, 12 | toArrowFunctionExpression, 13 | } from './utils'; 14 | 15 | export function analyzeVuexActions(methods: n.ObjectExpression, mapActionsLocalName: string): VuexActionNode[] { 16 | const nodes: VuexActionNode[] = []; 17 | 18 | for (const method of methods.properties) { 19 | if (method.type === 'SpreadElement' 20 | && method.argument.type === 'CallExpression' 21 | && method.argument.callee.type === 'Identifier' 22 | && method.argument.callee.name === mapActionsLocalName 23 | && (method.argument.arguments[0]?.type === 'MemberExpression' 24 | || method.argument.arguments[0]?.type === 'Identifier' 25 | || method.argument.arguments[0]?.type === 'Literal' 26 | || method.argument.arguments[0]?.type === 'CallExpression') 27 | && (method.argument.arguments[1]?.type === 'ArrayExpression' 28 | || method.argument.arguments[1]?.type === 'ObjectExpression')) { 29 | const namespace = method.argument.arguments[0]; 30 | const definition = method.argument.arguments[1]; 31 | 32 | if (definition.type === 'ArrayExpression') { 33 | for (const element of definition.elements) { 34 | if (element?.type === 'Literal' && typeof element.value === 'string') { 35 | nodes.push({ 36 | actionName: element.value, 37 | dependencies: [], 38 | name: element.value, 39 | namespace, 40 | node: null, 41 | type: 'vuexAction', 42 | comments: element.comments, 43 | }); 44 | } 45 | } 46 | } else { 47 | for (const prop of definition.properties) { 48 | if (prop.type === 'Property' 49 | && isStringKey(prop.key) 50 | && prop.value.type === 'Literal' 51 | && typeof prop.value.value === 'string') { 52 | nodes.push({ 53 | actionName: prop.value.value, 54 | dependencies: [], 55 | name: getStringKey(prop.key), 56 | namespace, 57 | node: null, 58 | type: 'vuexAction', 59 | comments: prop.comments, 60 | }); 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | return nodes; 68 | } 69 | 70 | export function analyzeVuexGetters(computed: n.ObjectExpression, mapGettersLocalName: string): VuexGetterNode[] { 71 | const nodes: VuexGetterNode[] = []; 72 | 73 | for (const getter of computed.properties) { 74 | if (getter.type === 'SpreadElement' 75 | && getter.argument.type === 'CallExpression' 76 | && getter.argument.callee.type === 'Identifier' 77 | && getter.argument.callee.name === mapGettersLocalName 78 | && (getter.argument.arguments[0]?.type === 'MemberExpression' 79 | || getter.argument.arguments[0]?.type === 'Identifier' 80 | || getter.argument.arguments[0]?.type === 'Literal' 81 | || getter.argument.arguments[0]?.type === 'CallExpression') 82 | && (getter.argument.arguments[1]?.type === 'ArrayExpression' 83 | || getter.argument.arguments[1]?.type === 'ObjectExpression')) { 84 | const namespace = getter.argument.arguments[0]; 85 | const definition = getter.argument.arguments[1]; 86 | 87 | if (definition.type === 'ArrayExpression') { 88 | for (const element of definition.elements) { 89 | if (element?.type === 'Literal' && typeof element.value === 'string') { 90 | nodes.push({ 91 | getterName: element.value, 92 | dependencies: [], 93 | name: element.value, 94 | namespace, 95 | node: null, 96 | type: 'vuexGetter', 97 | comments: element.comments, 98 | }); 99 | } 100 | } 101 | } else { 102 | for (const prop of definition.properties) { 103 | if (prop.type === 'Property' 104 | && isStringKey(prop.key) 105 | && prop.value.type === 'Literal' 106 | && typeof prop.value.value === 'string') { 107 | nodes.push({ 108 | getterName: prop.value.value, 109 | dependencies: [], 110 | name: getStringKey(prop.key), 111 | namespace, 112 | node: null, 113 | type: 'vuexGetter', 114 | comments: prop.comments, 115 | }); 116 | } 117 | } 118 | } 119 | } 120 | } 121 | 122 | return nodes; 123 | } 124 | 125 | export function analyzeVuexMutations(methods: n.ObjectExpression, mapMutationsLocalName: string): VuexMutationNode[] { 126 | const nodes: VuexMutationNode[] = []; 127 | 128 | for (const method of methods.properties) { 129 | if (method.type === 'SpreadElement' 130 | && method.argument.type === 'CallExpression' 131 | && method.argument.callee.type === 'Identifier' 132 | && method.argument.callee.name === mapMutationsLocalName 133 | && (method.argument.arguments[0]?.type === 'MemberExpression' 134 | || method.argument.arguments[0]?.type === 'Identifier' 135 | || method.argument.arguments[0]?.type === 'Literal' 136 | || method.argument.arguments[0]?.type === 'CallExpression') 137 | && (method.argument.arguments[1]?.type === 'ArrayExpression' 138 | || method.argument.arguments[1]?.type === 'ObjectExpression')) { 139 | const namespace = method.argument.arguments[0]; 140 | const definition = method.argument.arguments[1]; 141 | 142 | if (definition.type === 'ArrayExpression') { 143 | for (const element of definition.elements) { 144 | if (element?.type === 'Literal' && typeof element.value === 'string') { 145 | nodes.push({ 146 | mutationName: element.value, 147 | dependencies: [], 148 | name: element.value, 149 | namespace, 150 | node: null, 151 | type: 'vuexMutation', 152 | comments: element.comments, 153 | }); 154 | } 155 | } 156 | } else { 157 | for (const prop of definition.properties) { 158 | if (prop.type === 'Property' 159 | && isStringKey(prop.key) 160 | && prop.value.type === 'Literal' 161 | && typeof prop.value.value === 'string') { 162 | nodes.push({ 163 | mutationName: prop.value.value, 164 | dependencies: [], 165 | name: getStringKey(prop.key), 166 | namespace, 167 | node: null, 168 | type: 'vuexMutation', 169 | comments: prop.comments, 170 | }); 171 | } 172 | } 173 | } 174 | } 175 | } 176 | 177 | return nodes; 178 | } 179 | 180 | export function analyzeVuexState(computed: n.ObjectExpression, mapStateLocalName: string) { 181 | const nodes: VuexStateNode[] = []; 182 | 183 | for (const prop of computed.properties) { 184 | if (prop.type !== 'SpreadElement' 185 | || prop.argument.type !== 'CallExpression' 186 | || prop.argument.callee.type !== 'Identifier' 187 | || prop.argument.callee.name !== mapStateLocalName 188 | || !prop.argument.arguments[0] 189 | || !prop.argument.arguments[1] 190 | || prop.argument.arguments[0].type === 'SpreadElement' 191 | || prop.argument.arguments[1].type === 'SpreadElement' 192 | ) { 193 | continue; 194 | } 195 | 196 | const namespace = prop.argument.arguments[0]; 197 | const mapping = prop.argument.arguments[1]; 198 | 199 | if (mapping.type === 'ArrayExpression') { 200 | for (const el of mapping.elements) { 201 | if ( 202 | !el 203 | || el.type !== 'Literal' 204 | || typeof el.value !== 'string' 205 | ) { 206 | continue; 207 | } 208 | 209 | nodes.push({ 210 | comments: el.comments, 211 | dependencies: [], 212 | name: el.value, 213 | namespace, 214 | node: el, 215 | type: 'vuexState', 216 | }); 217 | } 218 | } else if (mapping.type === 'ObjectExpression') { 219 | for (const mappingProp of mapping.properties) { 220 | if ( 221 | mappingProp.type !== 'Property' 222 | || !isStringKey(mappingProp.key) 223 | ) { 224 | continue; 225 | } 226 | 227 | const localName = getStringKey(mappingProp.key); 228 | 229 | nodes.push({ 230 | comments: mappingProp.comments, 231 | dependencies: [], 232 | name: localName, 233 | namespace, 234 | node: mappingProp.value.type === 'FunctionExpression' 235 | ? toArrowFunctionExpression(mappingProp.value) 236 | : mappingProp.value as never, 237 | type: 'vuexState', 238 | }); 239 | } 240 | } 241 | } 242 | 243 | nodes.forEach((node) => { 244 | node.dependencies = analyzeDependencies(node); 245 | }); 246 | 247 | return nodes; 248 | } 249 | -------------------------------------------------------------------------------- /src/analyze/watcher-sources.ts: -------------------------------------------------------------------------------- 1 | import type { ScriptSetupAst } from '../ast'; 2 | import { CompoundWatcherRegex } from './utils'; 3 | 4 | export function analyzeWatcherSourceTypes(ast: ScriptSetupAst) { 5 | for (const watcher of ast.watchers) { 6 | const isCompound = CompoundWatcherRegex.test(watcher.watchName); 7 | 8 | let name = watcher.watchName; 9 | if (isCompound) { 10 | const parts = watcher.watchName.split('.'); 11 | name = parts[0]!; 12 | } 13 | 14 | const isRef = ast.data.some((dataNode) => dataNode.name === name); 15 | const isProp = ast.props?.some((propNode) => propNode.name === name) ?? false; 16 | 17 | // eslint-disable-next-line default-case 18 | switch (true) { 19 | case isCompound && isRef: watcher.sourceType = 'compoundRef'; break; 20 | case isCompound && isProp: watcher.sourceType = 'compoundProp'; break; 21 | case !isCompound && isRef: watcher.sourceType = 'ref'; break; 22 | case !isCompound && isProp: watcher.sourceType = 'prop'; break; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/analyze/watchers.ts: -------------------------------------------------------------------------------- 1 | import { namedTypes as n } from 'vue-metamorph'; 2 | import type { WatcherNode } from '../ast'; 3 | import { analyzeDependencies } from './dependencies'; 4 | import { 5 | getStringKey, 6 | isStringKey, 7 | toArrowFunctionExpression, 8 | } from './utils'; 9 | 10 | export function analyzeWatchers(watch: n.ObjectExpression): WatcherNode[] { 11 | const nodes: WatcherNode[] = []; 12 | 13 | for (const watcher of watch.properties) { 14 | if (watcher.type === 'Property' 15 | && (isStringKey(watcher.key)) 16 | && (watcher.value.type === 'FunctionExpression' 17 | || watcher.value.type === 'ObjectExpression' 18 | || watcher.value.type === 'ArrowFunctionExpression')) { 19 | const isDeep = watcher.value.type === 'ObjectExpression' 20 | && watcher.value.properties.some((watchProp) => watchProp.type === 'Property' 21 | && isStringKey(watchProp.key) 22 | && getStringKey(watchProp.key) === 'deep' 23 | && watchProp.value.type === 'Literal' 24 | && watchProp.value.value === true); 25 | 26 | const isImmediate = watcher.value.type === 'ObjectExpression' 27 | && watcher.value.properties.some((watchProp) => watchProp.type === 'Property' 28 | && isStringKey(watchProp.key) 29 | && getStringKey(watchProp.key) === 'immediate' 30 | && watchProp.value.type === 'Literal' 31 | && watchProp.value.value === true); 32 | 33 | const name = getStringKey(watcher.key); 34 | 35 | let handler = watcher.value.type === 'FunctionExpression' 36 | ? toArrowFunctionExpression(watcher.value) 37 | : watcher.value; 38 | 39 | if (watcher.value.type === 'ObjectExpression') { 40 | for (const property of watcher.value.properties) { 41 | if (property.type === 'Property' 42 | && isStringKey(property.key) 43 | && getStringKey(property.key) === 'handler' 44 | && (property.value.type === 'FunctionExpression' 45 | || property.value.type === 'ArrowFunctionExpression')) { 46 | handler = toArrowFunctionExpression(property.value); 47 | } 48 | } 49 | } 50 | 51 | nodes.push({ 52 | dependencies: [], 53 | isDeep, 54 | isImmediate, 55 | name: `${name}_watcher`, 56 | watchName: name, 57 | node: handler, 58 | type: 'watcher', 59 | sourceType: 'ref', // this will be overwritten later 60 | comments: watcher.comments, 61 | }); 62 | } 63 | } 64 | 65 | nodes.forEach((node) => { 66 | node.dependencies = analyzeDependencies(node); 67 | }); 68 | 69 | return nodes; 70 | } 71 | -------------------------------------------------------------------------------- /src/codegen/$options.ts: -------------------------------------------------------------------------------- 1 | import { builders as b } from 'vue-metamorph'; 2 | import type { OptionsNode } from '../ast'; 3 | 4 | export function renderOptionsNode(node: OptionsNode) { 5 | return b.variableDeclaration( 6 | 'const', 7 | [ 8 | b.variableDeclarator( 9 | b.identifier('$options'), 10 | node.node, 11 | ), 12 | ], 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/codegen/$refs.ts: -------------------------------------------------------------------------------- 1 | import { builders as b } from 'vue-metamorph'; 2 | import type { RefsNode } from '../ast'; 3 | import { VueVersion } from '../options'; 4 | import { isVersionLtEq } from '../version'; 5 | 6 | export function renderRefsNode(node: RefsNode, vueVersion: VueVersion, isTypescript: boolean) { 7 | // vue 2.7 / 3.4: const refName = ref(null); 8 | // vue 3.5: const refName = useTemplateRef('refName'); 9 | 10 | const refCall = b.callExpression( 11 | b.identifier('ref'), 12 | [b.literal(null)], 13 | ); 14 | 15 | if (isTypescript) { 16 | refCall.typeParameters = b.tsTypeParameterInstantiation([ 17 | b.tsUnionType([ 18 | b.tsTypeReference(b.identifier('HTMLElement')), 19 | b.tsNullKeyword(), 20 | ]), 21 | 22 | ]); 23 | } 24 | 25 | return b.variableDeclaration( 26 | 'const', 27 | [ 28 | b.variableDeclarator( 29 | b.identifier(node.name), 30 | isVersionLtEq(vueVersion, '3.4') 31 | ? refCall 32 | : b.callExpression( 33 | b.identifier('useTemplateRef'), 34 | [b.literal(node.name)], 35 | ), 36 | ), 37 | ], 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/codegen/computed.ts: -------------------------------------------------------------------------------- 1 | import { builders as b } from 'vue-metamorph'; 2 | import type { ComputedNode } from '../ast'; 3 | 4 | export function renderComputedNode(node: ComputedNode) { 5 | const decl = b.variableDeclaration('const', [ 6 | b.variableDeclarator( 7 | b.identifier(node.name), 8 | b.callExpression( 9 | b.identifier('computed'), 10 | [node.node], 11 | ), 12 | ), 13 | ]); 14 | 15 | decl.comments = node.comments; 16 | 17 | return decl; 18 | } 19 | -------------------------------------------------------------------------------- /src/codegen/data.ts: -------------------------------------------------------------------------------- 1 | import { builders as b } from 'vue-metamorph'; 2 | import type { DataNode } from '../ast'; 3 | 4 | export function renderDataNode(node: DataNode) { 5 | const decl = b.variableDeclaration('const', [ 6 | b.variableDeclarator( 7 | b.identifier(node.name), 8 | b.callExpression( 9 | b.identifier('ref'), 10 | [node.node], 11 | ), 12 | ), 13 | ]); 14 | 15 | decl.comments = node.comments; 16 | 17 | return decl; 18 | } 19 | -------------------------------------------------------------------------------- /src/codegen/directive.ts: -------------------------------------------------------------------------------- 1 | import { builders as b } from 'vue-metamorph'; 2 | import type { DirectiveNode } from '../ast'; 3 | 4 | export const transformDirectiveName = (str: string) => `v${str.slice(0, 1).toUpperCase()}${str.slice(1)}`; 5 | 6 | export function renderDirectiveNode(node: DirectiveNode) { 7 | if (!node.node) { 8 | // this directive is imported and was already handled during transformation 9 | return null; 10 | } 11 | 12 | return b.variableDeclaration( 13 | 'const', 14 | [b.variableDeclarator( 15 | b.identifier(transformDirectiveName(node.name)), 16 | node.node, 17 | )], 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/codegen/emits.ts: -------------------------------------------------------------------------------- 1 | import { builders as b } from 'vue-metamorph'; 2 | import type { EmitsNode } from '../ast'; 3 | 4 | export function renderEmitsNode(node: EmitsNode) { 5 | const decl = b.variableDeclaration('const', [ 6 | b.variableDeclarator( 7 | b.identifier('emit'), 8 | b.callExpression( 9 | b.identifier('defineEmits'), 10 | [node.node], 11 | ), 12 | ), 13 | ]); 14 | 15 | decl.comments = node.comments; 16 | 17 | return decl; 18 | } 19 | -------------------------------------------------------------------------------- /src/codegen/graph.ts: -------------------------------------------------------------------------------- 1 | export class Graph { 2 | nodes: Map = new Map(); 3 | 4 | edges: Map> = new Map(); 5 | 6 | addNode(name: string, data: T) { 7 | if (this.nodes.has(name)) { 8 | throw new Error(`"${name}" was defined more than once`); 9 | } 10 | 11 | this.nodes.set(name, data); 12 | this.edges.set(name, new Set()); 13 | } 14 | 15 | addEdge(to: string, from: string) { 16 | if (!this.nodes.has(from) || !this.nodes.has(to)) { 17 | return; 18 | } 19 | 20 | this.edges.get(from)!.add(to); 21 | } 22 | 23 | cycleTolerantTopSort() { 24 | const order: string[] = []; 25 | const seen: Record = {}; 26 | 27 | const sort = (start: string) => { 28 | if (seen[start]) { 29 | return; 30 | } 31 | 32 | const pathHas: Record = {}; 33 | const path: string[] = []; 34 | const stack: { 35 | node: string; 36 | visiting: boolean; 37 | }[] = []; 38 | 39 | stack.push({ 40 | node: start, 41 | visiting: true, 42 | }); 43 | 44 | while (stack.length > 0) { 45 | const top = stack.at(-1)!; 46 | 47 | if (!top.visiting) { 48 | stack.pop(); 49 | path.pop(); 50 | delete pathHas[top.node]; 51 | seen[top.node] = true; 52 | order.push(top.node); 53 | } else { 54 | if (seen[top.node] || pathHas[top.node]) { 55 | stack.pop(); 56 | continue; 57 | } 58 | 59 | pathHas[top.node] = true; 60 | top.visiting = false; 61 | path.push(top.node); 62 | stack.push( 63 | ...Array.from(this.edges.get(top.node)!) 64 | .map((node) => ({ 65 | node, 66 | visiting: true, 67 | })), 68 | ); 69 | } 70 | } 71 | }; 72 | 73 | Array.from(this.nodes.entries()) 74 | .forEach(([node]) => sort(node)); 75 | 76 | return order.map((node) => this.nodes.get(node)!); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/codegen/index.ts: -------------------------------------------------------------------------------- 1 | import { namedTypes as n, Kinds } from 'vue-metamorph'; 2 | import * as AST from '../ast'; 3 | import { Graph } from './graph'; 4 | import { renderMethodNode } from './method'; 5 | import { renderOptionsNode } from './$options'; 6 | import { renderRefsNode } from './$refs'; 7 | import { renderComputedNode } from './computed'; 8 | import { renderDataNode } from './data'; 9 | import { renderDirectiveNode } from './directive'; 10 | import { renderEmitsNode } from './emits'; 11 | import { renderLifecycleHook } from './lifecycle'; 12 | import { renderPropsNodes } from './props'; 13 | import { renderUnknownNode } from './unknown'; 14 | import { renderVuexActionNode } from './vuex-action'; 15 | import { renderVuexGetterNode } from './vuex-getter'; 16 | import { renderVuexMutationNode } from './vuex-mutation'; 17 | import { renderVuexStateNode } from './vuex-state'; 18 | import { renderWatcherNode } from './watcher'; 19 | import { renderPiniaMapStoresNode } from './pinia-store'; 20 | import { renderPiniaStateNode } from './pinia-state'; 21 | import { VueVersion } from '../options'; 22 | 23 | export function render( 24 | ast: AST.ScriptSetupAst, 25 | vueVersion: VueVersion, 26 | isTypescript: boolean, 27 | ) { 28 | const statements: Kinds.StatementKind[] = []; 29 | 30 | // 1. any statements before the options block 31 | statements.push(...ast.beforeOptionsStatements); 32 | 33 | // 2. $options 34 | if (ast.$options) { 35 | statements.push(renderOptionsNode(ast.$options)); 36 | } 37 | 38 | // 3. unknowns 39 | statements.push(...ast.unknowns.map((node) => renderUnknownNode(node))); 40 | 41 | // 4. props 42 | if (ast.props) { 43 | statements.push(renderPropsNodes(ast.props, ast.areThereDependenciesOn.props)); 44 | } 45 | 46 | // 5. after props statements (existing setup() block) 47 | statements.push(...ast.afterPropsStatements); 48 | 49 | // 6. emits 50 | if (ast.emits) { 51 | statements.push(renderEmitsNode(ast.emits)); 52 | } 53 | 54 | // 7. directives 55 | if (ast.directives) { 56 | statements.push( 57 | ...ast.directives 58 | .map(renderDirectiveNode) 59 | .filter((x): x is n.VariableDeclaration => !!x), 60 | ); 61 | } 62 | 63 | // 8. sorted statements 64 | const graph = new Graph(); 65 | const allNodes = [ 66 | ...ast.computed, 67 | ...ast.data, 68 | ...ast.lifecycleHooks, 69 | ...ast.methods, 70 | ...ast.props ?? [], 71 | ...ast.watchers, 72 | ...ast.vuexActions, 73 | ...ast.vuexGetters, 74 | ...ast.vuexMutations, 75 | ...ast.vuexState, 76 | ...ast.$refs, 77 | ...Object.values(ast.piniaActions), 78 | ...Object.values(ast.piniaStates), 79 | ...Object.values(ast.piniaStores), 80 | ]; 81 | 82 | allNodes.forEach((node) => graph.addNode(node.name, node)); 83 | allNodes.forEach((node) => { 84 | node.dependencies.forEach((dep) => { 85 | graph.addEdge(dep, node.name); 86 | }); 87 | }); 88 | 89 | for (const node of graph.cycleTolerantTopSort()) { 90 | switch (node.type) { 91 | case 'computed': statements.push(renderComputedNode(node)); break; 92 | case 'data': statements.push(renderDataNode(node)); break; 93 | case 'method': statements.push(renderMethodNode(node)); break; 94 | case 'watcher': statements.push(renderWatcherNode(node)); break; 95 | case 'lifecycle': statements.push(renderLifecycleHook(node)); break; 96 | case 'vuexAction': statements.push(renderVuexActionNode(node, isTypescript)); break; 97 | case 'vuexGetter': statements.push(renderVuexGetterNode(node)); break; 98 | case 'vuexMutation': statements.push(renderVuexMutationNode(node, isTypescript)); break; 99 | case 'vuexState': statements.push(renderVuexStateNode(node)); break; 100 | case 'refs': statements.push(renderRefsNode(node, vueVersion, isTypescript)); break; 101 | case 'piniaStore': statements.push(renderPiniaMapStoresNode(node)); break; 102 | case 'piniaState': { 103 | const rendered = renderPiniaStateNode(node); 104 | if (rendered) { 105 | statements.push(rendered); 106 | } 107 | break; 108 | } 109 | default: break; 110 | } 111 | } 112 | 113 | // 9. any statements after the options block 114 | statements.push(...ast.afterOptionsStatements); 115 | 116 | // 10. created hook 117 | if (ast.createdHook) { 118 | statements.push(ast.createdHook.node); 119 | } 120 | 121 | return statements; 122 | } 123 | -------------------------------------------------------------------------------- /src/codegen/lifecycle.ts: -------------------------------------------------------------------------------- 1 | import { builders as b } from 'vue-metamorph'; 2 | import type { LifecycleHookNode } from '../ast'; 3 | 4 | export function renderLifecycleHook(node: LifecycleHookNode) { 5 | return b.expressionStatement( 6 | b.callExpression( 7 | b.identifier(node.name), 8 | [node.node], 9 | ), 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/codegen/method.ts: -------------------------------------------------------------------------------- 1 | import { builders as b } from 'vue-metamorph'; 2 | import type { MethodNode } from '../ast'; 3 | 4 | export function renderMethodNode(node: MethodNode) { 5 | const decl = b.variableDeclaration( 6 | 'const', 7 | [b.variableDeclarator( 8 | b.identifier(node.name), 9 | node.node, 10 | )], 11 | ); 12 | 13 | decl.comments = node.comments; 14 | 15 | return decl; 16 | } 17 | -------------------------------------------------------------------------------- /src/codegen/pinia-state.ts: -------------------------------------------------------------------------------- 1 | import { builders as b } from 'vue-metamorph'; 2 | import type { PiniaStateNode } from '../ast'; 3 | 4 | export function renderPiniaStateNode(node: PiniaStateNode) { 5 | if (node.node.type !== 'ArrowFunctionExpression') { 6 | return null; 7 | } 8 | 9 | return b.variableDeclaration( 10 | 'const', 11 | [ 12 | b.variableDeclarator( 13 | b.identifier(node.name), 14 | b.callExpression( 15 | b.identifier('computed'), 16 | [node.node], 17 | ), 18 | ), 19 | ], 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/codegen/pinia-store.ts: -------------------------------------------------------------------------------- 1 | import { builders as b } from 'vue-metamorph'; 2 | import type { PiniaStoreNode } from '../ast'; 3 | 4 | export function renderPiniaMapStoresNode(node: PiniaStoreNode) { 5 | return b.variableDeclaration( 6 | 'const', 7 | [ 8 | b.variableDeclarator( 9 | b.identifier(node.name), 10 | b.callExpression(b.identifier(node.storeFunctionName), []), 11 | ), 12 | ], 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/codegen/props.ts: -------------------------------------------------------------------------------- 1 | import { builders as b } from 'vue-metamorph'; 2 | import type { PropsNode } from '../ast'; 3 | 4 | export function renderPropsNodes(nodes: PropsNode[], shouldEmitVariable: boolean) { 5 | const defineProps = b.callExpression( 6 | b.identifier('defineProps'), 7 | [b.objectExpression(nodes.map((node) => node.node))], 8 | ); 9 | 10 | return shouldEmitVariable 11 | ? b.variableDeclaration('const', [ 12 | b.variableDeclarator( 13 | b.identifier('props'), 14 | defineProps, 15 | ), 16 | ]) 17 | : b.expressionStatement(defineProps); 18 | } 19 | -------------------------------------------------------------------------------- /src/codegen/unknown.ts: -------------------------------------------------------------------------------- 1 | import { builders as b } from 'vue-metamorph'; 2 | import type { UnknownNode } from '../ast'; 3 | 4 | export function renderUnknownNode(node: UnknownNode) { 5 | const decl = b.variableDeclaration( 6 | 'const', 7 | [b.variableDeclarator( 8 | b.identifier(node.name), 9 | b.identifier('FIX_ME'), 10 | )], 11 | ); 12 | 13 | decl.comments = [ 14 | b.commentLine(` ⚠️ scriptshifter: Could not determine how/where the "${node.name}" variable was defined in this file`), 15 | ]; 16 | 17 | return decl; 18 | } 19 | -------------------------------------------------------------------------------- /src/codegen/vuex-action.ts: -------------------------------------------------------------------------------- 1 | import { builders as b } from 'vue-metamorph'; 2 | import type { VuexActionNode } from '../ast'; 3 | 4 | export function renderVuexActionNode(node: VuexActionNode, isTypescript: boolean) { 5 | const namespacedActionExpression = node.namespace.type === 'Literal' && typeof node.namespace.value === 'string' 6 | ? b.literal(`${node.namespace.value}/${node.actionName}`) 7 | : b.binaryExpression( 8 | '+', 9 | node.namespace, 10 | b.literal(`/${node.actionName}`), 11 | ); 12 | 13 | const varName = b.identifier('payload'); 14 | 15 | if (isTypescript) { 16 | varName.typeAnnotation = b.tsTypeAnnotation(b.tsUnknownKeyword()); 17 | } 18 | 19 | const decl = b.variableDeclaration( 20 | 'const', 21 | [b.variableDeclarator( 22 | b.identifier(node.name), 23 | b.arrowFunctionExpression( 24 | [varName], 25 | b.callExpression( 26 | b.memberExpression( 27 | b.identifier('$store'), 28 | b.identifier('dispatch'), 29 | ), 30 | [ 31 | namespacedActionExpression, 32 | b.identifier('payload'), 33 | ], 34 | ), 35 | ), 36 | )], 37 | ); 38 | 39 | decl.comments = node.comments; 40 | 41 | return decl; 42 | } 43 | -------------------------------------------------------------------------------- /src/codegen/vuex-getter.ts: -------------------------------------------------------------------------------- 1 | import { builders as b } from 'vue-metamorph'; 2 | import type { VuexGetterNode } from '../ast'; 3 | 4 | export function renderVuexGetterNode(node: VuexGetterNode) { 5 | const namespacedGetterExpression = node.namespace.type === 'Literal' && typeof node.namespace.value === 'string' 6 | ? b.literal(`${node.namespace.value}/${node.getterName}`) 7 | : b.binaryExpression( 8 | '+', 9 | node.namespace, 10 | b.literal(`/${node.getterName}`), 11 | ); 12 | 13 | const decl = b.variableDeclaration( 14 | 'const', 15 | [b.variableDeclarator( 16 | b.identifier(node.name), 17 | b.callExpression( 18 | b.identifier('computed'), 19 | [b.arrowFunctionExpression( 20 | [], 21 | b.memberExpression( 22 | b.memberExpression( 23 | b.identifier('$store'), 24 | b.identifier('getters'), 25 | ), 26 | namespacedGetterExpression, 27 | true, 28 | ), 29 | )], 30 | ), 31 | )], 32 | ); 33 | 34 | decl.comments = node.comments; 35 | 36 | return decl; 37 | } 38 | -------------------------------------------------------------------------------- /src/codegen/vuex-mutation.ts: -------------------------------------------------------------------------------- 1 | import { builders as b } from 'vue-metamorph'; 2 | import type { VuexMutationNode } from '../ast'; 3 | 4 | export function renderVuexMutationNode(node: VuexMutationNode, isTypescript: boolean) { 5 | const namespacedMutationExpression = node.namespace.type === 'Literal' && typeof node.namespace.value === 'string' 6 | ? b.literal(`${node.namespace.value}/${node.mutationName}`) 7 | : b.binaryExpression( 8 | '+', 9 | node.namespace, 10 | b.literal(`/${node.mutationName}`), 11 | ); 12 | 13 | const varName = b.identifier('payload'); 14 | 15 | if (isTypescript) { 16 | varName.typeAnnotation = b.tsTypeAnnotation(b.tsUnknownKeyword()); 17 | } 18 | 19 | const decl = b.variableDeclaration( 20 | 'const', 21 | [b.variableDeclarator( 22 | b.identifier(node.name), 23 | b.arrowFunctionExpression( 24 | [varName], 25 | b.blockStatement([ 26 | b.expressionStatement( 27 | b.callExpression( 28 | b.memberExpression( 29 | b.identifier('$store'), 30 | b.identifier('commit'), 31 | ), 32 | [ 33 | namespacedMutationExpression, 34 | b.identifier('payload'), 35 | ], 36 | ), 37 | ), 38 | ]), 39 | ), 40 | )], 41 | ); 42 | 43 | decl.comments = node.comments; 44 | 45 | return decl; 46 | } 47 | -------------------------------------------------------------------------------- /src/codegen/vuex-state.ts: -------------------------------------------------------------------------------- 1 | import { builders as b } from 'vue-metamorph'; 2 | import type { VuexStateNode } from '../ast'; 3 | 4 | export function renderVuexStateNode(node: VuexStateNode) { 5 | const accessHelperCall = b.callExpression( 6 | b.identifier('getVuexState'), 7 | [ 8 | b.memberExpression( 9 | b.identifier('$store'), 10 | b.identifier('state'), 11 | ), 12 | node.namespace, 13 | ], 14 | ); 15 | 16 | if (node.node.type === 'ArrowFunctionExpression') { 17 | const body = node.node.body.type === 'BlockStatement' 18 | ? node.node.body 19 | : b.blockStatement([]); 20 | 21 | const localStateName = node.node.params[0]!; 22 | 23 | body.body.unshift( 24 | b.variableDeclaration( 25 | 'const', 26 | [ 27 | b.variableDeclarator(localStateName, accessHelperCall), 28 | ], 29 | ), 30 | ); 31 | 32 | if (node.node.body.type !== 'BlockStatement') { 33 | body.body.push( 34 | b.returnStatement( 35 | node.node.body, 36 | ), 37 | ); 38 | } 39 | 40 | return b.variableDeclaration( 41 | 'const', 42 | [ 43 | b.variableDeclarator( 44 | b.identifier(node.name), 45 | b.callExpression( 46 | b.identifier('computed'), 47 | [ 48 | b.arrowFunctionExpression([], body), 49 | ], 50 | ), 51 | ), 52 | ], 53 | ); 54 | } 55 | 56 | return b.variableDeclaration( 57 | 'const', 58 | [ 59 | b.variableDeclarator( 60 | b.identifier(node.name), 61 | b.callExpression( 62 | b.identifier('computed'), 63 | [ 64 | b.arrowFunctionExpression( 65 | [], 66 | b.memberExpression( 67 | accessHelperCall, 68 | node.node as never, 69 | true, 70 | ), 71 | ), 72 | ], 73 | ), 74 | ), 75 | ], 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /src/codegen/watcher.ts: -------------------------------------------------------------------------------- 1 | import { builders as b } from 'vue-metamorph'; 2 | import type { WatcherNode } from '../ast'; 3 | 4 | // eslint-disable-next-line consistent-return 5 | export function renderWatcherSource(node: WatcherNode) { 6 | // eslint-disable-next-line default-case 7 | switch (node.sourceType) { 8 | case 'compoundRef': { 9 | const parts = node.watchName.split('.'); 10 | parts.splice(1, 0, 'value'); 11 | return b.arrowFunctionExpression( 12 | [], 13 | b.identifier(parts.join('.')), 14 | ); 15 | } 16 | 17 | case 'compoundProp': return b.arrowFunctionExpression( 18 | [], 19 | b.identifier(`props.${node.watchName}`), 20 | ); 21 | 22 | case 'prop': return b.arrowFunctionExpression( 23 | [], 24 | b.memberExpression( 25 | b.identifier('props'), 26 | b.identifier(node.watchName), 27 | ), 28 | ); 29 | 30 | case 'ref': return b.identifier(node.watchName); 31 | } 32 | } 33 | 34 | export function renderWatcherNode(node: WatcherNode) { 35 | const decl = b.expressionStatement( 36 | b.callExpression( 37 | b.identifier('watch'), 38 | [ 39 | renderWatcherSource(node), 40 | node.node, 41 | ...(node.isDeep || node.isImmediate) ? [ 42 | b.objectExpression([ 43 | ...node.isDeep ? [b.property('init', b.identifier('deep'), b.literal(true))] : [], 44 | ...node.isImmediate ? [b.property('init', b.identifier('immediate'), b.literal(true))] : [], 45 | ]), 46 | ] : [], 47 | ], 48 | ), 49 | ); 50 | 51 | decl.comments = node.comments; 52 | 53 | return decl; 54 | } 55 | -------------------------------------------------------------------------------- /src/compile.ts: -------------------------------------------------------------------------------- 1 | import { 2 | builders, 3 | type CodemodPlugin, 4 | namedTypes as n, 5 | } from 'vue-metamorph'; 6 | import { analyze } from './analyze'; 7 | import { transform } from './transform'; 8 | import { render } from './codegen'; 9 | import { analyzeIncompatibleOptions } from './analyze/incompatible'; 10 | import { VueVersion } from './options'; 11 | 12 | export const scriptshifter: CodemodPlugin = { 13 | type: 'codemod', 14 | name: 'scriptshifter', 15 | transform({ 16 | scriptASTs, 17 | sfcAST, 18 | utils: { astHelpers }, 19 | opts, 20 | filename, 21 | }) { 22 | if (!filename.endsWith('.vue') || !sfcAST || !scriptASTs[0]) { 23 | return 0; 24 | } 25 | 26 | const vueVersion = opts.vue as VueVersion; 27 | 28 | let transformCount = 0; 29 | let incompatibleOptions: n.Program | null = null; 30 | 31 | if (scriptASTs.some((ast) => ast.isScriptSetup)) { 32 | // if this is already a