├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── unit-test.yml ├── .gitignore ├── .npmrc ├── README.md ├── package.json ├── playground ├── index.html ├── package.json ├── public │ └── icon.svg ├── src │ ├── App.vue │ ├── auto-imports.d.ts │ ├── button-types.ts │ ├── components.d.ts │ ├── components │ │ └── Button.vue │ ├── main.ts │ ├── other-types.ts │ ├── test.ts │ └── typings │ │ └── index.ts ├── tsconfig.json └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src ├── core │ ├── ast.ts │ ├── constants.ts │ ├── index.ts │ └── utils.ts ├── index.ts ├── nuxt.ts └── vite.ts ├── test ├── __snapshots__ │ ├── common.test.ts.snap │ └── dynamic.test.ts.snap ├── _presets.ts ├── _utils.ts ├── common.test.ts ├── dynamic.test.ts └── fixtures │ ├── common │ ├── duplicate-imports │ │ ├── export-aliases │ │ │ ├── 1.ts │ │ │ ├── 2.ts │ │ │ ├── multi-level.ts │ │ │ └── types │ │ │ │ ├── 1.ts │ │ │ │ ├── 2.ts │ │ │ │ ├── 3.ts │ │ │ │ └── 4.ts │ │ ├── import-aliases │ │ │ ├── 1.ts │ │ │ ├── 2.ts │ │ │ ├── multi-level.ts │ │ │ └── types │ │ │ │ ├── 1.ts │ │ │ │ ├── 2.ts │ │ │ │ └── 3.ts │ │ └── import-export-default │ │ │ ├── 1.ts │ │ │ ├── 2.ts │ │ │ ├── multi-level.ts │ │ │ └── types │ │ │ ├── 1.ts │ │ │ ├── 2.ts │ │ │ ├── 3.ts │ │ │ └── 4.ts │ ├── enum-types │ │ └── default │ │ │ ├── empty.ts │ │ │ ├── mixed.ts │ │ │ ├── number.ts │ │ │ └── string.ts │ ├── export-aliases │ │ ├── default │ │ │ ├── _types.ts │ │ │ └── index.ts │ │ └── multi-level │ │ │ ├── 1.ts │ │ │ ├── 2.ts │ │ │ └── types │ │ │ └── 1.ts │ ├── export-all │ │ └── default │ │ │ ├── index.ts │ │ │ └── types │ │ │ ├── 1.ts │ │ │ └── 2.ts │ ├── import-aliases │ │ ├── default │ │ │ ├── _types.ts │ │ │ └── index.ts │ │ └── multi-level │ │ │ ├── _type_A.ts │ │ │ ├── _type_B.ts │ │ │ └── index.ts │ ├── import-export-default │ │ ├── default │ │ │ ├── 1.ts │ │ │ └── _types.ts │ │ ├── multi-level │ │ │ ├── 1.ts │ │ │ ├── 2.ts │ │ │ └── types │ │ │ │ ├── 1.ts │ │ │ │ ├── 2.ts │ │ │ │ └── 3.ts │ │ └── use-aliases │ │ │ ├── 1.ts │ │ │ ├── 2.ts │ │ │ ├── 3.ts │ │ │ └── types │ │ │ ├── alias.ts │ │ │ └── default.ts │ ├── import-same-type-implicitly │ │ └── default │ │ │ ├── 1.ts │ │ │ ├── 2.ts │ │ │ └── types │ │ │ ├── 1.ts │ │ │ └── 2.ts │ ├── interface-extends-interface │ │ ├── has-reference │ │ │ ├── 1.ts │ │ │ ├── 2.ts │ │ │ └── 3.ts │ │ └── no-reference │ │ │ └── index.ts │ ├── interface-without-reference │ │ └── default │ │ │ └── index.ts │ ├── mixed-aliases │ │ └── default │ │ │ ├── index.ts │ │ │ └── types │ │ │ ├── 1.ts │ │ │ └── 2.ts │ ├── multi-level-reference │ │ └── default │ │ │ ├── 1.ts │ │ │ └── 2.ts │ ├── redeclaration-of-types │ │ ├── default │ │ │ └── index.ts │ │ └── same-name │ │ │ ├── _type.ts │ │ │ └── index.ts │ ├── reference-in-property │ │ └── default │ │ │ └── index.ts │ └── strict-type-finding │ │ └── default │ │ ├── _types.ts │ │ └── index.ts │ └── dynamic │ ├── enum-types │ └── default │ │ ├── _types.ts │ │ ├── index.vue │ │ └── local.vue │ ├── import-priority │ ├── preferred-dts │ │ ├── _types.d.ts │ │ └── index.vue │ ├── preferred-ts │ │ ├── _types.d.ts │ │ ├── _types.ts │ │ ├── _types.tsx │ │ └── index.vue │ └── preferred-tsx │ │ ├── _types.d.ts │ │ ├── _types.tsx │ │ └── index.vue │ ├── interface-extends-interface │ ├── has-reference │ │ ├── 1.vue │ │ ├── 2.vue │ │ ├── 3.vue │ │ ├── external_1.vue │ │ ├── external_2.vue │ │ ├── external_3.vue │ │ └── externals │ │ │ ├── 1.ts │ │ │ ├── 2.ts │ │ │ └── 3.ts │ └── no-reference │ │ ├── index.ts │ │ ├── index.vue │ │ └── internal.vue │ ├── interface-without-reference │ └── no-transform │ │ └── index.vue │ ├── multi-level-reference │ └── default │ │ ├── 1.vue │ │ └── 2.vue │ └── tsx │ ├── import-tsx │ ├── _types.tsx │ └── index.vue │ └── lang-tsx │ └── index.vue ├── tsconfig.json ├── tsconfig.test.json ├── tsup.config.ts └── vitest.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .d.ts 4 | coverage 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | }, 5 | "extends": ["@antfu"], 6 | "rules": { 7 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], 8 | "no-restricted-imports": [ 9 | "error", 10 | { 11 | "paths": ["vql"] 12 | } 13 | ] 14 | }, 15 | "overrides": [ 16 | { 17 | "files": [ 18 | "playground/**/*.*" 19 | ], 20 | "rules": { 21 | "no-restricted-imports": "off" 22 | } 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /.github/workflows/unit-test.yml: -------------------------------------------------------------------------------- 1 | name: Unit Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | env: 13 | VITEST_SEGFAULT_RETRY: 3 14 | 15 | jobs: 16 | lint: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - name: Setup node 16.x 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 16.x 25 | 26 | - name: Setup ni 27 | run: npm i -g @antfu/ni 28 | 29 | - name: Install 30 | run: nci 31 | 32 | - name: Lint 33 | run: nr lint 34 | 35 | typecheck: 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v3 39 | 40 | - name: Setup node 16.x 41 | uses: actions/setup-node@v3 42 | with: 43 | node-version: 16.x 44 | 45 | - name: Setup ni 46 | run: npm i -g @antfu/ni 47 | 48 | - name: Install 49 | run: nci 50 | 51 | - name: Type Check 52 | run: nr typecheck 53 | 54 | test: 55 | strategy: 56 | matrix: 57 | version: [14.x, 16.x] 58 | os: [ubuntu-latest, windows-latest, macos-latest] 59 | fail-fast: false 60 | 61 | runs-on: ${{ matrix.os }} 62 | steps: 63 | - uses: actions/checkout@v3 64 | 65 | - name: Setup node ${{ matrix.version }} 66 | uses: actions/setup-node@v3 67 | with: 68 | node-version: ${{ matrix.version }} 69 | 70 | - name: Setup ni 71 | run: npm i -g @antfu/ni 72 | 73 | - name: Install 74 | run: nci 75 | 76 | - name: Build 77 | run: nr build 78 | 79 | - name: Unit Test 80 | run: nr test:ci 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | coverage 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | shamefully-hoist=true -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

vite-plugin-vue-type-imports

2 | 3 |

4 | Enables you to import types and use them in your defineProps and defineEmits. Supports both Vue 2 and Vue 3. 5 |

6 | 7 |

8 | NPM version 9 |

10 | 11 | > ⚠️ This Plugin is still in Development and there may be bugs. Use at your own risk. 12 | 13 | ## Install 14 | ```bash 15 | # Install Plugin 16 | npm i -D vite-plugin-vue-type-imports 17 | ``` 18 | 19 | ```ts 20 | // vite.config.ts 21 | 22 | import { defineConfig } from 'vite' 23 | import Vue from '@vitejs/plugin-vue' 24 | import VueTypeImports from 'vite-plugin-vue-type-imports' 25 | 26 | export default defineConfig({ 27 | plugins: [ 28 | Vue(), 29 | VueTypeImports(), 30 | ], 31 | }) 32 | ``` 33 | 34 | ### Nuxt 35 | ```ts 36 | // nuxt.config.ts 37 | 38 | export default { 39 | buildModules: [ 40 | 'vite-plugin-vue-type-imports/nuxt', 41 | ] 42 | } 43 | ``` 44 | 45 | ## Usage 46 | 47 | ```ts 48 | // types.ts 49 | 50 | export interface User { 51 | username: string 52 | password: string 53 | avatar?: string 54 | } 55 | ``` 56 | 57 | ```html 58 | 63 | 64 | 65 | ``` 66 | 67 | ## Known limitations 68 | - Namespace imports like `import * as Foo from 'foo'` are not supported. 69 | - [These types](https://www.typescriptlang.org/docs/handbook/2/types-from-types.html) are not supported. 70 | - The plugin currently only scans the content of ` 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite --open", 7 | "build": "vite build", 8 | "serve": "vite preview" 9 | }, 10 | "devDependencies": { 11 | "@iconify/json": "^2.1.87", 12 | "@unocss/preset-attributify": "^0.45.4", 13 | "@unocss/preset-icons": "^0.45.4", 14 | "@unocss/preset-uno": "^0.45.4", 15 | "@vitejs/plugin-vue": "^3.0.1", 16 | "unocss": "^0.45.4", 17 | "unplugin-auto-import": "^0.10.3", 18 | "unplugin-vue-components": "^0.22.0", 19 | "vite": "^3.0.4", 20 | "vite-plugin-inspect": "^0.6.0", 21 | "vite-plugin-vue-type-imports": "workspace:*" 22 | }, 23 | "dependencies": { 24 | "@vueuse/core": "^9.1.0", 25 | "vue": "^3.2.37" 26 | } 27 | } -------------------------------------------------------------------------------- /playground/public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /playground/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /playground/src/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by 'unplugin-auto-import' 2 | // We suggest you to commit this file into source control 3 | declare global { 4 | const asyncComputed: typeof import('@vueuse/core')['asyncComputed']; 5 | const autoResetRef: typeof import('@vueuse/core')['autoResetRef']; 6 | const biSyncRef: typeof import('@vueuse/core')['biSyncRef']; 7 | const computed: typeof import('vue')['computed']; 8 | const computedInject: typeof import('@vueuse/core')['computedInject']; 9 | const controlledComputed: typeof import('@vueuse/core')['controlledComputed']; 10 | const controlledRef: typeof import('@vueuse/core')['controlledRef']; 11 | const createApp: typeof import('vue')['createApp']; 12 | const createEventHook: typeof import('@vueuse/core')['createEventHook']; 13 | const createGlobalState: typeof import('@vueuse/core')['createGlobalState']; 14 | const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']; 15 | const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']; 16 | const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']; 17 | const customRef: typeof import('vue')['customRef']; 18 | const debouncedRef: typeof import('@vueuse/core')['debouncedRef']; 19 | const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']; 20 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']; 21 | const defineComponent: typeof import('vue')['defineComponent']; 22 | const eagerComputed: typeof import('@vueuse/core')['eagerComputed']; 23 | const effectScope: typeof import('vue')['effectScope']; 24 | const EffectScope: typeof import('vue')['EffectScope']; 25 | const extendRef: typeof import('@vueuse/core')['extendRef']; 26 | const getCurrentInstance: typeof import('vue')['getCurrentInstance']; 27 | const getCurrentScope: typeof import('vue')['getCurrentScope']; 28 | const h: typeof import('vue')['h']; 29 | const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']; 30 | const inject: typeof import('vue')['inject']; 31 | const isDefined: typeof import('@vueuse/core')['isDefined']; 32 | const isReadonly: typeof import('vue')['isReadonly']; 33 | const isRef: typeof import('vue')['isRef']; 34 | const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']; 35 | const markRaw: typeof import('vue')['markRaw']; 36 | const nextTick: typeof import('vue')['nextTick']; 37 | const onActivated: typeof import('vue')['onActivated']; 38 | const onBeforeMount: typeof import('vue')['onBeforeMount']; 39 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']; 40 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']; 41 | const onClickOutside: typeof import('@vueuse/core')['onClickOutside']; 42 | const onDeactivated: typeof import('vue')['onDeactivated']; 43 | const onErrorCaptured: typeof import('vue')['onErrorCaptured']; 44 | const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']; 45 | const onMounted: typeof import('vue')['onMounted']; 46 | const onRenderTracked: typeof import('vue')['onRenderTracked']; 47 | const onRenderTriggered: typeof import('vue')['onRenderTriggered']; 48 | const onScopeDispose: typeof import('vue')['onScopeDispose']; 49 | const onServerPrefetch: typeof import('vue')['onServerPrefetch']; 50 | const onStartTyping: typeof import('@vueuse/core')['onStartTyping']; 51 | const onUnmounted: typeof import('vue')['onUnmounted']; 52 | const onUpdated: typeof import('vue')['onUpdated']; 53 | const pausableWatch: typeof import('@vueuse/core')['pausableWatch']; 54 | const provide: typeof import('vue')['provide']; 55 | const reactify: typeof import('@vueuse/core')['reactify']; 56 | const reactifyObject: typeof import('@vueuse/core')['reactifyObject']; 57 | const reactive: typeof import('vue')['reactive']; 58 | const reactivePick: typeof import('@vueuse/core')['reactivePick']; 59 | const readonly: typeof import('vue')['readonly']; 60 | const ref: typeof import('vue')['ref']; 61 | const refDefault: typeof import('@vueuse/core')['refDefault']; 62 | const resolveComponent: typeof import('vue')['resolveComponent']; 63 | const shallowReactive: typeof import('vue')['shallowReactive']; 64 | const shallowReadonly: typeof import('vue')['shallowReadonly']; 65 | const shallowRef: typeof import('vue')['shallowRef']; 66 | const syncRef: typeof import('@vueuse/core')['syncRef']; 67 | const templateRef: typeof import('@vueuse/core')['templateRef']; 68 | const throttledRef: typeof import('@vueuse/core')['throttledRef']; 69 | const throttledWatch: typeof import('@vueuse/core')['throttledWatch']; 70 | const toRaw: typeof import('vue')['toRaw']; 71 | const toReactive: typeof import('@vueuse/core')['toReactive']; 72 | const toRef: typeof import('vue')['toRef']; 73 | const toRefs: typeof import('vue')['toRefs']; 74 | const triggerRef: typeof import('vue')['triggerRef']; 75 | const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']; 76 | const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']; 77 | const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']; 78 | const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']; 79 | const unref: typeof import('vue')['unref']; 80 | const unrefElement: typeof import('@vueuse/core')['unrefElement']; 81 | const until: typeof import('@vueuse/core')['until']; 82 | const useActiveElement: typeof import('@vueuse/core')['useActiveElement']; 83 | const useAsyncState: typeof import('@vueuse/core')['useAsyncState']; 84 | const useAttrs: typeof import('vue')['useAttrs']; 85 | const useBase64: typeof import('@vueuse/core')['useBase64']; 86 | const useBattery: typeof import('@vueuse/core')['useBattery']; 87 | const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']; 88 | const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']; 89 | const useClipboard: typeof import('@vueuse/core')['useClipboard']; 90 | const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']; 91 | const useCounter: typeof import('@vueuse/core')['useCounter']; 92 | const useCssModule: typeof import('vue')['useCssModule']; 93 | const useCssVar: typeof import('@vueuse/core')['useCssVar']; 94 | const useCssVars: typeof import('vue')['useCssVars']; 95 | const useDark: typeof import('@vueuse/core')['useDark']; 96 | const useDebounce: typeof import('@vueuse/core')['useDebounce']; 97 | const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']; 98 | const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']; 99 | const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']; 100 | const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']; 101 | const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']; 102 | const useDevicesList: typeof import('@vueuse/core')['useDevicesList']; 103 | const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']; 104 | const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']; 105 | const useDraggable: typeof import('@vueuse/core')['useDraggable']; 106 | const useElementBounding: typeof import('@vueuse/core')['useElementBounding']; 107 | const useElementHover: typeof import('@vueuse/core')['useElementHover']; 108 | const useElementSize: typeof import('@vueuse/core')['useElementSize']; 109 | const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']; 110 | const useEventBus: typeof import('@vueuse/core')['useEventBus']; 111 | const useEventListener: typeof import('@vueuse/core')['useEventListener']; 112 | const useEventSource: typeof import('@vueuse/core')['useEventSource']; 113 | const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']; 114 | const useFavicon: typeof import('@vueuse/core')['useFavicon']; 115 | const useFetch: typeof import('@vueuse/core')['useFetch']; 116 | const useFocus: typeof import('@vueuse/core')['useFocus']; 117 | const useFps: typeof import('@vueuse/core')['useFps']; 118 | const useFullscreen: typeof import('@vueuse/core')['useFullscreen']; 119 | const useGeolocation: typeof import('@vueuse/core')['useGeolocation']; 120 | const useIdle: typeof import('@vueuse/core')['useIdle']; 121 | const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']; 122 | const useInterval: typeof import('@vueuse/core')['useInterval']; 123 | const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']; 124 | const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']; 125 | const useLastChanged: typeof import('@vueuse/core')['useLastChanged']; 126 | const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']; 127 | const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']; 128 | const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']; 129 | const useMediaControls: typeof import('@vueuse/core')['useMediaControls']; 130 | const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']; 131 | const useMemory: typeof import('@vueuse/core')['useMemory']; 132 | const useMouse: typeof import('@vueuse/core')['useMouse']; 133 | const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']; 134 | const useMousePressed: typeof import('@vueuse/core')['useMousePressed']; 135 | const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']; 136 | const useNetwork: typeof import('@vueuse/core')['useNetwork']; 137 | const useNow: typeof import('@vueuse/core')['useNow']; 138 | const useOnline: typeof import('@vueuse/core')['useOnline']; 139 | const usePageLeave: typeof import('@vueuse/core')['usePageLeave']; 140 | const useParallax: typeof import('@vueuse/core')['useParallax']; 141 | const usePermission: typeof import('@vueuse/core')['usePermission']; 142 | const usePointer: typeof import('@vueuse/core')['usePointer']; 143 | const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']; 144 | const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']; 145 | const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']; 146 | const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']; 147 | const useRafFn: typeof import('@vueuse/core')['useRafFn']; 148 | const useRefHistory: typeof import('@vueuse/core')['useRefHistory']; 149 | const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']; 150 | const useScriptTag: typeof import('@vueuse/core')['useScriptTag']; 151 | const useScroll: typeof import('@vueuse/core')['useScroll']; 152 | const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']; 153 | const useShare: typeof import('@vueuse/core')['useShare']; 154 | const useSlots: typeof import('vue')['useSlots']; 155 | const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']; 156 | const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']; 157 | const useStorage: typeof import('@vueuse/core')['useStorage']; 158 | const useSwipe: typeof import('@vueuse/core')['useSwipe']; 159 | const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']; 160 | const useThrottle: typeof import('@vueuse/core')['useThrottle']; 161 | const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']; 162 | const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']; 163 | const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']; 164 | const useTimeout: typeof import('@vueuse/core')['useTimeout']; 165 | const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']; 166 | const useTimestamp: typeof import('@vueuse/core')['useTimestamp']; 167 | const useTitle: typeof import('@vueuse/core')['useTitle']; 168 | const useToggle: typeof import('@vueuse/core')['useToggle']; 169 | const useTransition: typeof import('@vueuse/core')['useTransition']; 170 | const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']; 171 | const useUserMedia: typeof import('@vueuse/core')['useUserMedia']; 172 | const useVirtualList: typeof import('@vueuse/core')['useVirtualList']; 173 | const useVModel: typeof import('@vueuse/core')['useVModel']; 174 | const useVModels: typeof import('@vueuse/core')['useVModels']; 175 | const useWakeLock: typeof import('@vueuse/core')['useWakeLock']; 176 | const useWebSocket: typeof import('@vueuse/core')['useWebSocket']; 177 | const useWebWorker: typeof import('@vueuse/core')['useWebWorker']; 178 | const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']; 179 | const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']; 180 | const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']; 181 | const useWindowSize: typeof import('@vueuse/core')['useWindowSize']; 182 | const watch: typeof import('vue')['watch']; 183 | const watchAtMost: typeof import('@vueuse/core')['watchAtMost']; 184 | const watchEffect: typeof import('vue')['watchEffect']; 185 | const watchOnce: typeof import('@vueuse/core')['watchOnce']; 186 | const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']; 187 | const whenever: typeof import('@vueuse/core')['whenever']; 188 | } 189 | export {}; 190 | -------------------------------------------------------------------------------- /playground/src/button-types.ts: -------------------------------------------------------------------------------- 1 | import type { Color } from './other-types' 2 | export { ButtonProps } from './other-types' 3 | 4 | export interface InputProps { 5 | name: Color 6 | } 7 | 8 | export interface ButtonEmits { 9 | (e: 'click'): void 10 | } 11 | -------------------------------------------------------------------------------- /playground/src/components.d.ts: -------------------------------------------------------------------------------- 1 | // generated by unplugin-vue-components 2 | // We suggest you to commit this file into source control 3 | // Read more: https://github.com/vuejs/vue-next/pull/3399 4 | import '@vue/runtime-core'; 5 | 6 | declare module '@vue/runtime-core' { 7 | export interface GlobalComponents { 8 | Button: typeof import('./components/Button.vue')['default']; 9 | } 10 | } 11 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /playground/src/components/Button.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 20 | -------------------------------------------------------------------------------- /playground/src/main.ts: -------------------------------------------------------------------------------- 1 | import '@unocss/reset/tailwind.css' 2 | import 'uno:icons.css' 3 | import 'uno.css' 4 | 5 | import { createApp } from 'vue' 6 | import App from './App.vue' 7 | 8 | createApp(App).mount('#app') 9 | -------------------------------------------------------------------------------- /playground/src/other-types.ts: -------------------------------------------------------------------------------- 1 | import type { MoreColors } from '~/test' 2 | 3 | export type Color = 'blue' | 'red' | MoreColors 4 | 5 | export interface ButtonProps { 6 | color: Color 7 | } 8 | -------------------------------------------------------------------------------- /playground/src/test.ts: -------------------------------------------------------------------------------- 1 | export type MoreColors = 'green' | number 2 | -------------------------------------------------------------------------------- /playground/src/typings/index.ts: -------------------------------------------------------------------------------- 1 | type Foo = [[number, number], [number, number]] 2 | type Bar = Foo 3 | 4 | export interface Props { 5 | foo: Foo 6 | bar: Bar 7 | } 8 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "target": "esnext", 6 | "useDefineForClassFields": true, 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "strict": true, 10 | "jsx": "preserve", 11 | "sourceMap": true, 12 | "resolveJsonModule": true, 13 | "esModuleInterop": true, 14 | "paths": { 15 | "~/*": ["src/*"] 16 | }, 17 | "lib": ["esnext", "dom"] 18 | }, 19 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] 20 | } -------------------------------------------------------------------------------- /playground/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { defineConfig } from 'vite' 3 | import Vue from '@vitejs/plugin-vue' 4 | import Components from 'unplugin-vue-components/vite' 5 | import AutoImport from 'unplugin-auto-import/vite' 6 | import Unocss from 'unocss/vite' 7 | import { presetAttributify, presetIcons, presetUno } from 'unocss' 8 | import VueTypeImports from 'vite-plugin-vue-type-imports' 9 | import Inspect from 'vite-plugin-inspect' 10 | 11 | export default defineConfig({ 12 | resolve: { 13 | alias: { 14 | '~/': `${path.resolve(__dirname, 'src')}/`, 15 | }, 16 | }, 17 | plugins: [ 18 | Vue(), 19 | VueTypeImports(), 20 | Unocss({ 21 | presets: [presetUno(), presetIcons(), presetAttributify()], 22 | }), 23 | Components({ 24 | dirs: ['src/components'], 25 | dts: 'src/components.d.ts', 26 | }), 27 | AutoImport({ 28 | imports: ['vue', '@vueuse/core'], 29 | dts: 'src/auto-imports.d.ts', 30 | }), 31 | Inspect(), 32 | ], 33 | }) 34 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - playground 3 | -------------------------------------------------------------------------------- /src/core/ast.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-use-before-define */ 2 | import fs from 'fs' 3 | import type { 4 | ExportDefaultDeclaration, 5 | ExportNamedDeclaration, 6 | ImportDeclaration, 7 | Node, 8 | Program, 9 | TSEnumDeclaration, 10 | TSExpressionWithTypeArguments, 11 | TSInterfaceDeclaration, 12 | TSTypeAliasDeclaration, 13 | TSTypeLiteral, 14 | TSTypeParameterInstantiation, 15 | TSTypeReference, 16 | TSUnionType, 17 | } from '@babel/types' 18 | import type { 19 | FullName, 20 | GroupedImportsResult, 21 | MaybeAliases, 22 | MaybeNumber, 23 | NameWithPath, 24 | Replacement, 25 | TSTypes, 26 | } from './utils' 27 | import { 28 | at, 29 | convertExportsToImports, 30 | debuggerFactory, 31 | getAst, 32 | groupImports, 33 | isDefineEmits, 34 | isDefineProps, 35 | isEnum, 36 | isNumber, 37 | isString, 38 | isTSTypes, 39 | isWithDefaults, 40 | resolveModulePath, 41 | } from './utils' 42 | 43 | const enum Prefixes { 44 | Default = '_VTI_TYPE_', 45 | Empty = '', 46 | } 47 | 48 | export interface IImport { 49 | start: number 50 | end: number 51 | local: string 52 | imported: string 53 | path: string 54 | } 55 | 56 | export interface IExport { 57 | start: number 58 | end: number 59 | local: string 60 | exported: string 61 | path?: string 62 | } 63 | 64 | /** 65 | * @example 66 | * ```typescript 67 | * export { Foo } 68 | * ``` 69 | */ 70 | export type INamedExport = Omit 71 | 72 | /** 73 | * @example 74 | * ```typescript 75 | * export { Foo } from 'foo' 76 | * ``` 77 | */ 78 | export type INamedFromExport = Required 79 | 80 | export type TypeInfo = Partial> 81 | 82 | export type GetTypesResult = (string | TypeInfo)[] 83 | 84 | export interface GetImportsResult { 85 | imports: IImport[] 86 | importNodes: ImportDeclaration[] 87 | } 88 | 89 | export interface GetExportsResult { 90 | namedExports: INamedExport[] 91 | namedFromExports: INamedFromExport[] 92 | exportAllSources: string[] 93 | } 94 | 95 | export type NodeMap = Map 96 | 97 | const createAstDebugger = debuggerFactory('AST') 98 | 99 | export function getAvailableImportsFromAst(ast: Program): GetImportsResult { 100 | const imports: IImport[] = [] 101 | const importNodes: ImportDeclaration[] = [] 102 | 103 | const addImport = (node: ImportDeclaration) => { 104 | for (const specifier of node.specifiers) { 105 | if (specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier') { 106 | imports.push({ 107 | start: specifier.imported.start!, 108 | end: specifier.local.end!, 109 | imported: specifier.imported.name, 110 | local: specifier.local.name, 111 | path: node.source.value, 112 | }) 113 | } 114 | else if (specifier.type === 'ImportDefaultSpecifier') { 115 | imports.push({ 116 | start: specifier.local.start!, 117 | end: specifier.local.end!, 118 | imported: 'default', 119 | local: specifier.local.name, 120 | path: node.source.value, 121 | }) 122 | } 123 | } 124 | 125 | importNodes.push(node) 126 | } 127 | 128 | for (const node of ast.body) { 129 | if (node.type === 'ImportDeclaration' && node.specifiers.length && node.source.value) 130 | addImport(node) 131 | } 132 | 133 | return { imports, importNodes } 134 | } 135 | 136 | export function getAvailableExportsFromAst(ast: Program): GetExportsResult { 137 | const namedExports: INamedExport[] = [] 138 | const namedFromExports: INamedFromExport[] = [] 139 | const exportAllSources: string[] = [] 140 | 141 | const addExport = (node: ExportNamedDeclaration) => { 142 | for (const specifier of node.specifiers) { 143 | if (specifier.type === 'ExportSpecifier' && specifier.exported.type === 'Identifier') { 144 | if (node.source) { 145 | namedFromExports.push({ 146 | start: specifier.local.start!, 147 | end: specifier.exported.end!, 148 | exported: specifier.exported.name, 149 | local: specifier.local.name, 150 | path: node.source.value, 151 | }) 152 | } 153 | else { 154 | namedExports.push({ 155 | start: specifier.local.start!, 156 | end: specifier.exported.end!, 157 | exported: specifier.exported.name, 158 | local: specifier.local.name, 159 | }) 160 | } 161 | } 162 | } 163 | } 164 | 165 | const addDefaultExport = (node: ExportDefaultDeclaration) => { 166 | if (node.declaration.type === 'Identifier') { 167 | namedExports.push({ 168 | start: node.declaration.start!, 169 | end: node.declaration.end!, 170 | exported: 'default', 171 | local: node.declaration.name, 172 | }) 173 | } 174 | } 175 | 176 | for (const node of ast.body) { 177 | // TODO(zorin): support export * from 178 | if (node.type === 'ExportNamedDeclaration') 179 | addExport(node) 180 | else if (node.type === 'ExportDefaultDeclaration') 181 | addDefaultExport(node) 182 | else if (node.type === 'ExportAllDeclaration') 183 | exportAllSources.push(node.source.value) 184 | } 185 | 186 | return { 187 | namedExports, 188 | namedFromExports, 189 | exportAllSources, 190 | } 191 | } 192 | 193 | export function getUsedInterfacesFromAst(ast: Program) { 194 | const interfaces: string[] = [] 195 | 196 | const addInterface = (node: Node) => { 197 | if (node.type === 'CallExpression' && node.typeParameters?.type === 'TSTypeParameterInstantiation') { 198 | const propsTypeDefinition = node.typeParameters.params[0] 199 | 200 | if (propsTypeDefinition.type === 'TSTypeReference' && propsTypeDefinition.typeName.type === 'Identifier') 201 | interfaces.push(propsTypeDefinition.typeName.name) 202 | 203 | // TODO(zorin): Support nested type params 204 | // if (propsTypeDefinition.typeParameters) 205 | // interfaces.push(...getTypesFromTypeParameters(propsTypeDefinition.typeParameters)); 206 | } 207 | } 208 | 209 | for (const node of ast.body) { 210 | if (node.type === 'ExpressionStatement') { 211 | if (isWithDefaults(node.expression)) 212 | addInterface(node.expression.arguments[0]) 213 | else if (isDefineProps(node.expression) || isDefineEmits(node.expression)) 214 | addInterface(node.expression) 215 | } 216 | 217 | if (node.type === 'VariableDeclaration' && !node.declare) { 218 | for (const decl of node.declarations) { 219 | if (decl.init) { 220 | if (isWithDefaults(decl.init)) 221 | addInterface(decl.init.arguments[0]) 222 | else if (isDefineProps(decl.init) || isDefineEmits(decl.init)) 223 | addInterface(decl.init) 224 | } 225 | } 226 | } 227 | } 228 | 229 | return interfaces 230 | } 231 | 232 | function getTypesFromTypeParameters(x: TSTypeParameterInstantiation) { 233 | const types: GetTypesResult = [] 234 | 235 | for (const p of x.params) { 236 | if (p.type === 'TSTypeLiteral') { types.push(...getTSTypeLiteralTypes(p)) } 237 | else if (p.type === 'TSTypeReference') { 238 | if (p.typeName.type === 'Identifier') 239 | types.push(p.typeName.name) 240 | } 241 | } 242 | 243 | return types 244 | } 245 | 246 | function getTSTypeLiteralTypes(x: TSTypeLiteral) { 247 | const types: GetTypesResult = [] 248 | 249 | for (const m of x.members) { 250 | if (m.type === 'TSPropertySignature') { 251 | if (m.typeAnnotation?.typeAnnotation.type === 'TSTypeLiteral') { 252 | types.push(...getTSTypeLiteralTypes(m.typeAnnotation.typeAnnotation)) 253 | } 254 | else if (m.typeAnnotation?.typeAnnotation.type === 'TSTypeReference') { 255 | if (m.typeAnnotation.typeAnnotation.typeName.type === 'Identifier') { 256 | // TODO(zorin): understand why we push a object 257 | types.push({ 258 | type: m.typeAnnotation.typeAnnotation.type, 259 | name: m.typeAnnotation.typeAnnotation.typeName.name, 260 | }) 261 | } 262 | 263 | if (m.typeAnnotation.typeAnnotation.typeParameters) 264 | types.push(...getTypesFromTypeParameters(m.typeAnnotation.typeAnnotation.typeParameters)) 265 | } 266 | else { 267 | types.push({ type: m.typeAnnotation?.typeAnnotation.type }) 268 | } 269 | } 270 | } 271 | 272 | return types 273 | } 274 | 275 | function extractAllTypescriptTypesFromAST(ast: Program): Record<'local' | 'exported', TSTypes[]> { 276 | const local: TSTypes[] = [] 277 | const exported: TSTypes[] = [] 278 | 279 | ast.body 280 | .forEach((node) => { 281 | // e.g. 'export interface | type | enum' 282 | if (node.type === 'ExportNamedDeclaration' && node.declaration && isTSTypes(node.declaration)) { 283 | local.push(node.declaration) 284 | exported.push(node.declaration) 285 | } 286 | 287 | // e.g. 'interface | type | enum' 288 | if (isTSTypes(node)) 289 | local.push(node) 290 | }) 291 | 292 | return { 293 | local, 294 | exported, 295 | } 296 | } 297 | 298 | export interface LocalTypeMetaData { 299 | // The source type which reference current type 300 | referenceSource?: string 301 | // The interface which extends current interface 302 | extendTarget?: string 303 | // Whether it is used in vue macros 304 | isUsedType?: boolean 305 | hasDuplicateImports?: boolean 306 | } 307 | 308 | export interface ReplacementRecord { 309 | target: FullName 310 | source: NameWithPath 311 | } 312 | 313 | export type TypeMetaData = Omit & { 314 | replacementTargets?: ReplacementRecord[] 315 | } 316 | 317 | export type ReferenceTypeMetaData = LocalTypeMetaData & { referenceSource: NameWithPath } 318 | 319 | export interface ExtractedTypeReplacement { 320 | offset: number 321 | replacements: Replacement[] 322 | } 323 | 324 | export interface ExtractedTypeInfo { 325 | typeKeyword: 'type' | 'interface' 326 | fullName: string 327 | body: string 328 | dependencies?: string[] 329 | } 330 | 331 | export type ExtractedTypes = Map 332 | 333 | export interface ExtractTypesFromSourceOptions { 334 | relativePath: string 335 | pathAliases: MaybeAliases 336 | metaDataMap: Record 337 | extractAliases?: Record 338 | // Data shared across recursions 339 | extraSpecifiers?: string[] 340 | extractedKeysCounter?: Record 341 | extractedNamesMap?: Record 342 | extractedTypes?: ExtractedTypes 343 | extractedTypeReplacements?: Record 344 | interfaceExtendsRecord?: Record 345 | // SFC only (i.e. Arguments passed only on the first call to the function) 346 | ast?: Program 347 | isInSFC?: boolean 348 | } 349 | 350 | export interface ExtractResult { 351 | result: ExtractedTypes 352 | namesMap: Record 353 | typeReplacements: Record 354 | extendsRecord: Record 355 | importNodes: ImportDeclaration[] 356 | extraSpecifiers: string[] 357 | sourceReplacements: Replacement[] 358 | } 359 | 360 | interface PreExtractionOptions { 361 | name: string 362 | metaData: ReferenceTypeMetaData 363 | replaceLocation: Omit 364 | } 365 | 366 | interface PostExtractionOptions { 367 | key: NameWithPath 368 | name: string 369 | fullName: FullName 370 | metaData: LocalTypeMetaData 371 | isEnum?: boolean 372 | } 373 | 374 | /** 375 | * Given a specific source file, extract the specified types. 376 | */ 377 | export async function extractTypesFromSource( 378 | source: string, 379 | types: string[], 380 | options: ExtractTypesFromSourceOptions, 381 | ): Promise { 382 | const { 383 | relativePath, 384 | pathAliases, 385 | extractAliases = {}, 386 | metaDataMap, 387 | 388 | extractedKeysCounter = {}, 389 | extractedNamesMap = {}, 390 | extractedTypes = new Map(), 391 | extractedTypeReplacements = {}, 392 | interfaceExtendsRecord = {}, 393 | extraSpecifiers = [], 394 | 395 | ast = getAst(source), 396 | isInSFC = false, 397 | } = options 398 | 399 | const debug = createAstDebugger('extractTypesFromSource') 400 | 401 | const missingTypes: Record<'local' | 'requested', string[]> = { 402 | local: [], 403 | requested: [], 404 | } 405 | 406 | const localMetaDataMap: Record = metaDataMap 407 | const replacementRecord: Record = {} 408 | 409 | debug('In SFC: %o', isInSFC) 410 | debug('Types to find: %o', types) 411 | debug('Local metadata map: %O', localMetaDataMap) 412 | debug('Extract aliases: %O', extractAliases) 413 | 414 | // Get external types 415 | const { imports, importNodes } = getAvailableImportsFromAst(ast) 416 | 417 | const { namedExports, namedFromExports, exportAllSources } = getAvailableExportsFromAst(ast) 418 | 419 | const hasExportAllDecl = !!exportAllSources.length 420 | 421 | debug('Relative path: %s', relativePath) 422 | debug('Counter: %O', extractedKeysCounter) 423 | 424 | // Categorize imports 425 | const groupedImportsResult = groupImports(imports, source, relativePath) 426 | 427 | const { localNodeMap, exportedNodeMap } = getTSNodeMap(extractAllTypescriptTypesFromAST(ast), namedExports) 428 | 429 | // local -> exported[] 430 | const exportAliasRecord: Record = {} 431 | 432 | // exported -> local 433 | const exportAliases = namedExports.reduce>((res, e) => { 434 | if (e.local !== e.exported) 435 | res[e.exported] = e.local 436 | 437 | return res 438 | }, {}) 439 | 440 | debug('Export aliases: %O', exportAliases) 441 | 442 | // local -> exported 443 | const reversedExportAliases = types.reduce>((res, maybeAlias) => { 444 | const localName = exportAliases[maybeAlias] 445 | 446 | if (!localName) { 447 | // Skip when it is exactly the local name 448 | return res 449 | } 450 | 451 | const alias = res[localName] 452 | 453 | // Add replacements to the existing's if we have already found an alias 454 | if (isString(alias)) { 455 | debug('Add replacements of %s to %s', maybeAlias, localName) 456 | 457 | const targetRecord = getSharedMetaData(localName)!.replacementTargets! 458 | 459 | const sourceRecord = getSharedMetaData(maybeAlias)!.replacementTargets! 460 | 461 | emptyMetaData(maybeAlias) 462 | 463 | targetRecord.push(...sourceRecord) 464 | } 465 | else { 466 | res[localName] = maybeAlias 467 | 468 | patchDataFromName(localName, maybeAlias) 469 | } 470 | 471 | exportAliasRecord[localName] ||= [] 472 | exportAliasRecord[localName].push(maybeAlias) 473 | 474 | return res 475 | }, {}) 476 | 477 | Object.entries(exportAliasRecord).forEach(([typeName, record]) => { 478 | // Add metadata if it has multiple aliases 479 | if (record.length > 1) { 480 | setMetaData(typeName, { 481 | hasDuplicateImports: true, 482 | }) 483 | } 484 | }) 485 | 486 | debug('Reversed export aliases: %O', reversedExportAliases) 487 | 488 | // Unwrap export aliases (exported -> local) to find types (and dedupe them) 489 | const processedTypes = [...new Set(types.map(name => exportAliases[name] || name))] 490 | 491 | debug('Processed types: %O', processedTypes) 492 | 493 | // SFC only variables (i.e. It will only be used in the first recursion) 494 | const sourceReplacements: Replacement[] = [] 495 | 496 | const extractFromPosition = (start: MaybeNumber, end: MaybeNumber) => 497 | isNumber(start) && isNumber(end) ? source.slice(start, end) : '' 498 | 499 | /** 500 | * Check if the given type name is included in the types that user (or previous recursion) requests to find 501 | */ 502 | function isRequestedType(name: string) { 503 | return processedTypes.includes(name) 504 | } 505 | 506 | function withAlias(name: string): string { 507 | return extractAliases[name] || name 508 | } 509 | 510 | function withPath(name: string): NameWithPath { 511 | return `${relativePath}:${name}` 512 | } 513 | 514 | function unwrapPath(key: NameWithPath): string { 515 | return at(key.split(':'), -1) 516 | } 517 | 518 | function setNamesMap(fullName: FullName, nameWithPath: NameWithPath) { 519 | debug('Set names map: %s => %O', fullName, nameWithPath) 520 | extractedNamesMap[fullName] = nameWithPath 521 | } 522 | 523 | function getSharedMetaData(name: string): TypeMetaData | undefined { 524 | return localMetaDataMap[name] 525 | } 526 | 527 | function getMetaData(name: string): LocalTypeMetaData | undefined { 528 | return localMetaDataMap[name] 529 | } 530 | 531 | function getReplacementRecord(name: string): ReplacementRecord[] | undefined { 532 | return getSharedMetaData(name)!.replacementTargets 533 | } 534 | 535 | function setMetaData(name: string, val: LocalTypeMetaData): LocalTypeMetaData { 536 | const metaData = localMetaDataMap[name] 537 | 538 | if (metaData) { 539 | const mergedMetaData: LocalTypeMetaData = { 540 | ...metaData, 541 | ...val, 542 | } 543 | 544 | debug('Overriding metadata: %s => %O => %O', name, metaData, mergedMetaData) 545 | return localMetaDataMap[name] = mergedMetaData 546 | } 547 | else { 548 | debug('Set metadata: %s => %O', name, val) 549 | return localMetaDataMap[name] = val 550 | } 551 | } 552 | 553 | function emptyMetaData(name: string) { 554 | debug('Empty metadata %s', name) 555 | localMetaDataMap[name] = undefined 556 | } 557 | 558 | function patchDataFromName(name: string, from: string) { 559 | debug('Patching data for %s from %s', name, from) 560 | 561 | const sourceRecord = getSharedMetaData(name)?.replacementTargets 562 | 563 | const targetRecord = (setMetaData(name, getSharedMetaData(from)!) as TypeMetaData).replacementTargets! 564 | 565 | emptyMetaData(from) 566 | 567 | /** 568 | * If sourceRecord exists, it means that user also imported the original (local) name of the type 569 | * @example 570 | * ```typescript 571 | * import { Foo, Bar } from './foo' 572 | * ``` 573 | * foo.ts 574 | * ```typescript 575 | * export { Foo, Foo as Bar } 576 | * ``` 577 | */ 578 | if (sourceRecord?.length) { 579 | targetRecord.push(...sourceRecord) 580 | 581 | exportAliasRecord[name] ||= [] 582 | exportAliasRecord[name].push(name) 583 | } 584 | 585 | extractAliases[name] = extractAliases[from] || from 586 | } 587 | 588 | function getCount(name: string): number { 589 | return extractedKeysCounter[withAlias(name)] || 0 590 | } 591 | 592 | function addCount(name: string): number { 593 | const aliasedName = withAlias(name) 594 | debug('Add count: %s', aliasedName) 595 | return extractedKeysCounter[aliasedName] = 1 + getCount(aliasedName) 596 | } 597 | 598 | function getSuffix(name: string, offset = 0): string { 599 | const count = getCount(name) + 1 + offset 600 | 601 | return count > 1 ? `_${count}` : '' 602 | } 603 | 604 | function getFullName(name: string): string { 605 | const { isUsedType } = getMetaData(name) || {} 606 | 607 | const key = withPath(name) 608 | const { fullName } = extractedTypes.get(key) || {} 609 | 610 | // Return the fullName when the type is already extracted from current file 611 | if (fullName) { 612 | debug('Existing fullName: %s', fullName) 613 | return fullName 614 | } 615 | 616 | let prefix = Prefixes.Default 617 | const suffix = getSuffix(name) 618 | const node = localNodeMap.get(name) 619 | 620 | /** 621 | * NOTE(zorin): Do not prefix types used directly in vue macros or types declared in SFC (except enum types) 622 | */ 623 | if (isUsedType || (isInSFC && node && !isEnum(node))) 624 | prefix = Prefixes.Empty 625 | 626 | const result = `${prefix}${withAlias(name)}${suffix}` 627 | 628 | debug('FullName: %s (%s)', result, name) 629 | 630 | return result 631 | } 632 | 633 | function convertFullNameToName(fullName: FullName): string { 634 | const fullNameRE = /(_VTI_TYPE_)?([\w\d$_]+)/g 635 | 636 | const matches = fullName.matchAll(fullNameRE) 637 | 638 | let result = '' 639 | 640 | for (const m of matches) { 641 | const arr = m[2].split('_') 642 | 643 | // Remove prefix if exists 644 | if (isNumber(parseInt(at(arr, -1)))) 645 | arr.pop() 646 | 647 | result = arr.join('_') 648 | } 649 | 650 | return result 651 | } 652 | 653 | function addReplacementRecord(name: string, record: ReplacementRecord) { 654 | debug('Add replacement record: %s -> %O', name, record) 655 | 656 | replacementRecord[name] ||= [] 657 | 658 | replacementRecord[name].push(record) 659 | } 660 | 661 | function addTypeReplacement(key: string, replacement: Replacement) { 662 | debug('Add type replacement: %s => %O', key, replacement) 663 | 664 | extractedTypeReplacements[key].replacements.push(replacement) 665 | } 666 | 667 | function addDependencyToType(key: string, dependency: string) { 668 | debug('Add dependency: %s => %s', key, dependency) 669 | 670 | const extractedTypeInfo = extractedTypes.get(key)! 671 | 672 | extractedTypeInfo.dependencies!.push(dependency) 673 | } 674 | 675 | // Change the name (the content of replacements) to the already extracted's 676 | function changeReplacementContent(replacementRecord: ReplacementRecord[], fullName: FullName) { 677 | const record: Record = {} 678 | 679 | replacementRecord.forEach(({ target, source }) => { 680 | if (target === fullName || record[source]?.includes(target)) 681 | return 682 | 683 | record[source] ||= [] 684 | record[source].push(target) 685 | 686 | const sourceTypeInfo = extractedTypes.get(source)! 687 | 688 | const replacement = extractedTypeReplacements[source] 689 | 690 | replacement.replacements = replacement.replacements.map((r) => { 691 | if (r.replacement === target) { 692 | debug('Change name: %s -> %s', r.replacement, fullName) 693 | 694 | return { 695 | ...r, 696 | replacement: fullName, 697 | } 698 | } 699 | 700 | return r 701 | }) 702 | 703 | sourceTypeInfo.dependencies = sourceTypeInfo.dependencies!.map(dep => dep === target ? fullName : dep) 704 | }) 705 | } 706 | 707 | function removeTypeFromSource(node: Exclude) { 708 | debug('Remove type "%s" from source', node.id.name) 709 | 710 | sourceReplacements.push({ 711 | start: node.start!, 712 | end: node.end!, 713 | replacement: '', 714 | }) 715 | } 716 | 717 | function getTSNodeMap({ local, exported }: Record<'local' | 'exported', TSTypes[]>, namedExports: INamedExport[]): Record<'localNodeMap' | 'exportedNodeMap', NodeMap> { 718 | const localNodeMap = new Map() 719 | const exportedNodeMap = new Map() 720 | 721 | for (const node of local) { 722 | if (isString(node.id.name)) 723 | localNodeMap.set(node.id.name, node) 724 | } 725 | 726 | for (const node of exported) { 727 | if (isString(node.id.name)) 728 | exportedNodeMap.set(node.id.name, node) 729 | } 730 | 731 | for (const e of namedExports) { 732 | const node = localNodeMap.get(e.local) 733 | 734 | if (node && isString(node.id.name)) 735 | exportedNodeMap.set(node.id.name, node) 736 | } 737 | 738 | return { 739 | localNodeMap, 740 | exportedNodeMap, 741 | } 742 | } 743 | 744 | function ExtractTypeByNode(node: TSTypes, fullName: string) { 745 | switch (node.type) { 746 | // Types e.g. export Type Color = 'red' | 'blue' 747 | case 'TSTypeAliasDeclaration': { 748 | extractTypesFromTypeAlias(node, fullName) 749 | break 750 | } 751 | // Interfaces e.g. export interface MyInterface {} 752 | case 'TSInterfaceDeclaration': { 753 | extractTypesFromInterface(node, fullName) 754 | break 755 | } 756 | // Enums e.g. export enum UserType {} 757 | case 'TSEnumDeclaration': { 758 | extractTypesFromEnum(node, fullName) 759 | break 760 | } 761 | } 762 | } 763 | 764 | /** 765 | * Extract ts types by name. 766 | */ 767 | function extractTypeByName(_name: string) { 768 | const name = _name 769 | 770 | const replacementRecord = getReplacementRecord(name) 771 | 772 | const key = withPath(name) 773 | 774 | // Skip types that are already extracted from current file 775 | if (extractedTypes.has(key)) { 776 | debug('Skipping type: %s', name) 777 | 778 | if (replacementRecord?.length) { 779 | const { fullName } = extractedTypes.get(key)! 780 | 781 | changeReplacementContent(replacementRecord, fullName) 782 | } 783 | 784 | return 785 | } 786 | 787 | const node = isRequestedType(name) && !isInSFC ? exportedNodeMap.get(name) : localNodeMap.get(name) 788 | 789 | if (node) { 790 | const fullName = getFullName(name) 791 | 792 | /** 793 | * NOTE(zorin): Remove types from source if we are extracting types in SFC (except enum types), 794 | * because we need to make sure the order is correct 795 | */ 796 | if (isInSFC && !isEnum(node)) 797 | removeTypeFromSource(node) 798 | 799 | ExtractTypeByNode(node, fullName) 800 | } 801 | else { 802 | const exportedName = reversedExportAliases[_name] 803 | const name = exportedName || _name 804 | 805 | if (isString(exportedName)) 806 | setMetaData(exportedName, getMetaData(_name)!) 807 | 808 | if (isInSFC) 809 | extraSpecifiers.push(name) 810 | 811 | debug('Missing type: %s', name) 812 | 813 | if (isRequestedType(_name) && !isInSFC) 814 | missingTypes.requested.push(name) 815 | else 816 | missingTypes.local.push(name) 817 | } 818 | } 819 | 820 | // Recursively calls this function to find types from other modules. 821 | const extractTypesFromModule = async (modulePath: string, types: string[], extractAliases: Record, metaDataMap: Record) => { 822 | const path = await resolveModulePath(modulePath, relativePath, pathAliases) 823 | 824 | if (!path) 825 | return 826 | 827 | /** 828 | * NOTE(zorin): Slow when use fsPromises.readFile(), tested on Arch Linux x64 (Kernel 5.16.11) 829 | * Wondering what make it slow. Temporarily, use fs.readFileSync() instead. 830 | */ 831 | const contents = fs.readFileSync(path, 'utf-8') 832 | 833 | await extractTypesFromSource(contents, types, { 834 | relativePath: path, 835 | pathAliases, 836 | extractAliases, 837 | extractedTypes, 838 | extractedKeysCounter, 839 | extractedNamesMap, 840 | extractedTypeReplacements, 841 | interfaceExtendsRecord, 842 | metaDataMap, 843 | extraSpecifiers, 844 | }) 845 | } 846 | 847 | function preReferenceExtraction(options: PreExtractionOptions): void { 848 | const { name, replaceLocation, metaData } = options 849 | 850 | const { referenceSource } = metaData 851 | 852 | const fullName = getFullName(name) 853 | 854 | addTypeReplacement(referenceSource, { 855 | start: replaceLocation.start, 856 | end: replaceLocation.end, 857 | replacement: fullName, 858 | }) 859 | 860 | addReplacementRecord(name, { 861 | target: fullName, 862 | source: referenceSource, 863 | }) 864 | 865 | addDependencyToType(referenceSource, fullName) 866 | 867 | setMetaData(name, metaData) 868 | } 869 | 870 | function postExtraction(options: PostExtractionOptions): void { 871 | const { key, name, fullName, isEnum, metaData: { isUsedType, hasDuplicateImports } } = options 872 | 873 | setNamesMap(fullName, key) 874 | 875 | /** 876 | * NOTE(zorin):Always add count for enum types 877 | * There are 2 reasons: 878 | * 1. I don't think users will use enum types in vue macros 879 | * 2. Whether they are declared in SFC or not, their names will always be prefixed (Because users may use them as values) 880 | * 881 | * Also, we don't need to add count for types used directly in vue macros or types declared in SFC 882 | * because they are not prefixed 883 | */ 884 | if (isEnum || !(isUsedType || isInSFC)) 885 | addCount(name) 886 | 887 | if (hasDuplicateImports) 888 | changeReplacementContent(getReplacementRecord(name)!, fullName) 889 | } 890 | 891 | const extractTypesFromTSUnionType = (union: TSUnionType, metaData: ReferenceTypeMetaData) => { 892 | const referenceSourceName = unwrapPath(metaData.referenceSource) 893 | 894 | union.types 895 | .filter((n): n is TSTypeReference => n.type === 'TSTypeReference') 896 | .forEach((typeReference) => { 897 | if (typeReference.typeName.type === 'Identifier' && typeReference.typeName.name !== referenceSourceName) { 898 | const name = typeReference.typeName.name 899 | 900 | preReferenceExtraction({ 901 | name, 902 | replaceLocation: { 903 | start: typeReference.start!, 904 | end: typeReference.end!, 905 | }, 906 | metaData: { ...metaData }, 907 | }) 908 | 909 | extractTypeByName(name) 910 | } 911 | }) 912 | } 913 | 914 | function extractExtendInterfaces(interfaces: TSExpressionWithTypeArguments[], metaData: LocalTypeMetaData) { 915 | for (const extend of interfaces) { 916 | if (extend.expression.type === 'Identifier') { 917 | const name = extend.expression.name 918 | setMetaData(name, metaData) 919 | 920 | /** 921 | * TODO(zorin): (Low priority) Add dependency to the source type. 922 | * Currently, If the type is only extended (no additional references), it will not be inlined. 923 | */ 924 | 925 | extractTypeByName(name) 926 | } 927 | } 928 | } 929 | 930 | /** 931 | * Extract ts type interfaces. Should also check top-level properties 932 | * in the interface to look for types to extract 933 | */ 934 | const extractTypesFromInterface = (node: TSInterfaceDeclaration, fullName: string) => { 935 | const interfaceName = node.id.name 936 | const key = withPath(interfaceName) 937 | 938 | const { extendTarget, isUsedType, hasDuplicateImports } = getMetaData(interfaceName) || {} 939 | 940 | const bodyStart = node.body.start! 941 | const bodyEnd = node.body.end! 942 | const offset = -bodyStart 943 | 944 | const extendsInterfaces = node.extends 945 | 946 | extractedTypes.set(key, { 947 | typeKeyword: 'interface', 948 | fullName, 949 | body: extractFromPosition(bodyStart, bodyEnd), 950 | dependencies: [], 951 | }) 952 | 953 | postExtraction({ 954 | key, 955 | fullName, 956 | name: interfaceName, 957 | metaData: { 958 | isUsedType, 959 | hasDuplicateImports, 960 | }, 961 | }) 962 | 963 | if (extendTarget) 964 | interfaceExtendsRecord[extendTarget].push(key) 965 | 966 | if (extendsInterfaces) { 967 | interfaceExtendsRecord[key] = [] 968 | 969 | extractExtendInterfaces(extendsInterfaces, { 970 | extendTarget: key, 971 | }) 972 | } 973 | 974 | const propertyBody = node.body.body 975 | 976 | if (propertyBody.length) { 977 | extractedTypeReplacements[key] = { 978 | offset, 979 | replacements: [], 980 | } 981 | } 982 | 983 | for (const prop of propertyBody) { 984 | if (prop.type === 'TSPropertySignature') { 985 | const typeAnnotation = prop.typeAnnotation?.typeAnnotation 986 | 987 | if (typeAnnotation?.type === 'TSUnionType') { 988 | extractTypesFromTSUnionType(typeAnnotation, { 989 | referenceSource: key, 990 | }) 991 | } 992 | else if ( 993 | typeAnnotation?.type === 'TSTypeReference' 994 | && typeAnnotation.typeName.type === 'Identifier' 995 | && typeAnnotation.typeName.name !== interfaceName 996 | ) { 997 | const name = typeAnnotation.typeName.name 998 | 999 | preReferenceExtraction({ 1000 | name, 1001 | replaceLocation: { 1002 | start: typeAnnotation.start!, 1003 | end: typeAnnotation.end!, 1004 | }, 1005 | metaData: { 1006 | referenceSource: key, 1007 | }, 1008 | }) 1009 | 1010 | extractTypeByName(name) 1011 | } 1012 | } 1013 | } 1014 | } 1015 | 1016 | /** 1017 | * Extract types from TSTypeAlias 1018 | */ 1019 | const extractTypesFromTypeAlias = (node: TSTypeAliasDeclaration, fullName: string) => { 1020 | const typeAliasName = node.id.name 1021 | const key = withPath(typeAliasName) 1022 | const typeAnnotation = node.typeAnnotation 1023 | 1024 | const { isUsedType, hasDuplicateImports } = getMetaData(typeAliasName) || {} 1025 | 1026 | extractedTypes.set(key, { 1027 | typeKeyword: 'type', 1028 | fullName, 1029 | body: extractFromPosition(typeAnnotation.start!, typeAnnotation.end!), 1030 | dependencies: [], 1031 | }) 1032 | 1033 | postExtraction({ 1034 | key, 1035 | fullName, 1036 | name: typeAliasName, 1037 | metaData: { 1038 | isUsedType, 1039 | hasDuplicateImports, 1040 | }, 1041 | }) 1042 | 1043 | extractedTypeReplacements[key] = { 1044 | offset: -typeAnnotation.start!, 1045 | replacements: [], 1046 | } 1047 | 1048 | if (typeAnnotation.type === 'TSUnionType') { 1049 | extractTypesFromTSUnionType(typeAnnotation, { referenceSource: key }) 1050 | } 1051 | // TODO(zorin): Support TSLiteral, IntersectionType 1052 | else if (typeAnnotation.type === 'TSTypeReference' && typeAnnotation.typeName.type === 'Identifier') { 1053 | const name = typeAnnotation.typeName.name 1054 | 1055 | preReferenceExtraction({ 1056 | name, 1057 | replaceLocation: { 1058 | start: typeAnnotation.typeName.start!, 1059 | end: typeAnnotation.typeName.end!, 1060 | }, 1061 | metaData: { 1062 | referenceSource: key, 1063 | }, 1064 | }) 1065 | 1066 | extractTypeByName(name) 1067 | } 1068 | } 1069 | 1070 | /** 1071 | * NOTE(zorin): Convert enum types to union types, since Vue can't handle them right now 1072 | * 1073 | * NOTE(wheat): Since I don't believe these can depend on any other 1074 | * types we just want to extract the string itself. 1075 | */ 1076 | const extractTypesFromEnum = (node: TSEnumDeclaration, fullName: string) => { 1077 | const enumName = node.id.name 1078 | const key = withPath(enumName) 1079 | const enumTypes: Set = new Set() 1080 | 1081 | const { hasDuplicateImports, referenceSource } = (getMetaData(enumName) || {}) as ReferenceTypeMetaData 1082 | 1083 | const referenceSourceFile = at(referenceSource.split(':'), -2) 1084 | 1085 | // Remove extra specifier if it is referenced from SFC 1086 | if (/\.vue$/.test(referenceSourceFile)) { 1087 | const name = convertFullNameToName(fullName) 1088 | 1089 | if (name) { 1090 | extraSpecifiers.forEach((specifier, idx) => { 1091 | if (specifier === name) 1092 | extraSpecifiers.splice(idx, 1) 1093 | }) 1094 | } 1095 | } 1096 | 1097 | // (semi-stable) Determine the type of enum, may not be able to process the use of complex scenes 1098 | for (const member of node.members) { 1099 | if (member.initializer) { 1100 | if (member.initializer.type === 'NumericLiteral') 1101 | enumTypes.add('number') 1102 | else if (member.initializer.type === 'StringLiteral') 1103 | enumTypes.add('string') 1104 | } 1105 | else { 1106 | enumTypes.add('number') 1107 | } 1108 | } 1109 | 1110 | const result = [...enumTypes].join(' | ') 1111 | 1112 | let body = '/* enum */ ' 1113 | 1114 | if (result) 1115 | body += result 1116 | else 1117 | body = '/* empty-enum */ number | string' 1118 | 1119 | extractedTypes.set(key, { 1120 | typeKeyword: 'type', 1121 | fullName, 1122 | body, 1123 | }) 1124 | 1125 | postExtraction({ 1126 | key, 1127 | fullName, 1128 | name: enumName, 1129 | metaData: { 1130 | hasDuplicateImports, 1131 | }, 1132 | isEnum: true, 1133 | }) 1134 | } 1135 | 1136 | /** 1137 | * TODO(zorin): Remove corresponding replacements and dependencies if we could not find the type 1138 | */ 1139 | async function findMissingTypesFromImport(modulePath: string, missingTypes: string[], aliases: Record = {}) { 1140 | debug('Find missing types %O from \'%s\'', missingTypes, modulePath) 1141 | // imported -> local[] 1142 | const importAliasRecord: Record = {} 1143 | 1144 | // Generate new extract aliases (originalName -> userAlias) to replace the name of types 1145 | const newExtractAliases = missingTypes.reduce>((res, maybeAlias) => { 1146 | const originalName = aliases[maybeAlias] 1147 | 1148 | if (!originalName) { 1149 | /** 1150 | * NOTE(zorin): Apply alias for `import { default } from 'foo'` 1151 | * In theory, this kind of syntax is only produced by the plugin (Users will get errors from TS if they write this kind of code) 1152 | * The plugin converts `export { default } from 'foo'` to `import { default } from 'foo';export { default }` 1153 | */ 1154 | if (maybeAlias === 'default') { 1155 | const alias = extractAliases.default 1156 | res.default = alias 1157 | setMetaData(alias, getMetaData('default')!) 1158 | } 1159 | 1160 | // Skip when it is exactly the imported name 1161 | return res 1162 | } 1163 | 1164 | const alias = res[originalName] 1165 | 1166 | // Add replacements to the existing's if we have already found an alias 1167 | if (isString(alias)) { 1168 | debug('Add replacements of %s to %s', maybeAlias, alias) 1169 | 1170 | const targetRecord = replacementRecord[alias] || getSharedMetaData(alias)!.replacementTargets! 1171 | 1172 | const sourceRecord = replacementRecord[maybeAlias] || getSharedMetaData(maybeAlias)!.replacementTargets! 1173 | 1174 | targetRecord.push(...sourceRecord) 1175 | } 1176 | else { 1177 | res[originalName] = maybeAlias 1178 | } 1179 | 1180 | importAliasRecord[originalName] ||= [] 1181 | importAliasRecord[originalName].push(maybeAlias) 1182 | 1183 | return res 1184 | }, {}) 1185 | 1186 | Object.entries(importAliasRecord).forEach(([typeName, record]) => { 1187 | // Add metadata if it has multiple aliases 1188 | if (record.length > 1) { 1189 | const alias = newExtractAliases[typeName] 1190 | 1191 | setMetaData(alias, { 1192 | hasDuplicateImports: true, 1193 | }) 1194 | } 1195 | }) 1196 | 1197 | debug('New extract aliases: %O', newExtractAliases) 1198 | 1199 | // Apply aliases and record the number of times each type appears (also we dedupe them in this step) 1200 | const typeCounter = missingTypes.reduce>((counter, _name) => { 1201 | const name = aliases[_name] || _name 1202 | 1203 | counter[name] ??= 0 1204 | 1205 | counter[name] += 1 1206 | 1207 | return counter 1208 | }, {}) 1209 | 1210 | // Unwrap aliases (userAlias -> originalName) to find types 1211 | const processedMissingTypes = Object.entries(typeCounter).map(([typeName, count]) => { 1212 | /** 1213 | * If the type has duplicate imports and its number does not match the number of its aliases, 1214 | * it means that the user also imported the original(exported) name of the type 1215 | * @example 1216 | * ```typescript 1217 | * import { Foo, Foo as Bar } from 'foo' 1218 | * ``` 1219 | */ 1220 | if (count > 1 && count !== importAliasRecord[typeName].length) { 1221 | const alias = newExtractAliases[typeName] 1222 | 1223 | debug('Push replacements of %s to %s (Original)', typeName, alias) 1224 | 1225 | const targetRecord = replacementRecord[alias] || getSharedMetaData(alias)!.replacementTargets! 1226 | 1227 | const sourceRecord = replacementRecord[typeName] || getSharedMetaData(typeName)!.replacementTargets! 1228 | 1229 | targetRecord.push(...sourceRecord) 1230 | 1231 | setMetaData(alias, { 1232 | hasDuplicateImports: true, 1233 | }) 1234 | } 1235 | 1236 | return typeName 1237 | }) 1238 | 1239 | debug('Processed missing types: %O', processedMissingTypes) 1240 | 1241 | // Generate aliases that apply the existing extract aliases for new extract aliases if exists 1242 | const processedNewExtractAliases = Object.fromEntries(Object.entries(newExtractAliases).map(([originalName, userAlias]) => [originalName, extractAliases[userAlias] || userAlias])) 1243 | 1244 | debug('Processed new extract aliases: %O', processedNewExtractAliases) 1245 | 1246 | // Generate new metadata map from the missing types 1247 | const newMetaDataMap = processedMissingTypes.reduce>((res, typeName) => { 1248 | // Apply new extract alias if exists 1249 | const name = newExtractAliases[typeName] || typeName 1250 | const metaData = getMetaData(name) 1251 | 1252 | if (metaData) { 1253 | (metaData as TypeMetaData).replacementTargets ||= replacementRecord[name] 1254 | 1255 | res[typeName] = metaData 1256 | } 1257 | 1258 | return res 1259 | }, {}) 1260 | 1261 | await extractTypesFromModule(modulePath, processedMissingTypes, processedNewExtractAliases, newMetaDataMap) 1262 | } 1263 | 1264 | async function findMissingTypes(missingTypes: string[], groupImportsResult: GroupedImportsResult): Promise { 1265 | const unresolved: string[] = [] 1266 | 1267 | const { groupedImports, localSpecifierMap } = groupImportsResult 1268 | 1269 | debug('Grouped imports: %O', groupedImports) 1270 | debug('Local specifier map: %O', localSpecifierMap) 1271 | 1272 | const resolvedImports = missingTypes.reduce>((res, typeName) => { 1273 | const modulePath = localSpecifierMap[typeName] 1274 | 1275 | if (isString(modulePath)) { 1276 | res[modulePath] ||= [] 1277 | res[modulePath].push(typeName) 1278 | } 1279 | else { 1280 | if (!hasExportAllDecl) 1281 | debug('Cannot find type: %s', typeName) 1282 | 1283 | unresolved.push(typeName) 1284 | } 1285 | 1286 | return res 1287 | }, {}) 1288 | 1289 | for (const [modulePath, types] of Object.entries(resolvedImports)) 1290 | await findMissingTypesFromImport(modulePath, [...new Set(types)], groupedImports[modulePath]) 1291 | 1292 | return unresolved 1293 | } 1294 | 1295 | for (const typeName of processedTypes) 1296 | extractTypeByName(typeName) 1297 | 1298 | debug('Local metadata map (after): %O', localMetaDataMap) 1299 | debug('Replacement record: %O', replacementRecord) 1300 | 1301 | const unresolvedTypes: string[] = [] 1302 | 1303 | if (missingTypes.local.length) { 1304 | debug('Find missing types (Local)') 1305 | 1306 | const unresolved = await findMissingTypes(missingTypes.local, groupedImportsResult) 1307 | 1308 | unresolvedTypes.push(...unresolved) 1309 | } 1310 | 1311 | if (missingTypes.requested.length) { 1312 | debug('Find missing types (Requested)') 1313 | 1314 | /** 1315 | * NOTE(zorin): For development convenience, we currently convert the export syntaxes to import syntaxes. 1316 | * This behavior may be changed in the future 1317 | */ 1318 | const groupedExportsResult = groupImports(convertExportsToImports([...namedExports, ...namedFromExports], groupedImportsResult), source, relativePath) 1319 | 1320 | const unresolved = await findMissingTypes(missingTypes.requested, groupedExportsResult) 1321 | 1322 | unresolvedTypes.push(...unresolved) 1323 | } 1324 | 1325 | if (!isInSFC && unresolvedTypes.length && hasExportAllDecl) { 1326 | debug('Find missing types (Export all)') 1327 | 1328 | for (const exportSource of exportAllSources) 1329 | await findMissingTypesFromImport(exportSource, unresolvedTypes) 1330 | } 1331 | 1332 | return { 1333 | result: extractedTypes, 1334 | namesMap: extractedNamesMap, 1335 | typeReplacements: extractedTypeReplacements, 1336 | extendsRecord: interfaceExtendsRecord, 1337 | importNodes, 1338 | extraSpecifiers, 1339 | sourceReplacements, 1340 | } 1341 | } 1342 | -------------------------------------------------------------------------------- /src/core/constants.ts: -------------------------------------------------------------------------------- 1 | // Vue macros 2 | export const DEFINE_PROPS = 'defineProps' 3 | export const DEFINE_EMITS = 'defineEmits' 4 | export const WITH_DEFAULTS = 'withDefaults' 5 | 6 | export const PLUGIN_NAME = 'vite-plugin-vue-type-imports' 7 | 8 | // Typescript types that the plugin allow to extract. 9 | export const TS_TYPES_KEYS = ['TSTypeAliasDeclaration', 'TSInterfaceDeclaration', 'TSEnumDeclaration'] 10 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | import { parse } from '@vue/compiler-sfc' 2 | import MagicString from 'magic-string' 3 | import type { TransformResult } from 'vite' 4 | import type { ExtractResult, TypeMetaData } from './ast' 5 | import { extractTypesFromSource, getUsedInterfacesFromAst } from './ast' 6 | import { debuggerFactory, getAst, notNullish, replaceAtIndexes, resolveDependencies, resolveExtends } from './utils' 7 | import type { MaybeAliases, Replacement } from './utils' 8 | 9 | export interface CleanOptions { 10 | interface?: boolean 11 | } 12 | 13 | export interface TransformOptions { 14 | id: string 15 | aliases?: MaybeAliases 16 | } 17 | 18 | export interface FinalizeResult { 19 | inlinedTypes: string 20 | replacements: Replacement[] 21 | } 22 | 23 | const createMainDebugger = debuggerFactory('Main') 24 | 25 | export function finalize(types: string[], extractResult: ExtractResult): FinalizeResult | null { 26 | const debug = createMainDebugger('Finalize') 27 | 28 | const { result, namesMap, typeReplacements, extendsRecord, importNodes, extraSpecifiers, sourceReplacements } = extractResult 29 | 30 | if (!result.size) 31 | return null 32 | 33 | Object.entries(typeReplacements).forEach(([k, r]) => { 34 | debug('Replacements %s => %O', k, r) 35 | }) 36 | 37 | debug('Keys map: %O', namesMap) 38 | 39 | // Apply replacements 40 | Object.entries(typeReplacements).forEach(([key, replacementInfo]) => { 41 | const extractedTypeInfo = result.get(key)! 42 | const { offset, replacements } = replacementInfo 43 | 44 | extractedTypeInfo.body = replaceAtIndexes(extractedTypeInfo.body, replacements, offset) 45 | }) 46 | 47 | const resolvedExtendsOrder = resolveExtends(extendsRecord) 48 | 49 | debug('Resolved extends order: %O', resolvedExtendsOrder) 50 | 51 | // Insert code for extended interfaces in order 52 | resolvedExtendsOrder.forEach((key) => { 53 | const extractedTypeInfo = result.get(key)! 54 | 55 | const extendTypes = extendsRecord[key] 56 | 57 | if (!extendTypes?.length) 58 | return 59 | 60 | const replacements: Replacement[] = extendTypes.map((key) => { 61 | const { body, dependencies } = result.get(key)! 62 | 63 | // Add dependencies for extended interfaces 64 | if (dependencies?.length) { 65 | extractedTypeInfo.dependencies ||= [] 66 | extractedTypeInfo.dependencies!.push(...dependencies) 67 | } 68 | 69 | return { 70 | start: 1, 71 | end: 1, 72 | replacement: body.slice(1, body.length - 1), 73 | } 74 | }) 75 | 76 | extractedTypeInfo.body = replaceAtIndexes(extractedTypeInfo.body, replacements) 77 | }) 78 | 79 | debug('Result: %O', result) 80 | debug('Extra specifiers 3: %O', extraSpecifiers) 81 | 82 | // Collect replacements to clean up import specifiers 83 | importNodes.forEach((i) => { 84 | let defaultSpecifier: string | undefined 85 | 86 | const savedSpecifiers = i.specifiers 87 | .map((specifier) => { 88 | if (specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier' && specifier.local.type === 'Identifier') { 89 | const imported = specifier.imported.name 90 | const local = specifier.local.name 91 | 92 | let fullName = local 93 | 94 | /** 95 | * NOTE(zorin): We only remove specifiers that used directly by user because the name of their dependencies are prefixed by the plugin 96 | */ 97 | const shouldSave = !types.includes(local) 98 | 99 | if (shouldSave && !extraSpecifiers.includes(local)) { 100 | if (imported !== local) 101 | fullName = `${imported} as ${local}` 102 | 103 | return fullName 104 | } 105 | 106 | return null 107 | } 108 | else if (specifier.type === 'ImportDefaultSpecifier') { 109 | const name = specifier.local.name 110 | 111 | if (!types.includes(name) && !extraSpecifiers.includes(name)) 112 | defaultSpecifier = name 113 | } 114 | 115 | return null 116 | }) 117 | .filter(notNullish) 118 | 119 | const replacement: string[] = [ 120 | 'import', 121 | ] 122 | 123 | if (defaultSpecifier) 124 | replacement.push(` ${defaultSpecifier}`) 125 | 126 | if (savedSpecifiers.length) 127 | replacement.push(`${defaultSpecifier ? ',' : ''} { ${savedSpecifiers.join(', ')} }`) 128 | 129 | // Remove the import statement if no specifiers are saved 130 | if (replacement.length === 1) { 131 | sourceReplacements.push({ 132 | start: i.start!, 133 | end: i.end!, 134 | replacement: '', 135 | }) 136 | } 137 | // Generate a new import statement to replace the original one 138 | else { 139 | replacement.push(` from '${i.source.value}'`) 140 | 141 | sourceReplacements.push({ 142 | start: i.start!, 143 | end: i.end!, 144 | replacement: replacement.join(''), 145 | }) 146 | } 147 | }) 148 | 149 | /** 150 | * NOTE(zorin): Get the order of inlining types 151 | * If the order is incorrect, Vue will not get the correct definition of types 152 | * 153 | * @example 154 | * Correct: 155 | * ```typescript 156 | * type Foo = string 157 | * type Bar = Foo 158 | * 159 | * interface Props {...} 160 | * ``` 161 | * Wrong: 162 | * ```typescript 163 | * type Bar = Foo 164 | * type Foo = string 165 | * 166 | * interface Props {...} 167 | * ``` 168 | */ 169 | const dependencies = resolveDependencies(result, namesMap, types) 170 | 171 | debug('Dependencies: %O', dependencies) 172 | 173 | const inlinedTypes = dependencies.map((key) => { 174 | const { typeKeyword, fullName, body } = result.get(key)! 175 | 176 | let maybeEqualSign = '' 177 | 178 | if (typeKeyword === 'type') 179 | maybeEqualSign = ' =' 180 | 181 | return `${typeKeyword} ${fullName}${maybeEqualSign} ${body}` 182 | }).join('\n') 183 | 184 | return { 185 | inlinedTypes, 186 | replacements: sourceReplacements, 187 | } 188 | } 189 | 190 | export async function transform(code: string, { id, aliases }: TransformOptions): Promise { 191 | const { 192 | descriptor: { scriptSetup }, 193 | } = parse(code) 194 | 195 | const isTS = scriptSetup && (scriptSetup.lang === 'ts' || scriptSetup.lang === 'tsx') 196 | 197 | if (!isTS || !scriptSetup.content) 198 | return 199 | 200 | const program = getAst(scriptSetup.content) 201 | 202 | const interfaces = getUsedInterfacesFromAst(program) 203 | 204 | const metaDataMap = Object.fromEntries(interfaces.map<[string, TypeMetaData]>(name => [name, { isUsedType: true }])) 205 | 206 | const extractResult = await extractTypesFromSource( 207 | scriptSetup.content, 208 | interfaces, 209 | { 210 | pathAliases: aliases, 211 | relativePath: id, 212 | metaDataMap, 213 | ast: program, 214 | isInSFC: true, 215 | }, 216 | ) 217 | 218 | const result = finalize(interfaces, extractResult) 219 | 220 | if (!result) 221 | return 222 | 223 | const { inlinedTypes, replacements } = result 224 | 225 | const s = new MagicString(code) 226 | 227 | // Add inlined types and replace import statements 228 | const newScriptSetupContent = [ 229 | '', 230 | inlinedTypes, 231 | replaceAtIndexes(scriptSetup.content, replacements), 232 | '', 233 | ].join('\n') 234 | 235 | s.overwrite(scriptSetup.loc.start.offset, scriptSetup.loc.end.offset, newScriptSetupContent) 236 | 237 | if (s.hasChanged()) { 238 | return { 239 | code: s.toString(), 240 | get map() { 241 | return s.generateMap({ 242 | source: id, 243 | includeContent: true, 244 | hires: true, 245 | }) 246 | }, 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/core/utils.ts: -------------------------------------------------------------------------------- 1 | import { basename, dirname, join, relative } from 'path' 2 | import colors from 'picocolors' 3 | import _debug from 'debug' 4 | import fg from 'fast-glob' 5 | import type { PackageInfo } from 'local-pkg' 6 | import { getPackageInfoSync, resolveModule } from 'local-pkg' 7 | import type { Alias, AliasOptions } from 'vite' 8 | import { babelParse, generateCodeFrame } from '@vue/compiler-sfc' 9 | import type { CallExpression, Node, Program, TSEnumDeclaration, TSInterfaceDeclaration, TSTypeAliasDeclaration } from '@babel/types' 10 | import type { ExtractedTypes, IExport, IImport } from './ast' 11 | import { DEFINE_EMITS, DEFINE_PROPS, PLUGIN_NAME, TS_TYPES_KEYS, WITH_DEFAULTS } from './constants' 12 | 13 | /** 14 | * Type name prefixed with path 15 | * 16 | * @example '/foo/bar/baz.ts:Props' 17 | */ 18 | export type NameWithPath = string 19 | 20 | /** 21 | * The actual name of type. It may be prefixed by the plugin. 22 | * 23 | * @example 24 | * ```text 25 | * 1. 'Foo' 26 | * 2. '_VTI_TYPE_Foo' 27 | * 3. '_VTI_TYPE_Foo_2' 28 | * ``` 29 | */ 30 | export type FullName = string 31 | 32 | export type TSTypes = TSTypeAliasDeclaration | TSInterfaceDeclaration | TSEnumDeclaration 33 | 34 | export type MaybeAliases = ((AliasOptions | undefined) & Alias[]) | undefined 35 | 36 | export type MaybeString = string | null | undefined 37 | 38 | export type MaybeNumber = number | null | undefined 39 | 40 | export type MaybeNode = Node | null | undefined 41 | 42 | type Pkg = PackageInfo['packageJson'] 43 | 44 | interface PackageJSON extends Pkg { 45 | types?: string 46 | typings?: string 47 | exports?: { 48 | [p: string]: { 49 | types?: string 50 | } 51 | } 52 | } 53 | 54 | /** 55 | * References: 56 | * https://github.com/tc39/proposal-relative-indexing-method#polyfill 57 | * https://github.com/antfu/utils/blob/main/src/array.ts 58 | */ 59 | export function at(arr: [], index: number): undefined 60 | export function at(arr: T[], index: number): T 61 | export function at(arr: T[] | [], index: number): T | undefined { 62 | const length = arr.length 63 | 64 | if (index < 0) 65 | index += length 66 | 67 | if (index < 0 || index > length || !length) 68 | return undefined 69 | 70 | return arr[index] 71 | } 72 | 73 | export function debuggerFactory(namespace: string) { 74 | return (name?: string) => { 75 | const _debugger = _debug(`${PLUGIN_NAME}:${namespace}${name ? `:${name}` : ''}`) 76 | 77 | /** 78 | * NOTE(zorin): Use `console.log` instead when testing. 79 | * Because the output of the default logger is incomplete (i.e. it will lost some debug messages) when testing. 80 | */ 81 | if (process.env.VITEST) { 82 | /* eslint-disable-next-line no-console */ 83 | _debugger.log = console.log.bind(console) 84 | } 85 | 86 | return _debugger 87 | } 88 | } 89 | 90 | const createUtilsDebugger = debuggerFactory('Utils') 91 | 92 | export function getAst(content: string): Program { 93 | return babelParse(content, { 94 | sourceType: 'module', 95 | plugins: ['typescript', 'topLevelAwait', 'jsx'], 96 | }).program 97 | } 98 | 99 | /** 100 | * Source: https://github.com/rollup/plugins/blob/master/packages/alias/src/index.ts 101 | */ 102 | export function matches(pattern: string | RegExp, importee: string) { 103 | if (pattern instanceof RegExp) 104 | return pattern.test(importee) 105 | 106 | if (importee.length < pattern.length) 107 | return false 108 | 109 | if (importee === pattern) 110 | return true 111 | 112 | const importeeStartsWithKey = importee.indexOf(pattern) === 0 113 | const importeeHasSlashAfterKey = importee.slice(pattern.length)[0] === '/' 114 | return importeeStartsWithKey && importeeHasSlashAfterKey 115 | } 116 | 117 | export function removeTSExtension(path: string): string { 118 | return path.replace(/\.ts$/, '') 119 | } 120 | 121 | export function resolvePath(path: string, from: string, aliases: MaybeAliases) { 122 | const debug = createUtilsDebugger('resolvePath') 123 | 124 | const matchedEntry = aliases?.find(entry => matches(entry.find, path)) 125 | 126 | // Path which is using aliases. e.g. '~/types' 127 | if (matchedEntry) 128 | return path.replace(matchedEntry.find, matchedEntry.replacement) 129 | 130 | /** 131 | * External package 132 | * If the path is just a single dot, append '/index' to prevent incorrect results 133 | */ 134 | const modulePath = resolveModule(path === '.' ? './index' : path) 135 | const dtsRE = /\.d\.ts$/ 136 | 137 | // Not a package. e.g. '../types' 138 | if (!modulePath) 139 | return join(dirname(from), path) 140 | 141 | // Result is a typescript declaration file. 142 | if (dtsRE.test(modulePath)) { 143 | return removeTSExtension(modulePath) 144 | } 145 | // Not a typescript file, find declaration file 146 | else { 147 | const pkg = (getPackageInfoSync(path)?.packageJson || {}) as PackageJSON 148 | 149 | const slashArr = path.split('/') 150 | 151 | let processedPath = '.' 152 | 153 | // Increase index for scoped packages 154 | const index = slashArr[0][0] === '@' ? 2 : 1 155 | 156 | // Get relative path if module path contains slashes 157 | if (slashArr.length > index) 158 | processedPath = `./${relative(slashArr.slice(0, index).join('/'), path)}` 159 | 160 | debug('Processed path: %s', processedPath) 161 | 162 | const result: string = removeTSExtension(pkg.exports?.[processedPath]?.types || pkg.types || pkg.typings || basename(modulePath)) 163 | 164 | return join(dirname(modulePath), result) 165 | } 166 | } 167 | 168 | export async function resolveModulePath(path: string, from: string, aliases?: MaybeAliases) { 169 | const debug = createUtilsDebugger('resolveModulePath') 170 | 171 | const maybePath = resolvePath(path, from, aliases)?.replace(/\\/g, '/') 172 | 173 | debug('Resolved path: %s', maybePath) 174 | 175 | if (!maybePath) 176 | return null 177 | 178 | // We follow the parsing order of typescript 179 | // https://www.typescriptlang.org/docs/handbook/module-resolution.html#how-typescript-resolves-modules 180 | // For modules: module > module/index 181 | // For extensions: .ts > .tsx > .d.ts 182 | const files = await fg([`${maybePath}.ts`, `${maybePath}.tsx`, `${maybePath}.d.ts`, `${maybePath}/index.ts`, `${maybePath}/index.tsx`, `${maybePath}/index.d.ts`], { 183 | onlyFiles: true, 184 | }) 185 | 186 | debug('Matched files: %O', files) 187 | 188 | if (files.length) 189 | return files[0] 190 | 191 | return null 192 | } 193 | 194 | export type GroupedImports = Record> 195 | 196 | export interface GroupedImportsResult { 197 | groupedImports: GroupedImports 198 | localSpecifierMap: Record 199 | } 200 | 201 | /** 202 | * Categorize imports 203 | * 204 | * @example 205 | * ``` 206 | * const code = `import { a as bb, b as cc, c as aa } from 'example'` 207 | * // ...(Operations of getting AST and imports) 208 | * const groupedImports = groupImports(imports); 209 | * 210 | * console.log(groupedImports) 211 | * // Result: 212 | * { 213 | * groupedImports: { 214 | * example: { 215 | * bb: 'a', 216 | * cc: 'b', 217 | * aa: 'c' 218 | * } 219 | * }, 220 | * localSpecifierMap: { 221 | * a: 'example', 222 | * b: 'example', 223 | * c: 'example' 224 | * } 225 | * } 226 | * ``` 227 | */ 228 | export function groupImports(imports: IImport[], source: string, fileName: string): GroupedImportsResult { 229 | const importedSpecifierMap: Record = {} 230 | const localSpecifierMap: Record = {} 231 | 232 | const groupedImports = imports.reduce((res, rawImport) => { 233 | const importedSpecifiers = importedSpecifierMap[rawImport.path] 234 | 235 | if (importedSpecifiers?.length && importedSpecifiers.includes(rawImport.imported)) { 236 | warn(`Duplicate imports of type "${rawImport.imported}" found.`, { 237 | fileName, 238 | codeFrame: generateCodeFrame(source, rawImport.start, rawImport.end), 239 | }) 240 | } 241 | 242 | res[rawImport.path] ||= {} 243 | 244 | const aliases = res[rawImport.path] 245 | 246 | localSpecifierMap[rawImport.local] = rawImport.path 247 | 248 | importedSpecifierMap[rawImport.path] ||= [] 249 | importedSpecifierMap[rawImport.path].push(rawImport.imported) 250 | 251 | if (rawImport.local !== rawImport.imported) 252 | aliases[rawImport.local] = rawImport.imported 253 | 254 | return res 255 | }, {}) 256 | 257 | return { 258 | groupedImports, 259 | localSpecifierMap, 260 | } 261 | } 262 | 263 | /** 264 | * Convert export syntaxes to import syntaxes 265 | * 266 | * @example 267 | * Source: 268 | * ```typescript 269 | * export { Foo } from 'foo' 270 | * ``` 271 | * Result: 272 | * ```typescript 273 | * import { Foo } from 'foo' 274 | * export { Foo } 275 | * ``` 276 | */ 277 | export function convertExportsToImports(exports: IExport[], groupImportsResult: GroupedImportsResult): IImport[] { 278 | const { groupedImports, localSpecifierMap } = groupImportsResult 279 | 280 | return exports.map(({ start, end, local, exported, path }) => { 281 | if (path || localSpecifierMap[local]) { 282 | const mappedPath = localSpecifierMap[local] 283 | let imported = local 284 | 285 | if (!path && isString(mappedPath)) 286 | imported = groupedImports[mappedPath][local] || local 287 | 288 | return { 289 | start, 290 | end, 291 | local: exported, 292 | imported, 293 | path: path || mappedPath, 294 | } 295 | } 296 | 297 | return null 298 | }).filter(notNullish) 299 | } 300 | 301 | // NOTE(zorin): Not used for now 302 | export function intersect(a: Array, b: Array): (A | B)[] { 303 | const setB = new Set(b) 304 | // @ts-expect-error unnecessary type checking (for now) 305 | return [...new Set(a)].filter(x => setB.has(x)) 306 | } 307 | 308 | export interface Replacement { 309 | start: number 310 | end: number 311 | replacement: string 312 | } 313 | 314 | /** 315 | * Replace all items at specified indexes from the bottom up. 316 | * 317 | * NOTE(zorin): We assume that each replacement selection does not overlap with each other 318 | */ 319 | export function replaceAtIndexes(source: string, replacements: Replacement[], offset = 0): string { 320 | replacements.sort((a, b) => b.start - a.start) 321 | let result = source 322 | 323 | for (const node of replacements) 324 | result = result.slice(0, node.start + offset) + node.replacement + result.slice(node.end + offset) 325 | 326 | return result.split(/\r?\n/).filter(Boolean).join('\n') 327 | } 328 | 329 | /** 330 | * Collect dependencies and flatten it by using BFS 331 | * 332 | * NOTE(zorin): Maybe this needs a better solution. 333 | * 334 | * @example 335 | * Source code: 336 | * ``` 337 | * type Foo = string; 338 | * type Bar = Foo; 339 | * 340 | * export interface Props { 341 | * foo: Foo; 342 | * bar: Bar; 343 | * } 344 | * ``` 345 | * Dependency graph: 346 | * ``` 347 | * Props ---Foo 348 | * |--Bar--Foo 349 | * ``` 350 | * Result: ['Foo', 'Bar', 'Props'] 351 | */ 352 | export function resolveDependencies(extracted: ExtractedTypes, namesMap: Record, dependencies: string[]): string[] { 353 | function _resolveDependencies() { 354 | // NOTE(zorin): I don't think users will use same type for defineProps and defineEmits, so currently we do not dedupe them 355 | const queue: string[] = dependencies 356 | const result: string[] = [] 357 | 358 | while (queue.length) { 359 | const shift = queue.shift()! 360 | 361 | const key = namesMap[shift] 362 | 363 | /** 364 | * Skip adding dependency 365 | * 366 | * NOTE(zorin): The only situation I know is invalid type (Types that are not even found after the recursion completes) 367 | */ 368 | if (!key) 369 | continue 370 | 371 | result.push(key) 372 | 373 | const dependencies = extracted.get(key)!.dependencies 374 | 375 | if (dependencies?.length) { 376 | // Dedupe dependencies 377 | new Set(dependencies).forEach(dep => queue.push(dep)) 378 | } 379 | } 380 | 381 | return result 382 | } 383 | 384 | // Dedupe from the end 385 | return [...new Set(_resolveDependencies().reverse())] 386 | } 387 | 388 | /** 389 | * Generate correct order of extension by using BFS. Similar to `resolveDependencies` 390 | * 391 | * NOTE(zorin): Maybe this needs a better solution. 392 | */ 393 | export function resolveExtends(record: Record) { 394 | function _resolveExtends() { 395 | const queue: string[] = Object.keys(record) 396 | const result: string[] = [] 397 | 398 | while (queue.length) { 399 | const shift = queue.shift()! 400 | 401 | result.push(shift) 402 | 403 | const extendTypes = record[shift] 404 | 405 | if (extendTypes?.length) 406 | extendTypes.forEach(key => queue.push(key)) 407 | } 408 | 409 | return result 410 | } 411 | 412 | // Dedupe from the end 413 | const result = [...new Set(_resolveExtends().reverse())] 414 | 415 | return result 416 | } 417 | 418 | export function isNumber(n: MaybeNumber): n is number { 419 | return typeof n === 'number' && n.toString() !== 'NaN' 420 | } 421 | 422 | export function isString(n: MaybeString): n is string { 423 | return typeof n === 'string' 424 | } 425 | 426 | export function isCallOf(node: MaybeNode, test: string | ((id: string) => boolean)): node is CallExpression { 427 | return !!( 428 | node 429 | && node.type === 'CallExpression' 430 | && node.callee.type === 'Identifier' 431 | && (typeof test === 'string' ? node.callee.name === test : test(node.callee.name)) 432 | ) 433 | } 434 | 435 | export function isTSTypes(node: MaybeNode): node is TSTypes { 436 | return !!(node && TS_TYPES_KEYS.includes(node.type)) 437 | } 438 | 439 | export function notNullish(val: T | null | undefined): val is NonNullable { 440 | return val != null 441 | } 442 | 443 | export interface LogOptions { 444 | fileName?: string 445 | codeFrame?: string 446 | } 447 | 448 | export function mergeLogMsg(options: LogOptions & { msg: string }) { 449 | const { msg, fileName, codeFrame } = options 450 | 451 | // NOTE(zorin): We only log basic message in test 452 | const result = [ 453 | msg, 454 | '', 455 | process.env.VITEST ? undefined : fileName, 456 | process.env.VITEST ? undefined : codeFrame, 457 | ].filter(notNullish) 458 | 459 | // Push newline if the last line is not an empty string 460 | if (at(result, -1)) 461 | result.push('') 462 | 463 | return result.join('\n') 464 | } 465 | 466 | export function warn(msg: string, { fileName, codeFrame }: LogOptions = {}) { 467 | const result = mergeLogMsg({ msg, fileName, codeFrame }) 468 | 469 | console.warn(colors.yellow(`[${PLUGIN_NAME}] WARN: ${result}`)) 470 | } 471 | 472 | export function error(msg: string, { fileName, codeFrame }: LogOptions = {}): never { 473 | const result = mergeLogMsg({ msg, fileName, codeFrame }) 474 | 475 | throw new Error(colors.red(`[${PLUGIN_NAME}] ERROR: ${result}`)) 476 | } 477 | 478 | export function isEnum(node: TSTypes | null | undefined): node is TSEnumDeclaration { 479 | return !!(node && node.type === 'TSEnumDeclaration') 480 | } 481 | 482 | export const isDefineProps = (node: Node): node is CallExpression => isCallOf(node, DEFINE_PROPS) 483 | export const isDefineEmits = (node: Node): node is CallExpression => isCallOf(node, DEFINE_EMITS) 484 | export const isWithDefaults = (node: Node): node is CallExpression => isCallOf(node, WITH_DEFAULTS) 485 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import VitePlugin from './vite' 2 | 3 | export default VitePlugin 4 | -------------------------------------------------------------------------------- /src/nuxt.ts: -------------------------------------------------------------------------------- 1 | import VueTypeImports from './vite' 2 | 3 | export default function (_inlineOptions: any, nuxt: any) { 4 | nuxt.hook('vite:extend', async (vite: any) => { 5 | vite.config.plugins = vite.config.plugins || [] 6 | vite.config.plugins.unshift(VueTypeImports()) 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /src/vite.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin, ResolvedConfig } from 'vite' 2 | import { transform } from './core' 3 | import { PLUGIN_NAME } from './core/constants' 4 | 5 | export default function VitePluginVueTypeImports(): Plugin { 6 | let resolvedConfig: ResolvedConfig | undefined 7 | 8 | return { 9 | name: PLUGIN_NAME, 10 | enforce: 'pre', 11 | async configResolved(config) { 12 | resolvedConfig = config 13 | }, 14 | async transform(code, id) { 15 | if (!/\.(vue)$/.test(id)) 16 | return 17 | 18 | const aliases = resolvedConfig?.resolve.alias 19 | 20 | const transformResult = await transform(code, { 21 | id, 22 | aliases, 23 | }) 24 | 25 | return transformResult 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/__snapshots__/common.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1 2 | 3 | exports[`Common > Duplicate imports > Export aliases > 1.ts (default) 1`] = ` 4 | " 12 | " 13 | `; 14 | 15 | exports[`Common > Duplicate imports > Export aliases > 2.ts (default) 1`] = ` 16 | " 24 | " 25 | `; 26 | 27 | exports[`Common > Duplicate imports > Export aliases > multi-level.ts (default) 1`] = ` 28 | " 36 | " 37 | `; 38 | 39 | exports[`Common > Duplicate imports > Import aliases > 1.ts (default) 1`] = ` 40 | " 48 | " 49 | `; 50 | 51 | exports[`Common > Duplicate imports > Import aliases > 2.ts (default) 1`] = ` 52 | " 60 | " 61 | `; 62 | 63 | exports[`Common > Duplicate imports > Import aliases > multi-level.ts (default) 1`] = ` 64 | " 72 | " 73 | `; 74 | 75 | exports[`Common > Duplicate imports > Import export default > 1.ts (default) 1`] = ` 76 | " 84 | " 85 | `; 86 | 87 | exports[`Common > Duplicate imports > Import export default > 2.ts (default) 1`] = ` 88 | " 96 | " 97 | `; 98 | 99 | exports[`Common > Duplicate imports > Import export default > multi-level.ts (default) 1`] = ` 100 | " 108 | " 109 | `; 110 | 111 | exports[`Common > Enum types > Default > empty.ts (default) 1`] = ` 112 | " 119 | " 120 | `; 121 | 122 | exports[`Common > Enum types > Default > mixed.ts (default) 1`] = ` 123 | " 130 | " 131 | `; 132 | 133 | exports[`Common > Enum types > Default > number.ts (default) 1`] = ` 134 | " 141 | " 142 | `; 143 | 144 | exports[`Common > Enum types > Default > string.ts (default) 1`] = ` 145 | " 152 | " 153 | `; 154 | 155 | exports[`Common > Export aliases > Default > index.ts (default) 1`] = ` 156 | " 163 | " 164 | `; 165 | 166 | exports[`Common > Export aliases > Multi level > 1.ts (default) 1`] = ` 167 | " 173 | " 174 | `; 175 | 176 | exports[`Common > Export aliases > Multi level > 2.ts (default) 1`] = ` 177 | " 183 | " 184 | `; 185 | 186 | exports[`Common > Export all > Default > index.ts (default) 1`] = ` 187 | " 194 | " 195 | `; 196 | 197 | exports[`Common > Import aliases > Default > index.ts (default) 1`] = ` 198 | " 205 | " 206 | `; 207 | 208 | exports[`Common > Import aliases > Multi level > index.ts (default) 1`] = ` 209 | " 217 | " 218 | `; 219 | 220 | exports[`Common > Import export default > Default > 1.ts (default) 1`] = ` 221 | " 228 | " 229 | `; 230 | 231 | exports[`Common > Import export default > Multi level > 1.ts (default) 1`] = ` 232 | " 238 | " 239 | `; 240 | 241 | exports[`Common > Import export default > Multi level > 2.ts (default) 1`] = ` 242 | " 249 | " 250 | `; 251 | 252 | exports[`Common > Import export default > Use aliases > 1.ts (default) 1`] = ` 253 | " 260 | " 261 | `; 262 | 263 | exports[`Common > Import export default > Use aliases > 2.ts (default) 1`] = ` 264 | " 271 | " 272 | `; 273 | 274 | exports[`Common > Import export default > Use aliases > 3.ts (default) 1`] = ` 275 | " 282 | " 283 | `; 284 | 285 | exports[`Common > Import same type implicitly > Default > 1.ts (default) 1`] = ` 286 | " 295 | " 296 | `; 297 | 298 | exports[`Common > Import same type implicitly > Default > 2.ts (default) 1`] = ` 299 | " 309 | " 310 | `; 311 | 312 | exports[`Common > Interface extends interface > Has reference > 1.ts (default) 1`] = ` 313 | " 324 | " 325 | `; 326 | 327 | exports[`Common > Interface extends interface > Has reference > 2.ts (default) 1`] = ` 328 | " 343 | " 344 | `; 345 | 346 | exports[`Common > Interface extends interface > Has reference > 3.ts (default) 1`] = ` 347 | " 365 | " 366 | `; 367 | 368 | exports[`Common > Interface extends interface > No reference > index.ts (default) 1`] = ` 369 | " 377 | " 378 | `; 379 | 380 | exports[`Common > Interface without reference > Default > index.ts (default) 1`] = ` 381 | " 388 | " 389 | `; 390 | 391 | exports[`Common > Mixed aliases > Default > index.ts (default) 1`] = ` 392 | " 399 | " 400 | `; 401 | 402 | exports[`Common > Multi level reference > Default > 1.ts (default) 1`] = ` 403 | " 412 | " 413 | `; 414 | 415 | exports[`Common > Multi level reference > Default > 2.ts (default) 1`] = ` 416 | " 429 | " 430 | `; 431 | 432 | exports[`Common > Redeclaration of types > Default > index.ts (default) 1`] = ` 433 | " 441 | " 442 | `; 443 | 444 | exports[`Common > Redeclaration of types > Same name > index.ts (default) 1`] = ` 445 | " 455 | " 456 | `; 457 | 458 | exports[`Common > Reference in property > Default > index.ts (default) 1`] = ` 459 | " 470 | " 471 | `; 472 | 473 | exports[`Common > Strict type finding > Default > index.ts (default) 1`] = ` 474 | " 480 | " 481 | `; 482 | -------------------------------------------------------------------------------- /test/__snapshots__/dynamic.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1 2 | 3 | exports[`Dynamic > Enum types > Default > index.vue (default) 1`] = ` 4 | " 12 | " 13 | `; 14 | 15 | exports[`Dynamic > Enum types > Default > local.vue (default) 1`] = ` 16 | " 31 | " 32 | `; 33 | 34 | exports[`Dynamic > Import priority > Preferred dts > index.vue (default) 1`] = ` 35 | " 44 | " 45 | `; 46 | 47 | exports[`Dynamic > Import priority > Preferred ts > index.vue (default) 1`] = ` 48 | " 57 | " 58 | `; 59 | 60 | exports[`Dynamic > Import priority > Preferred tsx > index.vue (default) 1`] = ` 61 | " 70 | " 71 | `; 72 | 73 | exports[`Dynamic > Interface extends interface > Has reference > 1.vue (default) 1`] = ` 74 | " 85 | " 86 | `; 87 | 88 | exports[`Dynamic > Interface extends interface > Has reference > 2.vue (default) 1`] = ` 89 | " 104 | " 105 | `; 106 | 107 | exports[`Dynamic > Interface extends interface > Has reference > 3.vue (default) 1`] = ` 108 | " 126 | " 127 | `; 128 | 129 | exports[`Dynamic > Interface extends interface > Has reference > external_1.vue (default) 1`] = ` 130 | " 141 | " 142 | `; 143 | 144 | exports[`Dynamic > Interface extends interface > Has reference > external_2.vue (default) 1`] = ` 145 | " 160 | " 161 | `; 162 | 163 | exports[`Dynamic > Interface extends interface > Has reference > external_3.vue (default) 1`] = ` 164 | " 182 | " 183 | `; 184 | 185 | exports[`Dynamic > Interface extends interface > No reference > index.vue (default) 1`] = ` 186 | " 194 | " 195 | `; 196 | 197 | exports[`Dynamic > Interface extends interface > No reference > internal.vue (default) 1`] = ` 198 | " 206 | " 207 | `; 208 | 209 | exports[`Dynamic > Interface without reference > No transform > index.vue (default) 1`] = ` 210 | " 217 | " 218 | `; 219 | 220 | exports[`Dynamic > Multi level reference > Default > 1.vue (default) 1`] = ` 221 | " 230 | " 231 | `; 232 | 233 | exports[`Dynamic > Multi level reference > Default > 2.vue (default) 1`] = ` 234 | " 247 | " 248 | `; 249 | 250 | exports[`Dynamic > Tsx > Import tsx > index.vue (default) 1`] = ` 251 | " 260 | " 261 | `; 262 | 263 | exports[`Dynamic > Tsx > Lang tsx > index.vue (default) 1`] = ` 264 | " 272 | " 273 | `; 274 | -------------------------------------------------------------------------------- /test/_presets.ts: -------------------------------------------------------------------------------- 1 | import type { TransformOptions } from '../src/core' 2 | 3 | export const presetNames = ['default'] as const 4 | 5 | export type PresetNames = typeof presetNames[number] 6 | 7 | export type Presets = Record 8 | 9 | export function generatePresets(id: string): Presets { 10 | return { 11 | default: { id }, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/_utils.ts: -------------------------------------------------------------------------------- 1 | import { basename, dirname, resolve } from 'path' 2 | import fg from 'fast-glob' 3 | import type { Awaitable } from 'vitest' 4 | import type { TransformOptions } from '../src/core' 5 | import { transform } from '../src/core' 6 | import { generatePresets } from './_presets' 7 | 8 | /** 9 | * Replace hyphen to space, and uppercase the first character 10 | */ 11 | export function normalizeName(val: string): string { 12 | const result = val.replace(/-/g, ' ') 13 | return result[0].toUpperCase() + result.slice(1) 14 | } 15 | 16 | export interface TestMetaData { 17 | entry: string 18 | entryName: string 19 | presetName: string 20 | options: TransformOptions 21 | } 22 | 23 | export type DirectoryStructure = Record> 24 | 25 | export function generateDirectoryStructure(files: string[], re?: RegExp): DirectoryStructure { 26 | const structureRE = re || /.+\/common\/(.+)\/(.+)\//g 27 | const result: DirectoryStructure = {} 28 | 29 | files.forEach((file) => { 30 | let scenario = '' 31 | let detailedScenario = '' 32 | 33 | const structureMatches = file.matchAll(structureRE) 34 | 35 | for (const m of structureMatches) { 36 | scenario = normalizeName(m[1]) 37 | detailedScenario = normalizeName(m[2]) 38 | } 39 | 40 | if (!(scenario || detailedScenario)) 41 | throw new Error('Error while parsing directory structure.') 42 | 43 | result[scenario] ||= {} 44 | result[scenario][detailedScenario] ||= [] 45 | result[scenario][detailedScenario].push(file) 46 | }) 47 | 48 | return result 49 | } 50 | 51 | export type CodeGetter = (metaData: TestMetaData) => Awaitable 52 | 53 | export interface DefineTransformTestOptions { 54 | category: string 55 | codeGetter: CodeGetter 56 | filePattern: string | string[] 57 | fileName: string 58 | structureRE?: RegExp 59 | realPath?: boolean 60 | skip?: boolean 61 | } 62 | 63 | export function defineTransformTest(options: DefineTransformTestOptions) { 64 | const { category, codeGetter, filePattern, fileName, structureRE, realPath, skip } = options 65 | 66 | if (skip) { 67 | describe.skip(category) 68 | return 69 | } 70 | 71 | describe(category, async () => { 72 | const dir = dirname(fileName) 73 | // NOTE(zorin): Relative paths 74 | const files = await fg(filePattern, { cwd: dir, onlyFiles: true, deep: 3 }) 75 | 76 | const directoryStructure = generateDirectoryStructure(files, structureRE) 77 | 78 | // Scenario 79 | describe.each(Object.keys(directoryStructure))('%s', (scenario) => { 80 | // Detailed scenario 81 | describe.each(Object.keys(directoryStructure[scenario]))('%s', (detailedScenario) => { 82 | const entries = directoryStructure[scenario][detailedScenario] 83 | 84 | const tests: TestMetaData[] = [] 85 | 86 | entries.forEach((entry) => { 87 | Object.entries(generatePresets(realPath ? resolve(dir, entry) : fileName)).forEach(([presetName, options]) => { 88 | tests.push({ 89 | entry, 90 | entryName: basename(entry), 91 | presetName, 92 | options, 93 | }) 94 | }) 95 | }) 96 | 97 | // Entry files 98 | test.each(tests)('$entryName ($presetName)', async (metaData) => { 99 | const code = await codeGetter(metaData) 100 | const result = await transform(code, metaData.options) 101 | 102 | expect(result!.code).toMatchSnapshot() 103 | }) 104 | }) 105 | }) 106 | }) 107 | } 108 | -------------------------------------------------------------------------------- /test/common.test.ts: -------------------------------------------------------------------------------- 1 | import { removeTSExtension } from '../src/core/utils' 2 | import type { CodeGetter } from './_utils' 3 | import { defineTransformTest } from './_utils' 4 | 5 | const codeGetter: CodeGetter = ({ entry }) => ` 10 | ` 11 | 12 | defineTransformTest({ 13 | category: 'Common', 14 | filePattern: ['./fixtures/common/**/!(_)*.ts'], 15 | fileName: __filename, 16 | codeGetter, 17 | skip: false, 18 | }) 19 | -------------------------------------------------------------------------------- /test/dynamic.test.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'node:fs/promises' 2 | import { resolve } from 'node:path' 3 | import type { CodeGetter } from './_utils' 4 | import { defineTransformTest } from './_utils' 5 | 6 | const codeGetter: CodeGetter = async ({ entry }) => readFile(resolve(__dirname, entry), 'utf-8') 7 | 8 | const structureRE = /.+\/dynamic\/(.+)\/(.+)\//g 9 | 10 | defineTransformTest({ 11 | category: 'Dynamic', 12 | filePattern: ['./fixtures/dynamic/**/*.vue'], 13 | fileName: __filename, 14 | codeGetter, 15 | structureRE, 16 | realPath: true, 17 | skip: false, 18 | }) 19 | -------------------------------------------------------------------------------- /test/fixtures/common/duplicate-imports/export-aliases/1.ts: -------------------------------------------------------------------------------- 1 | import type { Bar, Foo } from './types/1' 2 | 3 | export interface Props { 4 | foo: Foo 5 | bar: Bar 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/common/duplicate-imports/export-aliases/2.ts: -------------------------------------------------------------------------------- 1 | import type { Bar, Foo } from './types/2' 2 | 3 | export interface Props { 4 | foo: Foo 5 | bar: Bar 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/common/duplicate-imports/export-aliases/multi-level.ts: -------------------------------------------------------------------------------- 1 | import type { Bar, Foo } from './types/3' 2 | 3 | export interface Props { 4 | foo: Foo 5 | bar: Bar 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/common/duplicate-imports/export-aliases/types/1.ts: -------------------------------------------------------------------------------- 1 | type Foo = 'foo' 2 | 3 | export { Foo, Foo as Bar } 4 | -------------------------------------------------------------------------------- /test/fixtures/common/duplicate-imports/export-aliases/types/2.ts: -------------------------------------------------------------------------------- 1 | type F = 'foo' 2 | 3 | export { F as Foo, F as Bar } 4 | -------------------------------------------------------------------------------- /test/fixtures/common/duplicate-imports/export-aliases/types/3.ts: -------------------------------------------------------------------------------- 1 | export { Foo, Bar } from './4' 2 | -------------------------------------------------------------------------------- /test/fixtures/common/duplicate-imports/export-aliases/types/4.ts: -------------------------------------------------------------------------------- 1 | type F = 'foo' 2 | 3 | export { F as Foo, F as Bar } 4 | -------------------------------------------------------------------------------- /test/fixtures/common/duplicate-imports/import-aliases/1.ts: -------------------------------------------------------------------------------- 1 | import type { Foo as Bar, Foo } from './types/1' 2 | 3 | export interface Props { 4 | foo: Foo 5 | bar: Bar 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/common/duplicate-imports/import-aliases/2.ts: -------------------------------------------------------------------------------- 1 | import type { Foo as Bar, Foo as Baz } from './types/1' 2 | 3 | export interface Props { 4 | foo: Bar 5 | bar: Baz 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/common/duplicate-imports/import-aliases/multi-level.ts: -------------------------------------------------------------------------------- 1 | import type { Foo as Bar, Foo as Baz } from './types/1' 2 | 3 | export interface Props { 4 | foo: Bar 5 | bar: Baz 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/common/duplicate-imports/import-aliases/types/1.ts: -------------------------------------------------------------------------------- 1 | export type Foo = 'foo' 2 | -------------------------------------------------------------------------------- /test/fixtures/common/duplicate-imports/import-aliases/types/2.ts: -------------------------------------------------------------------------------- 1 | export { Foo } from './3' 2 | -------------------------------------------------------------------------------- /test/fixtures/common/duplicate-imports/import-aliases/types/3.ts: -------------------------------------------------------------------------------- 1 | export type Foo = 'foo' 2 | -------------------------------------------------------------------------------- /test/fixtures/common/duplicate-imports/import-export-default/1.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-named-default */ 2 | import type { default as Bar, default as Foo } from './types/1' 3 | 4 | export interface Props { 5 | foo: Foo 6 | bar: Bar 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/common/duplicate-imports/import-export-default/2.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-named-default */ 2 | import type { Bar, default as Foo } from './types/2' 3 | 4 | export interface Props { 5 | foo: Foo 6 | bar: Bar 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/common/duplicate-imports/import-export-default/multi-level.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-named-default */ 2 | import type { default as Bar, default as Foo } from './types/2' 3 | 4 | export interface Props { 5 | foo: Foo 6 | bar: Bar 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/common/duplicate-imports/import-export-default/types/1.ts: -------------------------------------------------------------------------------- 1 | type Foo = 'foo' 2 | 3 | export default Foo 4 | -------------------------------------------------------------------------------- /test/fixtures/common/duplicate-imports/import-export-default/types/2.ts: -------------------------------------------------------------------------------- 1 | type Foo = 'foo' 2 | 3 | export { Foo as default, Foo as Bar } 4 | -------------------------------------------------------------------------------- /test/fixtures/common/duplicate-imports/import-export-default/types/3.ts: -------------------------------------------------------------------------------- 1 | export { default } from './4' 2 | -------------------------------------------------------------------------------- /test/fixtures/common/duplicate-imports/import-export-default/types/4.ts: -------------------------------------------------------------------------------- 1 | type Foo = 'foo' 2 | 3 | export { Foo as default, Foo as Bar } 4 | -------------------------------------------------------------------------------- /test/fixtures/common/enum-types/default/empty.ts: -------------------------------------------------------------------------------- 1 | export enum Foo {} 2 | 3 | export interface Props { 4 | foo: Foo 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/common/enum-types/default/mixed.ts: -------------------------------------------------------------------------------- 1 | export enum Foo { 2 | Bar, 3 | Baz = 'baz', 4 | Qux = 'qux', 5 | } 6 | 7 | export interface Props { 8 | foo: Foo 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/common/enum-types/default/number.ts: -------------------------------------------------------------------------------- 1 | export enum Foo { 2 | Bar, 3 | Baz, 4 | Qux, 5 | } 6 | 7 | export interface Props { 8 | foo: Foo 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/common/enum-types/default/string.ts: -------------------------------------------------------------------------------- 1 | export enum Foo { 2 | Bar = 'bar', 3 | Baz = 'baz', 4 | Qux = 'qux', 5 | } 6 | 7 | export interface Props { 8 | foo: Foo 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/common/export-aliases/default/_types.ts: -------------------------------------------------------------------------------- 1 | type F = string 2 | 3 | export { F as Foo } 4 | -------------------------------------------------------------------------------- /test/fixtures/common/export-aliases/default/index.ts: -------------------------------------------------------------------------------- 1 | import type { Foo } from './_types' 2 | 3 | export interface Props { 4 | foo: Foo 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/common/export-aliases/multi-level/1.ts: -------------------------------------------------------------------------------- 1 | import { Props } from './types/1' 2 | 3 | export { Props } 4 | -------------------------------------------------------------------------------- /test/fixtures/common/export-aliases/multi-level/2.ts: -------------------------------------------------------------------------------- 1 | export { Props } from './types/1' 2 | -------------------------------------------------------------------------------- /test/fixtures/common/export-aliases/multi-level/types/1.ts: -------------------------------------------------------------------------------- 1 | interface P { 2 | foo: string 3 | } 4 | 5 | export { P as Props } 6 | -------------------------------------------------------------------------------- /test/fixtures/common/export-all/default/index.ts: -------------------------------------------------------------------------------- 1 | import type { Foo } from './types/1' 2 | 3 | export interface Props { 4 | foo: Foo 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/common/export-all/default/types/1.ts: -------------------------------------------------------------------------------- 1 | export * from './2' 2 | -------------------------------------------------------------------------------- /test/fixtures/common/export-all/default/types/2.ts: -------------------------------------------------------------------------------- 1 | export type Foo = 'foo' 2 | -------------------------------------------------------------------------------- /test/fixtures/common/import-aliases/default/_types.ts: -------------------------------------------------------------------------------- 1 | export type F = number 2 | -------------------------------------------------------------------------------- /test/fixtures/common/import-aliases/default/index.ts: -------------------------------------------------------------------------------- 1 | import type { F as Foo } from './_types' 2 | 3 | export interface Props { 4 | foo: Foo 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/common/import-aliases/multi-level/_type_A.ts: -------------------------------------------------------------------------------- 1 | import type { B as Bar } from './_type_B' 2 | 3 | export type F = number | Bar 4 | -------------------------------------------------------------------------------- /test/fixtures/common/import-aliases/multi-level/_type_B.ts: -------------------------------------------------------------------------------- 1 | export type B = string 2 | -------------------------------------------------------------------------------- /test/fixtures/common/import-aliases/multi-level/index.ts: -------------------------------------------------------------------------------- 1 | import type { F as Foo } from './_type_A' 2 | 3 | export interface Props { 4 | foo: Foo 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/common/import-export-default/default/1.ts: -------------------------------------------------------------------------------- 1 | import type Foo from './_types' 2 | 3 | export interface Props { 4 | foo: Foo 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/common/import-export-default/default/_types.ts: -------------------------------------------------------------------------------- 1 | type Foo = string 2 | 3 | export default Foo 4 | -------------------------------------------------------------------------------- /test/fixtures/common/import-export-default/multi-level/1.ts: -------------------------------------------------------------------------------- 1 | import Props from './types/1' 2 | 3 | export { Props } 4 | -------------------------------------------------------------------------------- /test/fixtures/common/import-export-default/multi-level/2.ts: -------------------------------------------------------------------------------- 1 | import type Foo from './types/2' 2 | 3 | export interface Props { 4 | foo: Foo 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/common/import-export-default/multi-level/types/1.ts: -------------------------------------------------------------------------------- 1 | export interface Props { 2 | foo: number 3 | } 4 | 5 | export default Props 6 | -------------------------------------------------------------------------------- /test/fixtures/common/import-export-default/multi-level/types/2.ts: -------------------------------------------------------------------------------- 1 | export { default } from './3' 2 | -------------------------------------------------------------------------------- /test/fixtures/common/import-export-default/multi-level/types/3.ts: -------------------------------------------------------------------------------- 1 | type Foo = string 2 | 3 | export default Foo 4 | -------------------------------------------------------------------------------- /test/fixtures/common/import-export-default/use-aliases/1.ts: -------------------------------------------------------------------------------- 1 | import type Foo from './types/alias' 2 | 3 | export interface Props { 4 | foo: Foo 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/common/import-export-default/use-aliases/2.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-named-default */ 2 | import type { default as Foo } from './types/default' 3 | 4 | export interface Props { 5 | foo: Foo 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/common/import-export-default/use-aliases/3.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-named-default */ 2 | import type { default as Foo } from './types/alias' 3 | 4 | export interface Props { 5 | foo: Foo 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/common/import-export-default/use-aliases/types/alias.ts: -------------------------------------------------------------------------------- 1 | type Foo = string 2 | 3 | export { Foo as default } 4 | -------------------------------------------------------------------------------- /test/fixtures/common/import-export-default/use-aliases/types/default.ts: -------------------------------------------------------------------------------- 1 | type Foo = string 2 | 3 | export default Foo 4 | -------------------------------------------------------------------------------- /test/fixtures/common/import-same-type-implicitly/default/1.ts: -------------------------------------------------------------------------------- 1 | import type { Foo } from './types/1' 2 | import type { Bar } from './types/2' 3 | 4 | export interface Props { 5 | foo: Foo 6 | bar: Bar 7 | baz: Bar 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/common/import-same-type-implicitly/default/2.ts: -------------------------------------------------------------------------------- 1 | import type { Foo } from './types/1' 2 | import type { Bar } from './types/2' 3 | 4 | type A = Bar 5 | 6 | export interface Props { 7 | foo: Foo 8 | bar: Bar 9 | baz: A 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/common/import-same-type-implicitly/default/types/1.ts: -------------------------------------------------------------------------------- 1 | export type Foo = 'foo' 2 | -------------------------------------------------------------------------------- /test/fixtures/common/import-same-type-implicitly/default/types/2.ts: -------------------------------------------------------------------------------- 1 | export { Foo as Bar } from './1' 2 | -------------------------------------------------------------------------------- /test/fixtures/common/interface-extends-interface/has-reference/1.ts: -------------------------------------------------------------------------------- 1 | type Baz = boolean 2 | 3 | type Bar = number 4 | 5 | type Foo = string 6 | 7 | export interface BaseProps { 8 | baz: Baz 9 | } 10 | 11 | export interface Props extends BaseProps { 12 | foo: Foo 13 | bar: Bar 14 | } 15 | -------------------------------------------------------------------------------- /test/fixtures/common/interface-extends-interface/has-reference/2.ts: -------------------------------------------------------------------------------- 1 | type Baz = boolean 2 | 3 | type Bar = number 4 | 5 | type Foo = string 6 | 7 | export interface BaseProps { 8 | baz: Baz 9 | } 10 | 11 | export interface Props extends BaseProps { 12 | foo: Foo 13 | bar: Bar 14 | base: BaseProps 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/common/interface-extends-interface/has-reference/3.ts: -------------------------------------------------------------------------------- 1 | type Baz = boolean 2 | 3 | type Bar = number 4 | 5 | type Foo = string 6 | 7 | type Qux = 'qux' 8 | 9 | interface Base { 10 | qux: Qux 11 | } 12 | 13 | export interface BaseProps extends Base { 14 | baz: Baz 15 | } 16 | 17 | export interface Props extends BaseProps { 18 | foo: Foo 19 | bar: Bar 20 | base: BaseProps 21 | } 22 | -------------------------------------------------------------------------------- /test/fixtures/common/interface-extends-interface/no-reference/index.ts: -------------------------------------------------------------------------------- 1 | // Interface extends interface, no reference 2 | export interface BaseProps { 3 | baz: boolean 4 | } 5 | 6 | export interface Props extends BaseProps { 7 | foo: string 8 | bar: number 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/common/interface-without-reference/default/index.ts: -------------------------------------------------------------------------------- 1 | // Interface which has no references 2 | export interface Props { 3 | foo: string 4 | bar: number 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/common/mixed-aliases/default/index.ts: -------------------------------------------------------------------------------- 1 | import type { Foo as F } from './types/1' 2 | 3 | export interface Props { 4 | foo: F 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/common/mixed-aliases/default/types/1.ts: -------------------------------------------------------------------------------- 1 | import { A as F } from './2' 2 | 3 | export { F as Foo } 4 | -------------------------------------------------------------------------------- /test/fixtures/common/mixed-aliases/default/types/2.ts: -------------------------------------------------------------------------------- 1 | type B = 'A' 2 | 3 | export { B as A } 4 | -------------------------------------------------------------------------------- /test/fixtures/common/multi-level-reference/default/1.ts: -------------------------------------------------------------------------------- 1 | type Foo = number 2 | 3 | type Bar = Foo 4 | 5 | export interface Props { 6 | foo: Foo 7 | bar: Bar 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/common/multi-level-reference/default/2.ts: -------------------------------------------------------------------------------- 1 | type Foo = number 2 | 3 | type Bar = Foo 4 | 5 | type Baz = Bar 6 | 7 | type Qux = Foo 8 | 9 | export interface Props { 10 | foo: Foo 11 | bar: Bar 12 | baz: Baz 13 | qux: Qux 14 | } 15 | -------------------------------------------------------------------------------- /test/fixtures/common/redeclaration-of-types/default/index.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/wheatjs/vite-plugin-vue-type-imports/issues/6 2 | export type Foo = [number, number] 3 | 4 | export interface Props { 5 | foo: Foo 6 | bar: Foo 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/common/redeclaration-of-types/same-name/_type.ts: -------------------------------------------------------------------------------- 1 | type Foo = 'foo_2' 2 | 3 | export type Bar = Foo 4 | -------------------------------------------------------------------------------- /test/fixtures/common/redeclaration-of-types/same-name/index.ts: -------------------------------------------------------------------------------- 1 | import type { Bar } from './_type' 2 | 3 | type Foo = 'foo' 4 | 5 | export interface Props { 6 | foo: Foo 7 | bar: Bar 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/common/reference-in-property/default/index.ts: -------------------------------------------------------------------------------- 1 | interface Foo { 2 | foo: 'foo' 3 | } 4 | 5 | type Bar = Foo 6 | 7 | export interface Props { 8 | foo: Foo 9 | bar: Bar 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/common/strict-type-finding/default/_types.ts: -------------------------------------------------------------------------------- 1 | export interface Props { 2 | bar: string 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/common/strict-type-finding/default/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | interface Props { 3 | foo: number 4 | } 5 | 6 | export { Props } from './_types' 7 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/enum-types/default/_types.ts: -------------------------------------------------------------------------------- 1 | export enum Foo { 2 | Bar, 3 | Baz, 4 | Qux, 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/enum-types/default/index.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/enum-types/default/local.vue: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/import-priority/preferred-dts/_types.d.ts: -------------------------------------------------------------------------------- 1 | export interface Foo { 2 | fromDTS: boolean 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/import-priority/preferred-dts/index.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/import-priority/preferred-ts/_types.d.ts: -------------------------------------------------------------------------------- 1 | export interface Foo { 2 | fromDTS: boolean 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/import-priority/preferred-ts/_types.ts: -------------------------------------------------------------------------------- 1 | export interface Foo { 2 | fromTS: boolean 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/import-priority/preferred-ts/_types.tsx: -------------------------------------------------------------------------------- 1 | export interface Foo { 2 | fromTSX: boolean 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/import-priority/preferred-ts/index.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/import-priority/preferred-tsx/_types.d.ts: -------------------------------------------------------------------------------- 1 | export interface Foo { 2 | fromDTS: boolean 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/import-priority/preferred-tsx/_types.tsx: -------------------------------------------------------------------------------- 1 | export interface Foo { 2 | fromTSX: boolean 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/import-priority/preferred-tsx/index.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/interface-extends-interface/has-reference/1.vue: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/interface-extends-interface/has-reference/2.vue: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/interface-extends-interface/has-reference/3.vue: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/interface-extends-interface/has-reference/external_1.vue: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/interface-extends-interface/has-reference/external_2.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/interface-extends-interface/has-reference/external_3.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/interface-extends-interface/has-reference/externals/1.ts: -------------------------------------------------------------------------------- 1 | type Baz = boolean 2 | 3 | export interface BaseProps { 4 | baz: Baz 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/interface-extends-interface/has-reference/externals/2.ts: -------------------------------------------------------------------------------- 1 | type Baz = boolean 2 | 3 | export interface BaseProps { 4 | baz: Baz 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/interface-extends-interface/has-reference/externals/3.ts: -------------------------------------------------------------------------------- 1 | type Baz = boolean 2 | 3 | type Qux = 'qux' 4 | 5 | interface Base { 6 | qux: Qux 7 | } 8 | 9 | export interface BaseProps extends Base { 10 | baz: Baz 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/interface-extends-interface/no-reference/index.ts: -------------------------------------------------------------------------------- 1 | export interface BaseProps { 2 | baz: boolean 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/interface-extends-interface/no-reference/index.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/interface-extends-interface/no-reference/internal.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/interface-without-reference/no-transform/index.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/multi-level-reference/default/1.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/multi-level-reference/default/2.vue: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/tsx/import-tsx/_types.tsx: -------------------------------------------------------------------------------- 1 | export interface Foo { 2 | fromTSX: boolean 3 | } 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 6 | const render =
7 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/tsx/import-tsx/index.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /test/fixtures/dynamic/tsx/lang-tsx/index.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "esnext", 5 | "lib": ["esnext"], 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "resolveJsonModule": true, 11 | "jsx": "preserve", 12 | "skipLibCheck": true, 13 | "skipDefaultLibCheck": true, 14 | "types": ["vitest/globals", "vitest/importMeta", "@vue/runtime-dom"], 15 | "forceConsistentCasingInFileNames": true 16 | }, 17 | "exclude": [ 18 | "**/dist/**", 19 | "**/node_modules/**", 20 | "**/coverage/**" 21 | ] 22 | } -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "**/playground/**" 5 | ] 6 | } -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | const isProduction = process.env.NODE_ENV === 'production' 4 | 5 | export default defineConfig({ 6 | define: { 7 | 'process.env.VITEST': 'undefined', 8 | }, 9 | minify: true, 10 | format: ['esm', 'cjs'], 11 | entry: ['./src/index.ts', './src/nuxt.ts'], 12 | clean: true, 13 | dts: isProduction, 14 | esbuildOptions(options) { 15 | if (isProduction) 16 | options.pure = ['console.log'] 17 | }, 18 | }) 19 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | esbuild: { 5 | target: 'node14', 6 | }, 7 | test: { 8 | coverage: { 9 | reporter: ['text', 'json', 'html'], 10 | }, 11 | globals: true, 12 | }, 13 | }) 14 | --------------------------------------------------------------------------------