├── .eslintrc ├── packages └── core │ ├── test │ ├── tmp │ │ └── .gitignore │ ├── __snapshots__ │ │ ├── search.test.ts.snap │ │ ├── stringifyComponentImport.test.ts.snap │ │ ├── dts.test.ts.snap │ │ └── transform.test.ts.snap │ ├── utils.test.ts │ ├── stringifyComponentImport.test.ts │ ├── search.test.ts │ ├── transform.test.ts │ └── dts.test.ts │ ├── src │ ├── resolvers.ts │ ├── _resolvers │ │ ├── index.ts │ │ ├── ano-ui.ts │ │ ├── uv-ui.ts │ │ ├── wot-design-uni.ts │ │ ├── uview-pro.ts │ │ └── uni-ui.ts │ ├── constants.ts │ ├── fs │ │ └── glob.ts │ ├── transforms │ │ ├── directive │ │ │ ├── vue3.ts │ │ │ ├── index.ts │ │ │ └── vue2.ts │ │ └── component.ts │ ├── transformer.ts │ ├── index.ts │ ├── options.ts │ ├── types.ts │ ├── declaration.ts │ ├── utils.ts │ └── context.ts │ ├── tsconfig.json │ ├── index.d.ts │ ├── build.config.ts │ ├── README.md │ └── package.json ├── .npmrc ├── pnpm-workspace.yaml ├── playground ├── src │ ├── static │ │ └── logo.png │ ├── components │ │ ├── ComponentAsync.vue │ │ ├── ComponentD.vue │ │ ├── book │ │ │ └── index.vue │ │ ├── ComponentA.vue │ │ ├── component-c.vue │ │ ├── ComponentB.vue │ │ └── Recursive.vue │ ├── shime-uni.d.ts │ ├── main.ts │ ├── App.vue │ ├── env.d.ts │ ├── pages.json │ ├── pages │ │ └── index │ │ │ └── index.vue │ ├── uni.scss │ └── manifest.json ├── .gitignore ├── tsconfig.json ├── index.html ├── vite.config.ts ├── components.d.ts └── package.json ├── .gitignore ├── tsconfig.json ├── .github └── workflows │ └── release.yml ├── package.json ├── LICENSE ├── README.md └── banner.svg /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@antfu" 3 | } 4 | -------------------------------------------------------------------------------- /packages/core/test/tmp/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | auto-install-peers=true -------------------------------------------------------------------------------- /packages/core/src/resolvers.ts: -------------------------------------------------------------------------------- 1 | export * from './_resolvers/index' 2 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig" 3 | } 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | - playground 4 | -------------------------------------------------------------------------------- /packages/core/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist/index' 2 | export { default } from './dist/index' -------------------------------------------------------------------------------- /playground/src/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uni-helper/vite-plugin-uni-components/HEAD/playground/src/static/logo.png -------------------------------------------------------------------------------- /playground/src/components/ComponentAsync.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .DS_Store 3 | .idea 4 | *.log 5 | *.tgz 6 | coverage 7 | dist 8 | lib-cov 9 | logs 10 | node_modules 11 | temp 12 | -------------------------------------------------------------------------------- /playground/src/shime-uni.d.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | 3 | declare module "vue" { 4 | type Hooks = App.AppInstance & Page.PageInstance; 5 | interface ComponentCustomOptions extends Hooks {} 6 | } -------------------------------------------------------------------------------- /packages/core/src/_resolvers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ano-ui' 2 | export * from './uni-ui' 3 | export * from './wot-design-uni' 4 | export * from './uv-ui' 5 | export * from './uview-pro' 6 | -------------------------------------------------------------------------------- /playground/src/components/ComponentD.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /playground/src/components/book/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /playground/src/components/ComponentA.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /playground/src/components/component-c.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /playground/src/components/ComponentB.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /playground/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createSSRApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | export function createApp() { 5 | const app = createSSRApp(App) 6 | return { 7 | app, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const MODULE_NAME = 'vite-plugin-uni-components' 2 | export const RESOLVER_EXT = '.vite-plugin-uni-components' 3 | export const DISABLE_COMMENT = '/* vite-plugin-uni-components disabled */' 4 | export const DIRECTIVE_IMPORT_PREFIX = 'v' 5 | -------------------------------------------------------------------------------- /playground/src/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 14 | -------------------------------------------------------------------------------- /packages/core/test/__snapshots__/search.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`search > should with namespace & collapse 1`] = `[]`; 4 | 5 | exports[`search > should with namespace 1`] = `[]`; 6 | 7 | exports[`search > should work 1`] = `[]`; 8 | -------------------------------------------------------------------------------- /playground/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | *.local 14 | 15 | # Editor directories and files 16 | .idea 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? -------------------------------------------------------------------------------- /playground/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import { DefineComponent } from 'vue' 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 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 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from 'unbuild' 2 | 3 | export default defineBuildConfig({ 4 | entries: [ 5 | 'src/index', 6 | 'src/resolvers', 7 | ], 8 | declaration: true, 9 | clean: true, 10 | rollup: { 11 | emitCJS: true, 12 | }, 13 | externals: [ 14 | 'vite', 15 | 'estree-walker', 16 | ], 17 | }) 18 | -------------------------------------------------------------------------------- /packages/core/src/_resolvers/ano-ui.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentResolver } from '../types' 2 | 3 | export function AnoResolver(): ComponentResolver { 4 | return { 5 | type: 'component', 6 | resolve: (name: string) => { 7 | if (name.match(/^A[A-Z]/)) 8 | return { name, from: `ano-ui/components/${name}/${name}.vue` } 9 | }, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.json", 3 | "compilerOptions": { 4 | "ignoreDeprecations": "5.0", 5 | "sourceMap": true, 6 | "baseUrl": ".", 7 | "paths": { 8 | "@/*": ["./src/*"] 9 | }, 10 | "lib": ["esnext", "dom"], 11 | "types": ["@dcloudio/types"] 12 | }, 13 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/test/__snapshots__/stringifyComponentImport.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`stringifyComponentImport > importName 1`] = `"import Test from 'test'"`; 4 | 5 | exports[`stringifyComponentImport > multiple sideEffects 1`] = `"import Test from 'test';import 'test.css';import css from 'test2.css'"`; 6 | 7 | exports[`stringifyComponentImport > plain css sideEffects 1`] = `"import Test from 'test';import 'test.css'"`; 8 | -------------------------------------------------------------------------------- /playground/src/pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ // pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages 3 | { 4 | "path": "pages/index/index", 5 | "style": { 6 | "navigationBarTitleText": "uni-app" 7 | } 8 | } 9 | ], 10 | "globalStyle": { 11 | "navigationBarTextStyle": "black", 12 | "navigationBarTitleText": "uni-app", 13 | "navigationBarBackgroundColor": "#F8F8F8", 14 | "backgroundColor": "#F8F8F8" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /playground/src/components/Recursive.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | 21 | 26 | -------------------------------------------------------------------------------- /packages/core/src/_resolvers/uv-ui.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentResolver } from '../types' 2 | 3 | import { kebabCase } from '../utils' 4 | 5 | export function UvResolver(): ComponentResolver { 6 | return { 7 | type: 'component', 8 | resolve: (name: string) => { 9 | if (name.match(/^Uv[A-Z]/)) { 10 | const compName = kebabCase(name) 11 | return { 12 | name, 13 | from: `@climblee/uv-ui/components/${compName}/${compName}.vue`, 14 | } 15 | } 16 | }, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/src/_resolvers/wot-design-uni.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentResolver } from '../types' 2 | 3 | import { kebabCase } from '../utils' 4 | 5 | export function WotResolver(): ComponentResolver { 6 | return { 7 | type: 'component', 8 | resolve: (name: string) => { 9 | if (name.match(/^Wd[A-Z]/)) { 10 | const compName = kebabCase(name) 11 | return { 12 | name, 13 | from: `wot-design-uni/components/${compName}/${compName}.vue`, 14 | } 15 | } 16 | }, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /packages/core/src/fs/glob.ts: -------------------------------------------------------------------------------- 1 | import fg from 'fast-glob' 2 | import Debug from 'debug' 3 | import type { Context } from '../context' 4 | 5 | const debug = Debug('vite-plugin-uni-components:glob') 6 | 7 | export function searchComponents(ctx: Context) { 8 | debug(`started with: [${ctx.options.globs.join(', ')}]`) 9 | const root = ctx.root 10 | 11 | const files = fg.sync(ctx.options.globs, { 12 | ignore: ['node_modules'], 13 | onlyFiles: true, 14 | cwd: root, 15 | absolute: true, 16 | }) 17 | 18 | if (!files.length && !ctx.options.resolvers?.length) 19 | 20 | console.warn('[vite-plugin-uni-components] no components found') 21 | 22 | debug(`${files.length} components found.`) 23 | 24 | ctx.addComponents(files) 25 | } 26 | -------------------------------------------------------------------------------- /packages/core/src/transforms/directive/vue3.ts: -------------------------------------------------------------------------------- 1 | import type MagicString from 'magic-string' 2 | import type { ResolveResult } from '../../transformer' 3 | 4 | export default function resolveVue3(code: string, s: MagicString): ResolveResult[] { 5 | const results: ResolveResult[] = [] 6 | 7 | for (const match of code.matchAll(/_resolveDirective\("(.+?)"\)/g)) { 8 | const matchedName = match[1] 9 | if (match.index != null && matchedName && !matchedName.startsWith('_')) { 10 | const start = match.index 11 | const end = start + match[0].length 12 | results.push({ 13 | rawName: matchedName, 14 | replace: resolved => s.overwrite(start, end, resolved), 15 | }) 16 | } 17 | } 18 | 19 | return results 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/src/_resolvers/uview-pro.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentResolver } from '../types' 2 | 3 | import { kebabCase } from '../utils' 4 | /** 5 | * uView Pro 组件解析器 6 | * @param prefix - 组件导入路径的前缀,默认为 'uview-pro' 7 | * @param [prefix='uview-pro'] - 可传入 uni_modules 路径以支持 uni_modules 8 | * @returns uView Pro 组件的解析器函数 9 | * @example 10 | * // 基本用法 11 | * uViewProResolver() 12 | * 13 | * // 支持 uni_modules 14 | * uViewProResolver('@/uni_modules/uview-pro') 15 | */ 16 | export function uViewProResolver(prefix = 'uview-pro'): ComponentResolver { 17 | return { 18 | type: 'component', 19 | resolve: (name: string) => { 20 | if (name.match(/^U[A-Z]/)) { 21 | const compName = kebabCase(name) 22 | return { 23 | name, 24 | from: `${prefix}/components/${compName}/${compName}.vue`, 25 | } 26 | } 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/core/test/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import type { ResolvedOptions } from '../src' 3 | import { getNameFromFilePath } from '../src/utils' 4 | 5 | describe('getNameFromFilePath', () => { 6 | const options: Partial = { 7 | directoryAsNamespace: true, 8 | globalNamespaces: [], 9 | collapseSamePrefixes: false, 10 | resolvedDirs: ['/src/components'], 11 | } 12 | 13 | it('normal name', () => { 14 | const inComponentFilePath = '/src/components/a/b.vue' 15 | expect(getNameFromFilePath(inComponentFilePath, options as ResolvedOptions)).toBe('a-b') 16 | }) 17 | 18 | it('special char', () => { 19 | const inComponentFilePath = '/src/components/[a1]/b_2/c 3/d.4/[...ef]/ghi.vue' 20 | expect(getNameFromFilePath(inComponentFilePath, options as ResolvedOptions)).toBe('a1-b2-c3-d4-ef-ghi') 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /packages/core/src/_resolvers/uni-ui.ts: -------------------------------------------------------------------------------- 1 | import type { FilterPattern } from '@rollup/pluginutils' 2 | import type { ComponentResolver } from '../types' 3 | import { isExclude, kebabCase } from '../utils' 4 | 5 | export interface UniUIResolverOptions { 6 | /** 7 | * RegExp or string to match component names that will NOT be imported 8 | */ 9 | exclude?: FilterPattern 10 | } 11 | 12 | export function UniUIResolver( 13 | options: UniUIResolverOptions = {}, 14 | ): ComponentResolver { 15 | return { 16 | type: 'component', 17 | resolve: (name: string) => { 18 | // Compatible with @uni-helper/vite-plugin-uni-layouts 19 | if (isExclude(name, options.exclude) || name === 'UniLayout') 20 | return 21 | 22 | if (name.match(/^Uni[A-Z]/)) { 23 | const partialName = kebabCase(name) 24 | return { 25 | name, 26 | from: `@dcloudio/uni-ui/lib/${partialName}/${partialName}.vue`, 27 | } 28 | } 29 | }, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: Install pnpm 17 | uses: pnpm/action-setup@v2 18 | 19 | - name: Set node 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 18.x 23 | cache: pnpm 24 | registry-url: 'https://registry.npmjs.org' 25 | 26 | - run: npx changelogithub 27 | continue-on-error: true 28 | env: 29 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 30 | 31 | - name: Install Dependencies 32 | run: pnpm i 33 | 34 | - name: PNPM build 35 | run: pnpm run build 36 | 37 | - name: Publish to NPM 38 | run: pnpm -r publish --access public --no-git-checks 39 | env: 40 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 41 | -------------------------------------------------------------------------------- /playground/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | import { defineConfig } from 'vite' 3 | import Uni from '@dcloudio/vite-plugin-uni' 4 | import Components from '@uni-helper/vite-plugin-uni-components' 5 | import { UniUIResolver, UvResolver, WotResolver, uViewProResolver } from '@uni-helper/vite-plugin-uni-components/resolvers' 6 | import { AnoResolver } from 'ano-ui' 7 | import Inspect from 'vite-plugin-inspect' 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig({ 11 | resolve: { 12 | alias: { 13 | '/~/': `${resolve(__dirname, 'src')}/`, 14 | }, 15 | }, 16 | plugins: [ 17 | Components({ 18 | directoryAsNamespace: true, 19 | dts: true, 20 | globalNamespaces: ['global'], 21 | resolvers: [ 22 | AnoResolver(), 23 | UniUIResolver({ 24 | exclude: 'UniTest', 25 | }), 26 | WotResolver(), 27 | UvResolver(), 28 | uViewProResolver(), 29 | ], 30 | excludeNames: ['Book'], 31 | }), 32 | Uni(), 33 | Inspect(), 34 | ], 35 | }) 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorepo", 3 | "version": "0.2.3", 4 | "private": true, 5 | "packageManager": "pnpm@8.15.5", 6 | "scripts": { 7 | "stub": "pnpm -r --filter=./packages/* --parallel run stub", 8 | "dev": "pnpm stub", 9 | "build": "pnpm -r --filter=./packages/* run build && pnpm -r run build-post", 10 | "release": "bumpp package.json packages/**/package.json", 11 | "play:h5": "npm -C playground run dev:h5", 12 | "play:build:h5": "npm -C playground run build:h5", 13 | "play:mp-weixin": "npm -C playground run dev:mp-weixin", 14 | "play:build:mp-weixin": "npm -C playground run build:mp-weixin", 15 | "prepublishOnly": "nr build", 16 | "test": "vitest", 17 | "typecheck": "tsc --noEmit", 18 | "lint": "eslint .", 19 | "lint:fix": "pnpm lint --fix" 20 | }, 21 | "devDependencies": { 22 | "@antfu/eslint-config": "^0.38.4", 23 | "bumpp": "^9.1.0", 24 | "eslint": "^8.37.0", 25 | "typescript": "^5.0.3", 26 | "unbuild": "^1.2.0", 27 | "vite": "^4.2.1", 28 | "vitest": "^0.29.8" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Neil Lee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/core/test/stringifyComponentImport.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { Context } from '../src/context' 3 | import { stringifyComponentImport } from '../src/utils' 4 | 5 | describe('stringifyComponentImport', () => { 6 | it('importName', async () => { 7 | const ctx = new Context({}) 8 | expect( 9 | stringifyComponentImport({ 10 | as: 'Test', 11 | from: 'test', 12 | name: 'a', 13 | }, ctx), 14 | ).toMatchSnapshot() 15 | }) 16 | 17 | it('plain css sideEffects', async () => { 18 | const ctx = new Context({}) 19 | expect( 20 | stringifyComponentImport({ 21 | as: 'Test', 22 | from: 'test', 23 | sideEffects: 'test.css', 24 | }, ctx), 25 | ).toMatchSnapshot() 26 | }) 27 | 28 | it('multiple sideEffects', async () => { 29 | const ctx = new Context({}) 30 | expect( 31 | stringifyComponentImport({ 32 | as: 'Test', 33 | from: 'test', 34 | sideEffects: [ 35 | 'test.css', 36 | { as: 'css', from: 'test2.css' }, 37 | ], 38 | }, ctx), 39 | ).toMatchSnapshot() 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /packages/core/src/transformer.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug' 2 | import MagicString from 'magic-string' 3 | import type { TransformResult } from 'rollup' 4 | import type { SupportedTransformer, Transformer } from './types' 5 | import { DISABLE_COMMENT } from './constants' 6 | import type { Context } from './context' 7 | import transformComponent from './transforms/component' 8 | import transformDirectives from './transforms/directive' 9 | 10 | const debug = Debug('vite-plugin-uni-components:transformer') 11 | 12 | export interface ResolveResult { 13 | rawName: string 14 | replace: (resolved: string) => void 15 | } 16 | 17 | export default function transformer(ctx: Context, transformer: SupportedTransformer): Transformer { 18 | return async (code, id, path) => { 19 | ctx.searchGlob() 20 | 21 | const sfcPath = ctx.normalizePath(path) 22 | debug(sfcPath) 23 | 24 | const s = new MagicString(code) 25 | 26 | await transformComponent(code, transformer, s, ctx, sfcPath) 27 | if (ctx.options.directives) 28 | await transformDirectives(code, transformer, s, ctx, sfcPath) 29 | 30 | s.prepend(DISABLE_COMMENT) 31 | 32 | const result: TransformResult = { code: s.toString() } 33 | if (ctx.sourcemap) 34 | result.map = s.generateMap({ source: id, includeContent: true }) 35 | return result 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/core/src/transforms/directive/index.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug' 2 | import type MagicString from 'magic-string' 3 | import { pascalCase, stringifyComponentImport } from '../../utils' 4 | import type { Context } from '../../context' 5 | import type { SupportedTransformer } from '../../types' 6 | import { DIRECTIVE_IMPORT_PREFIX } from '../../constants' 7 | import vue2Resolver from './vue2' 8 | import vue3Resolver from './vue3' 9 | 10 | const debug = Debug('vite-plugin-uni-components:transform:directive') 11 | 12 | export default async function transformDirective(code: string, transformer: SupportedTransformer, s: MagicString, ctx: Context, sfcPath: string) { 13 | let no = 0 14 | 15 | const results = await (transformer === 'vue2' ? vue2Resolver(code, s) : vue3Resolver(code, s)) 16 | for (const { rawName, replace } of results) { 17 | debug(`| ${rawName}`) 18 | const name = `${DIRECTIVE_IMPORT_PREFIX}${pascalCase(rawName)}` 19 | ctx.updateUsageMap(sfcPath, [name]) 20 | 21 | const directive = await ctx.findComponent(name, 'directive', [sfcPath]) 22 | if (!directive) 23 | continue 24 | 25 | const varName = `__unplugin_directives_${no}` 26 | s.prepend(`${stringifyComponentImport({ ...directive, as: varName }, ctx)};\n`) 27 | no += 1 28 | replace(varName) 29 | } 30 | 31 | debug(`^ (${no})`) 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # @uni-helper/vite-plugin-uni-components 2 | 3 | Forked from [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components) and modified to adapt UniApp. 4 | 5 | [![NPM version](https://img.shields.io/npm/v/@uni-helper/vite-plugin-uni-components?color=a1b858&label=)](https://www.npmjs.com/package/@uni-helper/vite-plugin-uni-components) 6 | 7 | ## Install 8 | 9 | ```bash 10 | pnpm i -D @uni-helper/vite-plugin-uni-components 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```ts 16 | // vite.config.ts 17 | import { defineConfig } from 'vite' 18 | import Uni from '@dcloudio/vite-plugin-uni' 19 | import Components from '@uni-helper/vite-plugin-uni-components' 20 | 21 | // https://vitejs.dev/config/ 22 | export default defineConfig({ 23 | plugins: [ 24 | // make sure put it before `Uni()` 25 | Components(), 26 | Uni(), 27 | ], 28 | }) 29 | ``` 30 | 31 | ## Component type prompt 32 | 33 | If you use `pnpm`, please create a `.npmrc` file in root, see [issue](https://github.com/antfu/unplugin-vue-components/issues/389). 34 | 35 | ``` 36 | // .npmrc 37 | public-hoist-pattern[]=@vue* 38 | // or 39 | // shamefully-hoist = true 40 | ``` 41 | 42 | see more in [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components#installation) 43 | 44 | ## License 45 | 46 | [MIT](./LICENSE) License © 2023-PRESENT [Neil Lee](https://github.com/zguolee) 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | banner 2 | 3 |
4 | 5 | 6 | 7 | 8 | 从 **unplugin-vue-components** 派生并修改以适应Uniapp。 9 | 10 | ## 安装 11 | 12 | ```bash 13 | pnpm i -D @uni-helper/vite-plugin-uni-components 14 | ``` 15 | 16 | ## 使用 17 | 18 | 📖 **请阅读[完整文档](https://uni-helper.js.org/vite-plugin-uni-components)了解完整使用方法!** 19 | 20 | ```ts 21 | // vite.config.ts 22 | import Uni from '@dcloudio/vite-plugin-uni' 23 | import Components from '@uni-helper/vite-plugin-uni-components' 24 | import { defineConfig } from 'vite' 25 | 26 | export default defineConfig({ 27 | plugins: [ 28 | Components(), // 需要在 Uni() 之前调用 29 | Uni(), 30 | ], 31 | }) 32 | ``` 33 | 34 | ## 感谢 35 | 36 | - [vite-plugin-uni-components](https://github.com/unplugin/unplugin-vue-components) 37 | -------------------------------------------------------------------------------- /playground/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by vite-plugin-uni-components 5 | // Read more: https://github.com/vuejs/core/pull/3399 6 | export {} 7 | 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | AButton: typeof import('ano-ui/components/AButton/AButton.vue')['default'] 11 | ComponentA: typeof import('./src/components/ComponentA.vue')['default'] 12 | ComponentAsync: typeof import('./src/components/ComponentAsync.vue')['default'] 13 | ComponentB: typeof import('./src/components/ComponentB.vue')['default'] 14 | ComponentC: typeof import('./src/components/component-c.vue')['default'] 15 | ComponentD: typeof import('./src/components/ComponentD.vue')['default'] 16 | Recursive: typeof import('./src/components/Recursive.vue')['default'] 17 | UButton: typeof import('uview-pro/components/u-button/u-button.vue')['default'] 18 | UniCalendar: typeof import('@dcloudio/uni-ui/lib/uni-calendar/uni-calendar.vue')['default'] 19 | UvButton: typeof import('@climblee/uv-ui/components/uv-button/uv-button.vue')['default'] 20 | UvIcon: typeof import('@climblee/uv-ui/components/uv-icon/uv-icon.vue')['default'] 21 | UvLoadingIcon: typeof import('@climblee/uv-ui/components/uv-loading-icon/uv-loading-icon.vue')['default'] 22 | WdButton: typeof import('wot-design-uni/components/wd-button/wd-button.vue')['default'] 23 | WdIcon: typeof import('wot-design-uni/components/wd-icon/wd-icon.vue')['default'] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'node:fs' 2 | import chokidar from 'chokidar' 3 | import type { Plugin } from 'vite' 4 | import { shouldTransform } from './utils' 5 | import type { Options } from './types' 6 | import { Context } from './context' 7 | 8 | export default function VitePluginComponents(options: Options = {}): Plugin { 9 | const ctx: Context = new Context(options) 10 | 11 | return { 12 | name: 'vite-plugin-uni-components', 13 | enforce: 'post', 14 | configResolved(config) { 15 | ctx.setRoot(config.root) 16 | ctx.sourcemap = true 17 | 18 | if (config.plugins.find(i => i.name === 'vite-plugin-vue2')) 19 | ctx.setTransformer('vue2') 20 | 21 | if (ctx.options.dts) { 22 | ctx.searchGlob() 23 | if (!existsSync(ctx.options.dts)) 24 | ctx.generateDeclaration() 25 | } 26 | 27 | if (config.build.watch && config.command === 'build') 28 | ctx.setupWatcher(chokidar.watch(ctx.options.globs)) 29 | }, 30 | configureServer(server) { 31 | ctx.setupViteServer(server) 32 | }, 33 | async transform(code, id) { 34 | if (!shouldTransform(code)) 35 | return null 36 | try { 37 | const result = await ctx.transform(code, id) 38 | ctx.generateDeclaration() 39 | return result 40 | } 41 | catch (e) { 42 | this.error(e as string) 43 | } 44 | }, 45 | } 46 | } 47 | 48 | export * from './types' 49 | export { camelCase, pascalCase, kebabCase } from './utils' 50 | -------------------------------------------------------------------------------- /playground/src/pages/index/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 59 | 60 | 68 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uni-helper/vite-plugin-uni-components", 3 | "type": "module", 4 | "version": "0.2.3", 5 | "packageManager": "pnpm@8.15.5", 6 | "description": "", 7 | "license": "MIT", 8 | "homepage": "https://github.com/uni-helper/vite-plugin-uni-components#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/uni-helper/vite-plugin-uni-components.git" 12 | }, 13 | "bugs": "https://github.com/uni-helper/vite-plugin-uni-components/issues", 14 | "keywords": [], 15 | "sideEffects": false, 16 | "exports": { 17 | ".": { 18 | "types": "./dist/index.d.ts", 19 | "require": "./dist/index.cjs", 20 | "import": "./dist/index.mjs" 21 | }, 22 | "./resolvers": { 23 | "types": "./dist/resolvers.d.ts", 24 | "require": "./dist/resolvers.cjs", 25 | "import": "./dist/resolvers.mjs" 26 | }, 27 | "./*": "./*" 28 | }, 29 | "main": "dist/index.cjs", 30 | "module": "dist/index.mjs", 31 | "types": "index.d.ts", 32 | "typesVersions": { 33 | "*": { 34 | "*": [ 35 | "./dist/*" 36 | ] 37 | } 38 | }, 39 | "files": [ 40 | "dist" 41 | ], 42 | "scripts": { 43 | "build": "unbuild", 44 | "stub": "unbuild --stub" 45 | }, 46 | "dependencies": { 47 | "@antfu/utils": "^0.7.2", 48 | "@rollup/pluginutils": "^5.0.2", 49 | "chokidar": "^3.5.3", 50 | "debug": "^4.3.4", 51 | "fast-glob": "^3.2.12", 52 | "local-pkg": "^0.4.3", 53 | "magic-string": "^0.30.0", 54 | "minimatch": "^8.0.3", 55 | "resolve": "^1.22.2" 56 | }, 57 | "devDependencies": { 58 | "@babel/parser": "^7.21.4", 59 | "@babel/types": "^7.21.4", 60 | "@types/debug": "^4.1.7", 61 | "@types/minimatch": "^5.1.2", 62 | "@types/node": "^18.15.11", 63 | "@types/resolve": "^1.20.2", 64 | "bumpp": "^9.1.0", 65 | "esno": "^0.16.3", 66 | "estree-walker": "^3.0.3", 67 | "pathe": "^1.1.0", 68 | "rollup": "^3.20.2" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /playground/src/uni.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 这里是uni-app内置的常用样式变量 3 | * 4 | * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量 5 | * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App 6 | * 7 | */ 8 | 9 | /** 10 | * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能 11 | * 12 | * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件 13 | */ 14 | 15 | /* 颜色变量 */ 16 | 17 | /* 行为相关颜色 */ 18 | $uni-color-primary: #007aff; 19 | $uni-color-success: #4cd964; 20 | $uni-color-warning: #f0ad4e; 21 | $uni-color-error: #dd524d; 22 | 23 | /* 文字基本颜色 */ 24 | $uni-text-color: #333; // 基本色 25 | $uni-text-color-inverse: #fff; // 反色 26 | $uni-text-color-grey: #999; // 辅助灰色,如加载更多的提示信息 27 | $uni-text-color-placeholder: #808080; 28 | $uni-text-color-disable: #c0c0c0; 29 | 30 | /* 背景颜色 */ 31 | $uni-bg-color: #fff; 32 | $uni-bg-color-grey: #f8f8f8; 33 | $uni-bg-color-hover: #f1f1f1; // 点击状态颜色 34 | $uni-bg-color-mask: rgba(0, 0, 0, 0.4); // 遮罩颜色 35 | 36 | /* 边框颜色 */ 37 | $uni-border-color: #c8c7cc; 38 | 39 | /* 尺寸变量 */ 40 | 41 | /* 文字尺寸 */ 42 | $uni-font-size-sm: 12px; 43 | $uni-font-size-base: 14px; 44 | $uni-font-size-lg: 16px; 45 | 46 | /* 图片尺寸 */ 47 | $uni-img-size-sm: 20px; 48 | $uni-img-size-base: 26px; 49 | $uni-img-size-lg: 40px; 50 | 51 | /* Border Radius */ 52 | $uni-border-radius-sm: 2px; 53 | $uni-border-radius-base: 3px; 54 | $uni-border-radius-lg: 6px; 55 | $uni-border-radius-circle: 50%; 56 | 57 | /* 水平间距 */ 58 | $uni-spacing-row-sm: 5px; 59 | $uni-spacing-row-base: 10px; 60 | $uni-spacing-row-lg: 15px; 61 | 62 | /* 垂直间距 */ 63 | $uni-spacing-col-sm: 4px; 64 | $uni-spacing-col-base: 8px; 65 | $uni-spacing-col-lg: 12px; 66 | 67 | /* 透明度 */ 68 | $uni-opacity-disabled: 0.3; // 组件禁用态的透明度 69 | 70 | /* 文章场景相关 */ 71 | $uni-color-title: #2c405a; // 文章标题颜色 72 | $uni-font-size-title: 20px; 73 | $uni-color-subtitle: #555; // 二级标题颜色 74 | $uni-font-size-subtitle: 18px; 75 | $uni-color-paragraph: #3f536e; // 文章段落颜色 76 | $uni-font-size-paragraph: 15px; 77 | 78 | /* uView Pro 主题变量 */ 79 | @import "uview-pro/theme.scss"; 80 | -------------------------------------------------------------------------------- /packages/core/test/search.test.ts: -------------------------------------------------------------------------------- 1 | import { relative, resolve } from 'pathe' 2 | import { describe, expect, it } from 'vitest' 3 | import { Context } from '../src/context' 4 | 5 | const root = resolve(__dirname, '../examples/vite-vue3') 6 | 7 | function cleanup(data: any) { 8 | return Object.values(data).map((e: any) => { 9 | delete e.absolute 10 | e.from = relative(root, e.from).replace(/\\/g, '/') 11 | return e 12 | }).sort((a, b) => (a.as as string).localeCompare(b.as)) 13 | } 14 | 15 | describe('search', () => { 16 | it('should work', async () => { 17 | const ctx = new Context({}) 18 | ctx.setRoot(root) 19 | ctx.searchGlob() 20 | 21 | expect(cleanup(ctx.componentNameMap)).toMatchSnapshot() 22 | }) 23 | 24 | it('should with namespace', async () => { 25 | const ctx = new Context({ 26 | directoryAsNamespace: true, 27 | globalNamespaces: ['global'], 28 | }) 29 | ctx.setRoot(root) 30 | ctx.searchGlob() 31 | 32 | expect(cleanup(ctx.componentNameMap)).toMatchSnapshot() 33 | }) 34 | 35 | it('should with namespace & collapse', async () => { 36 | const ctx = new Context({ 37 | directoryAsNamespace: true, 38 | collapseSamePrefixes: true, 39 | globalNamespaces: ['global'], 40 | }) 41 | ctx.setRoot(root) 42 | ctx.searchGlob() 43 | 44 | expect(cleanup(ctx.componentNameMap)).toMatchSnapshot() 45 | }) 46 | 47 | it('should globs exclude work', () => { 48 | const ctx = new Context({ 49 | globs: [ 50 | 'src/components/*.vue', 51 | '!src/components/ComponentA.vue', 52 | ], 53 | }) 54 | ctx.setRoot(root) 55 | ctx.searchGlob() 56 | 57 | expect(cleanup(ctx.componentNameMap).map(i => i.as)).not.toEqual(expect.arrayContaining(['ComponentA'])) 58 | }) 59 | 60 | it('should globs exclude work with dirs', () => { 61 | const ctx = new Context({ 62 | dirs: [ 63 | 'src/components', 64 | '!src/components/book', 65 | ], 66 | }) 67 | ctx.setRoot(root) 68 | ctx.searchGlob() 69 | 70 | expect(cleanup(ctx.componentNameMap).map(i => i.as)).not.toEqual(expect.arrayContaining(['Book'])) 71 | }) 72 | 73 | it('should excludeNames', () => { 74 | const ctx = new Context({ 75 | dirs: ['src/components'], 76 | excludeNames: ['Book'], 77 | }) 78 | ctx.setRoot(root) 79 | ctx.searchGlob() 80 | 81 | expect(cleanup(ctx.componentNameMap).map(i => i.as)).not.toEqual(expect.arrayContaining(['Book'])) 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /playground/src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "appid": "", 4 | "description": "", 5 | "versionName": "1.0.0", 6 | "versionCode": "100", 7 | "transformPx": false, 8 | /* 5+App特有相关 */ 9 | "app-plus": { 10 | "usingComponents": true, 11 | "nvueStyleCompiler": "uni-app", 12 | "compilerVersion": 3, 13 | "splashscreen": { 14 | "alwaysShowBeforeRender": true, 15 | "waiting": true, 16 | "autoclose": true, 17 | "delay": 0 18 | }, 19 | /* 模块配置 */ 20 | "modules": {}, 21 | /* 应用发布信息 */ 22 | "distribute": { 23 | /* android打包配置 */ 24 | "android": { 25 | "permissions": [ 26 | "", 27 | "", 28 | "", 29 | "", 30 | "", 31 | "", 32 | "", 33 | "", 34 | "", 35 | "", 36 | "", 37 | "", 38 | "", 39 | "", 40 | "" 41 | ] 42 | }, 43 | /* ios打包配置 */ 44 | "ios": {}, 45 | /* SDK配置 */ 46 | "sdkConfigs": {} 47 | } 48 | }, 49 | /* 快应用特有相关 */ 50 | "quickapp": {}, 51 | /* 小程序特有相关 */ 52 | "mp-weixin": { 53 | "appid": "", 54 | "setting": { 55 | "urlCheck": false 56 | }, 57 | "usingComponents": true 58 | }, 59 | "mp-alipay": { 60 | "usingComponents": true 61 | }, 62 | "mp-baidu": { 63 | "usingComponents": true 64 | }, 65 | "mp-toutiao": { 66 | "usingComponents": true 67 | }, 68 | "uniStatistics": { 69 | "enable": false 70 | }, 71 | "vueVersion": "3" 72 | } 73 | -------------------------------------------------------------------------------- /packages/core/src/transforms/component.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug' 2 | import type MagicString from 'magic-string' 3 | import { pascalCase, stringifyComponentImport } from '../utils' 4 | import type { Context } from '../context' 5 | import type { ResolveResult } from '../transformer' 6 | import type { SupportedTransformer } from '../types' 7 | 8 | const debug = Debug('vite-plugin-uni-components:transform:component') 9 | 10 | function resolveVue2(code: string, s: MagicString) { 11 | const results: ResolveResult[] = [] 12 | for (const match of code.matchAll(/\b(_c|h)\([\s\n\t]*['"](.+?)["']([,)])/g)) { 13 | const [full, renderFunctionName, matchedName, append] = match 14 | if (match.index != null && matchedName && !matchedName.startsWith('_')) { 15 | const start = match.index 16 | const end = start + full.length 17 | results.push({ 18 | rawName: matchedName, 19 | replace: resolved => s.overwrite(start, end, `${renderFunctionName}(${resolved}${append}`), 20 | }) 21 | } 22 | } 23 | 24 | return results 25 | } 26 | 27 | function resolveVue3(code: string, s: MagicString) { 28 | const results: ResolveResult[] = [] 29 | 30 | /** 31 | * when using some plugin like plugin-vue-jsx, resolveComponent will be imported as resolveComponent1 to avoid duplicate import 32 | */ 33 | for (const match of code.matchAll(/_resolveComponent[0-9]*\("(.+?)"\)/g)) { 34 | const matchedName = match[1] 35 | if (match.index != null && matchedName && !matchedName.startsWith('_')) { 36 | const start = match.index 37 | const end = start + match[0].length 38 | results.push({ 39 | rawName: matchedName, 40 | replace: resolved => s.overwrite(start, end, resolved), 41 | }) 42 | } 43 | } 44 | 45 | return results 46 | } 47 | 48 | export default async function transformComponent(code: string, transformer: SupportedTransformer, s: MagicString, ctx: Context, sfcPath: string) { 49 | let no = 0 50 | 51 | const results = transformer === 'vue2' ? resolveVue2(code, s) : resolveVue3(code, s) 52 | 53 | for (const { rawName, replace } of results) { 54 | debug(`| ${rawName}`) 55 | const name = pascalCase(rawName) 56 | ctx.updateUsageMap(sfcPath, [name]) 57 | const component = await ctx.findComponent(name, 'component', [sfcPath]) 58 | if (component) { 59 | const varName = `__unplugin_components_${no}` 60 | s.prepend(`${stringifyComponentImport({ ...component, as: varName }, ctx)};\n`) 61 | no += 1 62 | replace(varName) 63 | } 64 | } 65 | 66 | debug(`^ (${no})`) 67 | } 68 | -------------------------------------------------------------------------------- /packages/core/src/options.ts: -------------------------------------------------------------------------------- 1 | import { join, resolve } from 'node:path' 2 | import { slash, toArray } from '@antfu/utils' 3 | import { getPackageInfoSync, isPackageExists } from 'local-pkg' 4 | import type { ComponentResolver, ComponentResolverObject, Options, ResolvedOptions } from './types' 5 | 6 | export const defaultOptions: Omit, 'include' | 'exclude' | 'excludeNames' | 'transformer' | 'globs' | 'directives' | 'types' | 'version'> = { 7 | dirs: 'src/components', 8 | extensions: 'vue', 9 | deep: true, 10 | dts: isPackageExists('typescript'), 11 | 12 | directoryAsNamespace: false, 13 | collapseSamePrefixes: false, 14 | globalNamespaces: [], 15 | 16 | resolvers: [], 17 | 18 | importPathTransform: v => v, 19 | 20 | allowOverrides: false, 21 | } 22 | 23 | function normalizeResolvers(resolvers: (ComponentResolver | ComponentResolver[])[]): ComponentResolverObject[] { 24 | return toArray(resolvers).flat().map(r => typeof r === 'function' ? { resolve: r, type: 'component' } : r) 25 | } 26 | 27 | export function resolveOptions(options: Options, root: string): ResolvedOptions { 28 | const resolved = Object.assign({}, defaultOptions, options) as ResolvedOptions 29 | resolved.resolvers = normalizeResolvers(resolved.resolvers) 30 | resolved.extensions = toArray(resolved.extensions) 31 | 32 | if (resolved.globs) { 33 | resolved.globs = toArray(resolved.globs).map((glob: string) => slash(resolve(root, glob))) 34 | resolved.resolvedDirs = [] 35 | } 36 | else { 37 | const extsGlob = resolved.extensions.length === 1 38 | ? resolved.extensions 39 | : `{${resolved.extensions.join(',')}}` 40 | 41 | resolved.dirs = toArray(resolved.dirs) 42 | resolved.resolvedDirs = resolved.dirs.map(i => slash(resolve(root, i))) 43 | 44 | resolved.globs = resolved.resolvedDirs.map(i => resolved.deep 45 | ? slash(join(i, `**/*.${extsGlob}`)) 46 | : slash(join(i, `*.${extsGlob}`)), 47 | ) 48 | 49 | if (!resolved.extensions.length) 50 | throw new Error('[vite-plugin-uni-components] `extensions` option is required to search for components') 51 | } 52 | 53 | resolved.dts = !resolved.dts 54 | ? false 55 | : resolve( 56 | root, 57 | typeof resolved.dts === 'string' 58 | ? resolved.dts 59 | : 'components.d.ts', 60 | ) 61 | 62 | resolved.types = resolved.types || [] 63 | 64 | resolved.root = root 65 | resolved.version = resolved.version ?? getVueVersion(root) 66 | if (resolved.version < 2 || resolved.version >= 4) 67 | throw new Error(`[vite-plugin-uni-components] unsupported version: ${resolved.version}`) 68 | 69 | resolved.transformer = options.transformer || `vue${Math.trunc(resolved.version) as 2 | 3}` 70 | resolved.directives = (typeof options.directives === 'boolean') 71 | ? options.directives 72 | : !resolved.resolvers.some(i => i.type === 'directive') 73 | ? false 74 | : resolved.version >= 3 75 | return resolved 76 | } 77 | 78 | function getVueVersion(root: string): 2 | 2.7 | 3 { 79 | const raw = getPackageInfoSync('vue', { paths: [root] })?.version || '3' 80 | const version = +(raw.split('.').slice(0, 2).join('.')) 81 | if (version === 2.7) 82 | return 2.7 83 | else if (version < 2.7) 84 | return 2 85 | return 3 86 | } 87 | -------------------------------------------------------------------------------- /packages/core/src/transforms/directive/vue2.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | BlockStatement, CallExpression, FunctionExpression, Node, ObjectProperty, Program, VariableDeclaration, 3 | } from '@babel/types' 4 | import type MagicString from 'magic-string' 5 | import { importModule, isPackageExists } from 'local-pkg' 6 | import type { ResolveResult } from '../../transformer' 7 | 8 | /** 9 | * get Vue 2 render function position 10 | * @param ast 11 | * @returns 12 | */ 13 | function getRenderFnStart(program: Program): number { 14 | const renderFn = program.body.find((node): node is VariableDeclaration => 15 | node.type === 'VariableDeclaration' 16 | && node.declarations[0].id.type === 'Identifier' 17 | && ['render', '_sfc_render'].includes(node.declarations[0].id.name), 18 | ) 19 | const start = (((renderFn?.declarations[0].init as FunctionExpression)?.body) as BlockStatement)?.start 20 | if (start === null || start === undefined) 21 | throw new Error('[vite-plugin-uni-components:directive] Cannot find render function position.') 22 | return start + 1 23 | } 24 | 25 | export default async function resolveVue2(code: string, s: MagicString): Promise { 26 | if (!isPackageExists('@babel/parser')) 27 | throw new Error('[vite-plugin-uni-components:directive] To use Vue 2 directive you will need to install Babel first: "npm install -D @babel/parser"') 28 | 29 | const { parse } = await importModule('@babel/parser') 30 | const { program } = parse(code, { 31 | sourceType: 'module', 32 | }) 33 | 34 | const nodes: CallExpression[] = [] 35 | const { walk } = await import('estree-walker') 36 | walk(program as any, { 37 | enter(node: any) { 38 | if ((node as Node).type === 'CallExpression') 39 | nodes.push(node) 40 | }, 41 | }) 42 | 43 | if (nodes.length === 0) 44 | return [] 45 | 46 | let _renderStart: number | undefined 47 | const getRenderStart = () => { 48 | if (_renderStart !== undefined) 49 | return _renderStart 50 | return (_renderStart = getRenderFnStart(program)) 51 | } 52 | 53 | const results: ResolveResult[] = [] 54 | for (const node of nodes) { 55 | const { callee, arguments: args } = node 56 | // _c(_, {}) 57 | if (callee.type !== 'Identifier' || callee.name !== '_c' || args[1]?.type !== 'ObjectExpression') 58 | continue 59 | 60 | // { directives: [] } 61 | const directives = args[1].properties.find( 62 | (property): property is ObjectProperty => 63 | property.type === 'ObjectProperty' 64 | && property.key.type === 'Identifier' 65 | && property.key.name === 'directives', 66 | )?.value 67 | if (!directives || directives.type !== 'ArrayExpression') 68 | continue 69 | 70 | for (const directive of directives.elements) { 71 | if (directive?.type !== 'ObjectExpression') 72 | continue 73 | 74 | const nameNode = directive.properties.find( 75 | (p): p is ObjectProperty => 76 | p.type === 'ObjectProperty' 77 | && p.key.type === 'Identifier' 78 | && p.key.name === 'name', 79 | )?.value 80 | if (nameNode?.type !== 'StringLiteral') 81 | continue 82 | const name = nameNode.value 83 | if (!name || name.startsWith('_')) 84 | continue 85 | results.push({ 86 | rawName: name, 87 | replace: (resolved) => { 88 | s.prependLeft(getRenderStart(), `\nthis.$options.directives["${name}"] = ${resolved};`) 89 | }, 90 | }) 91 | } 92 | } 93 | 94 | return results 95 | } 96 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uni-preset-vue", 3 | "version": "0.0.0", 4 | "private": "true", 5 | "scripts": { 6 | "dev:app": "uni -p app", 7 | "dev:app-android": "uni -p app-android", 8 | "dev:app-ios": "uni -p app-ios", 9 | "dev:custom": "uni -p", 10 | "dev:h5": "cross-env DEBUG=vite-plugin-uni-components:* uni", 11 | "dev:h5:ssr": "uni --ssr", 12 | "dev:mp-alipay": "uni -p mp-alipay", 13 | "dev:mp-baidu": "uni -p mp-baidu", 14 | "dev:mp-kuaishou": "uni -p mp-kuaishou", 15 | "dev:mp-lark": "uni -p mp-lark", 16 | "dev:mp-qq": "uni -p mp-qq", 17 | "dev:mp-toutiao": "uni -p mp-toutiao", 18 | "dev:mp-weixin": "cross-env DEBUG=vite-plugin-uni-components:* uni -p mp-weixin", 19 | "dev:quickapp-webview": "uni -p quickapp-webview", 20 | "dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei", 21 | "dev:quickapp-webview-union": "uni -p quickapp-webview-union", 22 | "build:app": "uni build -p app", 23 | "build:app-android": "uni build -p app-android", 24 | "build:app-ios": "uni build -p app-ios", 25 | "build:custom": "uni build -p", 26 | "build:h5": "cross-env DEBUG=vite-plugin-uni-components:* uni build", 27 | "build:h5:ssr": "uni build --ssr", 28 | "build:mp-alipay": "uni build -p mp-alipay", 29 | "build:mp-baidu": "uni build -p mp-baidu", 30 | "build:mp-kuaishou": "uni build -p mp-kuaishou", 31 | "build:mp-lark": "uni build -p mp-lark", 32 | "build:mp-qq": "uni build -p mp-qq", 33 | "build:mp-toutiao": "uni build -p mp-toutiao", 34 | "build:mp-weixin": "cross-env DEBUG=vite-plugin-uni-components:* uni build -p mp-weixin", 35 | "build:quickapp-webview": "uni build -p quickapp-webview", 36 | "build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei", 37 | "build:quickapp-webview-union": "uni build -p quickapp-webview-union", 38 | "type-check": "vue-tsc --noEmit" 39 | }, 40 | "dependencies": { 41 | "@dcloudio/uni-app": "3.0.0-4070620250821001", 42 | "@dcloudio/uni-app-harmony": "3.0.0-4070620250821001", 43 | "@dcloudio/uni-app-plus": "3.0.0-4070620250821001", 44 | "@dcloudio/uni-components": "3.0.0-4070620250821001", 45 | "@dcloudio/uni-h5": "3.0.0-4070620250821001", 46 | "@dcloudio/uni-mp-alipay": "3.0.0-4070620250821001", 47 | "@dcloudio/uni-mp-baidu": "3.0.0-4070620250821001", 48 | "@dcloudio/uni-mp-harmony": "3.0.0-4070620250821001", 49 | "@dcloudio/uni-mp-jd": "3.0.0-4070620250821001", 50 | "@dcloudio/uni-mp-kuaishou": "3.0.0-4070620250821001", 51 | "@dcloudio/uni-mp-lark": "3.0.0-4070620250821001", 52 | "@dcloudio/uni-mp-qq": "3.0.0-4070620250821001", 53 | "@dcloudio/uni-mp-toutiao": "3.0.0-4070620250821001", 54 | "@dcloudio/uni-mp-weixin": "3.0.0-4070620250821001", 55 | "@dcloudio/uni-mp-xhs": "3.0.0-4070620250821001", 56 | "@dcloudio/uni-quickapp-webview": "3.0.0-4070620250821001", 57 | "@dcloudio/uni-ui": "^1.4.26", 58 | "vue": "^3.5.21", 59 | "vue-i18n": "^9.2.2" 60 | }, 61 | "devDependencies": { 62 | "@climblee/uv-ui": "^1.1.20", 63 | "@dcloudio/types": "^3.4.21", 64 | "@dcloudio/uni-automator": "3.0.0-4070620250821001", 65 | "@dcloudio/uni-cli-shared": "3.0.0-4070620250821001", 66 | "@dcloudio/uni-stacktracey": "3.0.0-4070620250821001", 67 | "@dcloudio/vite-plugin-uni": "3.0.0-4070620250821001", 68 | "@uni-helper/vite-plugin-uni-components": "workspace:*", 69 | "@vue/runtime-core": "^3.4.21", 70 | "@vue/tsconfig": "^0.1.3", 71 | "ano-ui": "0.6.5", 72 | "cross-env": "^7.0.3", 73 | "sass": "^1.61.0", 74 | "typescript": "^5.0.3", 75 | "uview-pro": "^0.0.23", 76 | "vite": "5.2.8", 77 | "vite-plugin-inspect": "^0.7.19", 78 | "vue-tsc": "^1.2.0", 79 | "wot-design-uni": "1.1.3" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/core/test/__snapshots__/dts.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`dts > components only 1`] = ` 4 | "/* eslint-disable */ 5 | /* prettier-ignore */ 6 | // @ts-nocheck 7 | // Generated by vite-plugin-uni-components 8 | // Read more: https://github.com/vuejs/core/pull/3399 9 | export {} 10 | 11 | declare module 'vue' { 12 | export interface GlobalComponents { 13 | TestComp: typeof import('test/component/TestComp')['default'] 14 | } 15 | } 16 | " 17 | `; 18 | 19 | exports[`dts > directive only 1`] = ` 20 | "/* eslint-disable */ 21 | /* prettier-ignore */ 22 | // @ts-nocheck 23 | // Generated by vite-plugin-uni-components 24 | // Read more: https://github.com/vuejs/core/pull/3399 25 | export {} 26 | 27 | declare module 'vue' { 28 | export interface ComponentCustomProperties { 29 | vLoading: typeof import('test/directive/Loading')['default'] 30 | } 31 | } 32 | " 33 | `; 34 | 35 | exports[`dts > getDeclaration 1`] = ` 36 | "/* eslint-disable */ 37 | /* prettier-ignore */ 38 | // @ts-nocheck 39 | // Generated by vite-plugin-uni-components 40 | // Read more: https://github.com/vuejs/core/pull/3399 41 | export {} 42 | 43 | declare module 'vue' { 44 | export interface GlobalComponents { 45 | TestComp: typeof import('test/component/TestComp')['default'] 46 | } 47 | export interface ComponentCustomProperties { 48 | vLoading: typeof import('test/directive/Loading')['default'] 49 | } 50 | } 51 | " 52 | `; 53 | 54 | exports[`dts > parseDeclaration - has icon component like 1`] = ` 55 | { 56 | "component": { 57 | "ComponentA": "typeof import('./src/components/ComponentA.vue')['default']", 58 | "IMdi:diceD12": "typeof import('~icons/mdi/dice-d12')['default']", 59 | "IMdiLightAlarm": "typeof import('~icons/mdi-light/alarm')['default']", 60 | }, 61 | "directive": {}, 62 | } 63 | `; 64 | 65 | exports[`dts > parseDeclaration - with directives 1`] = ` 66 | { 67 | "component": { 68 | "ComponentA": "typeof import('./src/components/ComponentA.vue')['default']", 69 | "IMdi:diceD12": "typeof import('~icons/mdi/dice-d12')['default']", 70 | "IMdiLightAlarm": "typeof import('~icons/mdi-light/alarm')['default']", 71 | }, 72 | "directive": { 73 | "vDirective": "typeof import('foo')", 74 | "vLoading": "typeof import('test/directive/Loading')['default']", 75 | "vSome": "typeof import('test/directive/Some')['default']", 76 | }, 77 | } 78 | `; 79 | 80 | exports[`dts > parseDeclaration 1`] = ` 81 | { 82 | "component": { 83 | "ComponentA": "typeof import('./src/components/ComponentA.vue')['default']", 84 | "ComponentB": "typeof import('./src/components/ComponentB.vue')['default']", 85 | "ComponentC": "typeof import('./src/components/component-c.vue')['default']", 86 | }, 87 | "directive": {}, 88 | } 89 | `; 90 | 91 | exports[`dts > vue 2.7 components only 1`] = ` 92 | "/* eslint-disable */ 93 | /* prettier-ignore */ 94 | // @ts-nocheck 95 | // Generated by vite-plugin-uni-components 96 | // Read more: https://github.com/vuejs/core/pull/3399 97 | export {} 98 | 99 | declare module 'vue' { 100 | export interface GlobalComponents { 101 | TestComp: typeof import('test/component/TestComp')['default'] 102 | } 103 | } 104 | " 105 | `; 106 | 107 | exports[`dts > writeDeclaration - keep unused 1`] = ` 108 | "/* eslint-disable */ 109 | /* prettier-ignore */ 110 | // @ts-nocheck 111 | // Generated by vite-plugin-uni-components 112 | // Read more: https://github.com/vuejs/core/pull/3399 113 | export {} 114 | 115 | declare module 'vue' { 116 | export interface GlobalComponents { 117 | SomeComp: typeof import('test/component/SomeComp')['default'] 118 | TestComp: typeof import('test/component/TestComp')['default'] 119 | } 120 | export interface ComponentCustomProperties { 121 | vDirective: typeof import('foo') 122 | vLoading: typeof import('test/directive/Loading')['default'] 123 | vSome: typeof import('test/directive/Some')['default'] 124 | } 125 | } 126 | " 127 | `; 128 | 129 | exports[`dts > writeDeclaration 1`] = ` 130 | "/* eslint-disable */ 131 | /* prettier-ignore */ 132 | // @ts-nocheck 133 | // Generated by vite-plugin-uni-components 134 | // Read more: https://github.com/vuejs/core/pull/3399 135 | export {} 136 | 137 | declare module 'vue' { 138 | export interface GlobalComponents { 139 | TestComp: typeof import('test/component/TestComp')['default'] 140 | } 141 | export interface ComponentCustomProperties { 142 | vLoading: typeof import('test/directive/Loading')['default'] 143 | } 144 | } 145 | " 146 | `; 147 | -------------------------------------------------------------------------------- /packages/core/test/__snapshots__/transform.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`component and directive as same name > vue2 transform should work 1`] = ` 4 | { 5 | "code": "/* vite-plugin-uni-components disabled */import __unplugin_directives_0 from 'test/directive/Loading'; 6 | import __unplugin_components_0 from 'test/component/Loading'; 7 | 8 | var render = function () { 9 | this.$options.directives[\\"loading\\"] = __unplugin_directives_0; 10 | var _vm = this 11 | var _h = _vm.$createElement 12 | var _c = _vm._self._c || _h 13 | return _c(__unplugin_components_0, { 14 | directives: [ 15 | { name: \\"loading\\", rawName: \\"v-loading\\", value: 123, expression: \\"123\\" } 16 | ] 17 | }) 18 | } 19 | var staticRenderFns = [] 20 | render._withStripped = true 21 | export { render, staticRenderFns } 22 | ", 23 | } 24 | `; 25 | 26 | exports[`component and directive as same name > vue2.7 transform should work 1`] = ` 27 | { 28 | "code": "/* vite-plugin-uni-components disabled */import __unplugin_directives_0 from 'test/directive/Loading'; 29 | import __unplugin_components_0 from 'test/component/Div'; 30 | 31 | import { defineComponent as _defineComponent } from \\"vue\\"; 32 | const _sfc_main = /* @__PURE__ */ _defineComponent({ 33 | __name: \\"App\\", 34 | setup(__props) { 35 | return { __sfc: true }; 36 | } 37 | }); 38 | var _sfc_render = function render() { 39 | this.$options.directives[\\"loading\\"] = __unplugin_directives_0; 40 | var _vm = this, _c = _vm._self._c, _setup = _vm._self._setupProxy; 41 | return _c(__unplugin_components_0, { directives: [{ name: \\"loading\\", rawName: \\"v-loading\\", value: 123, expression: \\"123\\" }] }, [], 1); 42 | }; 43 | ", 44 | } 45 | `; 46 | 47 | exports[`component and directive as same name > vue3 transform should work 1`] = ` 48 | { 49 | "code": "/* vite-plugin-uni-components disabled */import __unplugin_directives_0 from 'test/directive/ElInfiniteScroll'; 50 | import __unplugin_components_0 from 'test/component/ElInfiniteScroll'; 51 | 52 | const render = (_ctx, _cache) => { 53 | const _component_el_infinite_scroll = __unplugin_components_0 54 | const _directive_el_infinite_scroll = __unplugin_directives_0 55 | 56 | return _withDirectives( 57 | (_openBlock(), 58 | _createBlock(_component_test_comp, null, null, 512 /* NEED_PATCH */)), 59 | [[_directive_loading, 123]] 60 | ) 61 | } 62 | ", 63 | } 64 | `; 65 | 66 | exports[`transform > vue2 transform should work 1`] = ` 67 | { 68 | "code": "/* vite-plugin-uni-components disabled */import __unplugin_directives_0 from 'test/directive/Loading'; 69 | import __unplugin_components_0 from 'test/component/TestComp'; 70 | 71 | var render = function () { 72 | this.$options.directives[\\"loading\\"] = __unplugin_directives_0; 73 | var _vm = this 74 | var _h = _vm.$createElement 75 | var _c = _vm._self._c || _h 76 | return _c(__unplugin_components_0, { 77 | directives: [ 78 | { name: \\"loading\\", rawName: \\"v-loading\\", value: 123, expression: \\"123\\" } 79 | ] 80 | }) 81 | } 82 | var staticRenderFns = [] 83 | render._withStripped = true 84 | export { render, staticRenderFns } 85 | ", 86 | } 87 | `; 88 | 89 | exports[`transform > vue2 transform with jsx should work 1`] = ` 90 | { 91 | "code": "/* vite-plugin-uni-components disabled */import __unplugin_components_0 from 'test/component/TestComp'; 92 | 93 | export default { 94 | render(){ 95 | return h(__unplugin_components_0, { 96 | directives: [ 97 | { name: \\"loading\\", rawName: \\"v-loading\\", value: 123, expression: \\"123\\" } 98 | ] 99 | }) 100 | } 101 | } 102 | ", 103 | } 104 | `; 105 | 106 | exports[`transform > vue3 transform should work 1`] = ` 107 | { 108 | "code": "/* vite-plugin-uni-components disabled */import __unplugin_directives_0 from 'test/directive/Loading'; 109 | import __unplugin_components_0 from 'test/component/TestComp'; 110 | 111 | const render = (_ctx, _cache) => { 112 | const _component_test_comp = __unplugin_components_0 113 | const _directive_loading = __unplugin_directives_0 114 | 115 | return _withDirectives( 116 | (_openBlock(), 117 | _createBlock(_component_test_comp, null, null, 512 /* NEED_PATCH */)), 118 | [[_directive_loading, 123]] 119 | ) 120 | } 121 | ", 122 | } 123 | `; 124 | -------------------------------------------------------------------------------- /packages/core/test/transform.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import type { ComponentResolver } from '../src' 3 | import { Context } from '../src/context' 4 | 5 | const resolver: ComponentResolver[] = [ 6 | { 7 | type: 'component', 8 | resolve: name => ({ from: `test/component/${name}` }), 9 | }, 10 | { 11 | type: 'directive', 12 | resolve: name => ({ from: `test/directive/${name}` }), 13 | }, 14 | ] 15 | 16 | describe('transform', () => { 17 | it('vue2 transform should work', async () => { 18 | const code = ` 19 | var render = function () { 20 | var _vm = this 21 | var _h = _vm.$createElement 22 | var _c = _vm._self._c || _h 23 | return _c("test-comp", { 24 | directives: [ 25 | { name: "loading", rawName: "v-loading", value: 123, expression: "123" } 26 | ] 27 | }) 28 | } 29 | var staticRenderFns = [] 30 | render._withStripped = true 31 | export { render, staticRenderFns } 32 | ` 33 | 34 | const ctx = new Context({ 35 | resolvers: [resolver], 36 | transformer: 'vue2', 37 | directives: true, 38 | }) 39 | ctx.sourcemap = false 40 | expect(await ctx.transform(code, '')).toMatchSnapshot() 41 | }) 42 | 43 | it('vue2 transform with jsx should work', async () => { 44 | const code = ` 45 | export default { 46 | render(){ 47 | return h("test-comp", { 48 | directives: [ 49 | { name: "loading", rawName: "v-loading", value: 123, expression: "123" } 50 | ] 51 | }) 52 | } 53 | } 54 | ` 55 | 56 | const ctx = new Context({ 57 | resolvers: [resolver], 58 | transformer: 'vue2', 59 | directives: true, 60 | }) 61 | ctx.sourcemap = false 62 | expect(await ctx.transform(code, '')).toMatchSnapshot() 63 | }) 64 | 65 | it('vue3 transform should work', async () => { 66 | const code = ` 67 | const render = (_ctx, _cache) => { 68 | const _component_test_comp = _resolveComponent("test-comp") 69 | const _directive_loading = _resolveDirective("loading") 70 | 71 | return _withDirectives( 72 | (_openBlock(), 73 | _createBlock(_component_test_comp, null, null, 512 /* NEED_PATCH */)), 74 | [[_directive_loading, 123]] 75 | ) 76 | } 77 | ` 78 | 79 | const ctx = new Context({ 80 | resolvers: [resolver], 81 | transformer: 'vue3', 82 | directives: true, 83 | }) 84 | ctx.sourcemap = false 85 | expect(await ctx.transform(code, '')).toMatchSnapshot() 86 | }) 87 | }) 88 | 89 | describe('component and directive as same name', () => { 90 | it('vue2 transform should work', async () => { 91 | const code = ` 92 | var render = function () { 93 | var _vm = this 94 | var _h = _vm.$createElement 95 | var _c = _vm._self._c || _h 96 | return _c("loading", { 97 | directives: [ 98 | { name: "loading", rawName: "v-loading", value: 123, expression: "123" } 99 | ] 100 | }) 101 | } 102 | var staticRenderFns = [] 103 | render._withStripped = true 104 | export { render, staticRenderFns } 105 | ` 106 | 107 | const ctx = new Context({ 108 | resolvers: [resolver], 109 | transformer: 'vue2', 110 | directives: true, 111 | }) 112 | ctx.sourcemap = false 113 | expect(await ctx.transform(code, '')).toMatchSnapshot() 114 | }) 115 | 116 | it('vue2.7 transform should work', async () => { 117 | const code = ` 118 | import { defineComponent as _defineComponent } from "vue"; 119 | const _sfc_main = /* @__PURE__ */ _defineComponent({ 120 | __name: "App", 121 | setup(__props) { 122 | return { __sfc: true }; 123 | } 124 | }); 125 | var _sfc_render = function render() { 126 | var _vm = this, _c = _vm._self._c, _setup = _vm._self._setupProxy; 127 | return _c("div", { directives: [{ name: "loading", rawName: "v-loading", value: 123, expression: "123" }] }, [], 1); 128 | }; 129 | ` 130 | 131 | const ctx = new Context({ 132 | resolvers: [resolver], 133 | transformer: 'vue2', 134 | directives: true, 135 | }) 136 | ctx.sourcemap = false 137 | expect(await ctx.transform(code, '')).toMatchSnapshot() 138 | }) 139 | 140 | it('vue3 transform should work', async () => { 141 | const code = ` 142 | const render = (_ctx, _cache) => { 143 | const _component_el_infinite_scroll = _resolveComponent("el-infinite-scroll") 144 | const _directive_el_infinite_scroll = _resolveDirective("el-infinite-scroll") 145 | 146 | return _withDirectives( 147 | (_openBlock(), 148 | _createBlock(_component_test_comp, null, null, 512 /* NEED_PATCH */)), 149 | [[_directive_loading, 123]] 150 | ) 151 | } 152 | ` 153 | 154 | const ctx = new Context({ 155 | resolvers: [resolver], 156 | transformer: 'vue3', 157 | directives: true, 158 | }) 159 | ctx.sourcemap = false 160 | expect(await ctx.transform(code, '')).toMatchSnapshot() 161 | }) 162 | }) 163 | -------------------------------------------------------------------------------- /packages/core/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { FilterPattern } from '@rollup/pluginutils' 2 | import type { Awaitable } from '@antfu/utils' 3 | 4 | export interface ImportInfoLegacy { 5 | /** 6 | * @deprecated renamed to `as` 7 | */ 8 | name?: string 9 | /** 10 | * @deprecated renamed to `name` 11 | */ 12 | importName?: string 13 | /** 14 | * @deprecated renamed to `from` 15 | */ 16 | path: string 17 | 18 | sideEffects?: SideEffectsInfo 19 | } 20 | 21 | export interface ImportInfo { 22 | as?: string 23 | name?: string 24 | from: string 25 | } 26 | 27 | export type SideEffectsInfo = (ImportInfo | string)[] | ImportInfo | string | undefined 28 | 29 | export interface ComponentInfo extends ImportInfo { 30 | sideEffects?: SideEffectsInfo 31 | } 32 | 33 | export type ComponentResolveResult = Awaitable 34 | 35 | export type ComponentResolverFunction = (name: string) => ComponentResolveResult 36 | export interface ComponentResolverObject { 37 | type: 'component' | 'directive' 38 | resolve: ComponentResolverFunction 39 | } 40 | 41 | export type ComponentResolver = ComponentResolverFunction | ComponentResolverObject 42 | 43 | export type Matcher = (id: string) => boolean | null | undefined 44 | 45 | export type Transformer = (code: string, id: string, path: string, query: Record) => Awaitable 46 | 47 | export type SupportedTransformer = 'vue3' | 'vue2' 48 | 49 | export interface TypeImport { 50 | from: string 51 | names: string[] 52 | } 53 | 54 | export interface Options { 55 | /** 56 | * RegExp or glob to match files to be transformed 57 | */ 58 | include?: FilterPattern 59 | 60 | /** 61 | * RegExp or glob to match files to NOT be transformed 62 | */ 63 | exclude?: FilterPattern 64 | 65 | /** 66 | * RegExp or string to match component names that will NOT be imported 67 | */ 68 | excludeNames?: FilterPattern 69 | 70 | /** 71 | * Relative paths to the directory to search for components. 72 | * @default 'src/components' 73 | */ 74 | dirs?: string | string[] 75 | 76 | /** 77 | * Valid file extensions for components. 78 | * @default ['vue'] 79 | */ 80 | extensions?: string | string[] 81 | 82 | /** 83 | * Glob patterns to match file names to be detected as components. 84 | * 85 | * When specified, the `dirs` and `extensions` options will be ignored. 86 | */ 87 | globs?: string | string[] 88 | 89 | /** 90 | * Search for subdirectories 91 | * @default true 92 | */ 93 | deep?: boolean 94 | 95 | /** 96 | * Allow subdirectories as namespace prefix for components 97 | * @default false 98 | */ 99 | directoryAsNamespace?: boolean 100 | 101 | /** 102 | * Collapse same prefixes (camel-sensitive) of folders and components 103 | * to prevent duplication inside namespaced component name. 104 | * 105 | * Works when `directoryAsNamespace: true` 106 | * @default false 107 | */ 108 | collapseSamePrefixes?: boolean 109 | 110 | /** 111 | * Subdirectory paths for ignoring namespace prefixes 112 | * 113 | * Works when `directoryAsNamespace: true` 114 | * @default "[]" 115 | */ 116 | globalNamespaces?: string[] 117 | 118 | /** 119 | * Pass a custom function to resolve the component importing path from the component name. 120 | * 121 | * The component names are always in PascalCase 122 | */ 123 | resolvers?: (ComponentResolver | ComponentResolver[])[] 124 | 125 | /** 126 | * Apply custom transform over the path for importing 127 | */ 128 | importPathTransform?: (path: string) => string | undefined 129 | 130 | /** 131 | * Transformer to apply 132 | * 133 | * @default 'vue3' 134 | */ 135 | transformer?: SupportedTransformer 136 | 137 | /** 138 | * Generate TypeScript declaration for global components 139 | * 140 | * Accept boolean or a path related to project root 141 | * 142 | * @see https://github.com/vuejs/core/pull/3399 143 | * @see https://github.com/johnsoncodehk/volar#using 144 | * @default true 145 | */ 146 | dts?: boolean | string 147 | 148 | /** 149 | * Do not emit warning on component overriding 150 | * 151 | * @default false 152 | */ 153 | allowOverrides?: boolean 154 | 155 | /** 156 | * auto import for directives. 157 | * 158 | * default: `true` for Vue 3, `false` for Vue 2 159 | * 160 | * Babel is needed to do the transformation for Vue 2, it's disabled by default for performance concerns. 161 | * To install Babel, run: `npm install -D @babel/parser` 162 | * @default undefined 163 | */ 164 | directives?: boolean 165 | 166 | /** 167 | * Only provide types of components in library (registered globally) 168 | **/ 169 | types?: TypeImport[] 170 | 171 | /** 172 | * Vue version of project. It will detect automatically if not specified. 173 | */ 174 | version?: 2 | 2.7 | 3 175 | } 176 | 177 | export type ResolvedOptions = Omit< 178 | Required, 179 | 'resolvers' | 'extensions' | 'dirs' | 'globalComponentsDeclaration' 180 | > & { 181 | resolvers: ComponentResolverObject[] 182 | extensions: string[] 183 | dirs: string[] 184 | resolvedDirs: string[] 185 | globs: string[] 186 | dts: string | false 187 | root: string 188 | } 189 | -------------------------------------------------------------------------------- /packages/core/src/declaration.ts: -------------------------------------------------------------------------------- 1 | import { dirname, isAbsolute, relative } from 'node:path' 2 | import { existsSync } from 'node:fs' 3 | import { mkdir, readFile, writeFile as writeFile_ } from 'node:fs/promises' 4 | import { notNullish, slash } from '@antfu/utils' 5 | import type { ComponentInfo, Options } from './types' 6 | import type { Context } from './context' 7 | import { getTransformedPath } from './utils' 8 | 9 | const multilineCommentsRE = /\/\*.*?\*\//gms 10 | const singlelineCommentsRE = /\/\/.*$/gm 11 | 12 | function extractImports(code: string) { 13 | return Object.fromEntries(Array.from(code.matchAll(/['"]?([^\s'"]+)['"]?\s*:\s*(.+?)[,;\n]/g)).map(i => [i[1], i[2]])) 14 | } 15 | 16 | export function parseDeclaration(code: string): DeclarationImports | undefined { 17 | if (!code) 18 | return 19 | 20 | code = code 21 | .replace(multilineCommentsRE, '') 22 | .replace(singlelineCommentsRE, '') 23 | 24 | const imports: DeclarationImports = { 25 | component: {}, 26 | directive: {}, 27 | } 28 | const componentDeclaration = /export\s+interface\s+GlobalComponents\s*{(.*?)}/s.exec(code)?.[0] 29 | if (componentDeclaration) 30 | imports.component = extractImports(componentDeclaration) 31 | 32 | const directiveDeclaration = /export\s+interface\s+ComponentCustomProperties\s*{(.*?)}/s.exec(code)?.[0] 33 | if (directiveDeclaration) 34 | imports.directive = extractImports(directiveDeclaration) 35 | 36 | return imports 37 | } 38 | 39 | /** 40 | * Converts `ComponentInfo` to an array 41 | * 42 | * `[name, "typeof import(path)[importName]"]` 43 | */ 44 | function stringifyComponentInfo(filepath: string, { from: path, as: name, name: importName }: ComponentInfo, importPathTransform?: Options['importPathTransform']): [string, string] | undefined { 45 | if (!name) 46 | return undefined 47 | path = getTransformedPath(path, importPathTransform) 48 | const related = isAbsolute(path) 49 | ? `./${relative(dirname(filepath), path)}` 50 | : path 51 | // const entry = `typeof import('${slash(related)}')['${importName || 'default'}']` 52 | const entry = `typeof import('${slash(related)}')['default']` 53 | return [name, entry] 54 | } 55 | 56 | /** 57 | * Converts array of `ComponentInfo` to an import map 58 | * 59 | * `{ name: "typeof import(path)[importName]", ... }` 60 | */ 61 | export function stringifyComponentsInfo(filepath: string, components: ComponentInfo[], importPathTransform?: Options['importPathTransform']): Record { 62 | return Object.fromEntries( 63 | components.map(info => stringifyComponentInfo(filepath, info, importPathTransform)) 64 | .filter(notNullish), 65 | ) 66 | } 67 | 68 | export interface DeclarationImports { 69 | component: Record 70 | directive: Record 71 | } 72 | 73 | export function getDeclarationImports(ctx: Context, filepath: string): DeclarationImports | undefined { 74 | const component = stringifyComponentsInfo(filepath, [ 75 | ...Object.values({ 76 | ...ctx.componentNameMap, 77 | ...ctx.componentCustomMap, 78 | }), 79 | ], ctx.options.importPathTransform) 80 | 81 | const directive = stringifyComponentsInfo( 82 | filepath, 83 | Object.values(ctx.directiveCustomMap), 84 | ctx.options.importPathTransform, 85 | ) 86 | 87 | if ( 88 | (Object.keys(component).length + Object.keys(directive).length) === 0 89 | ) 90 | return 91 | 92 | return { component, directive } 93 | } 94 | 95 | export function stringifyDeclarationImports(imports: Record) { 96 | return Object.entries(imports) 97 | .sort(([a], [b]) => a.localeCompare(b)) 98 | .map(([name, v]) => { 99 | if (!/^\w+$/.test(name)) 100 | name = `'${name}'` 101 | return `${name}: ${v}` 102 | }) 103 | } 104 | 105 | export function getDeclaration(ctx: Context, filepath: string, originalImports?: DeclarationImports) { 106 | const imports = getDeclarationImports(ctx, filepath) 107 | if (!imports) 108 | return 109 | 110 | const declarations = { 111 | component: stringifyDeclarationImports({ ...originalImports?.component, ...imports.component }), 112 | directive: stringifyDeclarationImports({ ...originalImports?.directive, ...imports.directive }), 113 | } 114 | 115 | const head = ctx.options.version >= 2.7 116 | ? `export {} 117 | 118 | declare module 'vue' {` 119 | : `import '@vue/runtime-core' 120 | 121 | export {} 122 | 123 | declare module '@vue/runtime-core' {` 124 | 125 | let code = `/* eslint-disable */ 126 | /* prettier-ignore */ 127 | // @ts-nocheck 128 | // Generated by vite-plugin-uni-components 129 | // Read more: https://github.com/vuejs/core/pull/3399 130 | ${head}` 131 | 132 | if (Object.keys(declarations.component).length > 0) { 133 | code += ` 134 | export interface GlobalComponents { 135 | ${declarations.component.join('\n ')} 136 | }` 137 | } 138 | if (Object.keys(declarations.directive).length > 0) { 139 | code += ` 140 | export interface ComponentCustomProperties { 141 | ${declarations.directive.join('\n ')} 142 | }` 143 | } 144 | code += '\n}\n' 145 | return code 146 | } 147 | 148 | async function writeFile(filePath: string, content: string) { 149 | await mkdir(dirname(filePath), { recursive: true }) 150 | return await writeFile_(filePath, content, 'utf-8') 151 | } 152 | 153 | export async function writeDeclaration(ctx: Context, filepath: string, removeUnused = false) { 154 | const originalContent = existsSync(filepath) ? await readFile(filepath, 'utf-8') : '' 155 | const originalImports = removeUnused ? undefined : parseDeclaration(originalContent) 156 | 157 | const code = getDeclaration(ctx, filepath, originalImports) 158 | if (!code) 159 | return 160 | 161 | if (code !== originalContent) 162 | await writeFile(filepath, code) 163 | } 164 | -------------------------------------------------------------------------------- /packages/core/test/dts.test.ts: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from 'node:fs/promises' 2 | import path from 'node:path' 3 | import { describe, expect, it } from 'vitest' 4 | import type { ComponentResolver } from '../src' 5 | import { Context } from '../src/context' 6 | import { getDeclaration, parseDeclaration } from '../src/declaration' 7 | 8 | const resolver: ComponentResolver[] = [ 9 | { 10 | type: 'component', 11 | resolve: name => ({ from: `test/component/${name}` }), 12 | }, 13 | { 14 | type: 'directive', 15 | resolve: name => ({ from: `test/directive/${name}` }), 16 | }, 17 | ] 18 | 19 | describe('dts', () => { 20 | it('getDeclaration', async () => { 21 | const ctx = new Context({ 22 | resolvers: resolver, 23 | directives: true, 24 | }) 25 | const code = ` 26 | const _component_test_comp = _resolveComponent("test-comp") 27 | const _directive_loading = _resolveDirective("loading")` 28 | await ctx.transform(code, '') 29 | 30 | const declarations = getDeclaration(ctx, 'test.d.ts') 31 | expect(declarations).toMatchSnapshot() 32 | }) 33 | 34 | it('writeDeclaration', async () => { 35 | const filepath = path.resolve(__dirname, 'tmp/dts-test.d.ts') 36 | const ctx = new Context({ 37 | resolvers: resolver, 38 | directives: true, 39 | dts: filepath, 40 | }) 41 | const code = ` 42 | const _component_test_comp = _resolveComponent("test-comp") 43 | const _directive_loading = _resolveDirective("loading")` 44 | await ctx.transform(code, '') 45 | await ctx._generateDeclaration() 46 | 47 | expect(await readFile(filepath, 'utf-8')).matchSnapshot() 48 | }) 49 | 50 | it('writeDeclaration - keep unused', async () => { 51 | const filepath = path.resolve(__dirname, 'tmp/dts-keep-unused.d.ts') 52 | await writeFile( 53 | filepath, 54 | ` 55 | declare module 'vue' { 56 | export interface GlobalComponents { 57 | SomeComp: typeof import('test/component/SomeComp')['default'] 58 | TestComp: typeof import('test/component/OldComp')['default'] 59 | } 60 | export interface ComponentCustomProperties{ 61 | // with comment: b 62 | // a: 63 | vSome: typeof import('test/directive/Some')['default'];vDirective:typeof import('foo') 64 | } 65 | }`, 66 | 'utf-8', 67 | ) 68 | const ctx = new Context({ 69 | resolvers: resolver, 70 | directives: true, 71 | dts: filepath, 72 | }) 73 | const code = ` 74 | const _component_test_comp = _resolveComponent("test-comp") 75 | const _directive_loading = _resolveDirective("loading")` 76 | await ctx.transform(code, '') 77 | await ctx._generateDeclaration(false) 78 | 79 | const contents = await readFile(filepath, 'utf-8') 80 | expect(contents).matchSnapshot() 81 | expect(contents).not.toContain('OldComp') 82 | expect(contents).not.toContain('comment') 83 | expect(contents).toContain('vSome') 84 | }) 85 | 86 | it('components only', async () => { 87 | const ctx = new Context({ 88 | resolvers: resolver, 89 | directives: true, 90 | }) 91 | const code = 'const _component_test_comp = _resolveComponent("test-comp")' 92 | await ctx.transform(code, '') 93 | 94 | const declarations = getDeclaration(ctx, 'test.d.ts') 95 | expect(declarations).toMatchSnapshot() 96 | }) 97 | 98 | it('vue 2.7 components only', async () => { 99 | const ctx = new Context({ 100 | resolvers: resolver, 101 | directives: true, 102 | version: 2.7, 103 | }) 104 | const code = 'const _component_test_comp = _c("test-comp")' 105 | await ctx.transform(code, '') 106 | 107 | const declarations = getDeclaration(ctx, 'test.d.ts') 108 | expect(declarations).toMatchSnapshot() 109 | }) 110 | 111 | it('directive only', async () => { 112 | const ctx = new Context({ 113 | resolvers: resolver, 114 | directives: true, 115 | types: [], 116 | }) 117 | const code = 'const _directive_loading = _resolveDirective("loading")' 118 | await ctx.transform(code, '') 119 | 120 | const declarations = getDeclaration(ctx, 'test.d.ts') 121 | expect(declarations).toMatchSnapshot() 122 | }) 123 | 124 | it('parseDeclaration', async () => { 125 | const code = ` 126 | /* eslint-disable */ 127 | // generated by unplugin-vue-components 128 | // We suggest you to commit this file into source control 129 | // Read more: https://github.com/vuejs/core/pull/3399 130 | export {} 131 | 132 | /* prettier-ignore */ 133 | declare module 'vue' { 134 | export interface GlobalComponents { 135 | ComponentA: typeof import('./src/components/ComponentA.vue')['default'] 136 | ComponentB: typeof import('./src/components/ComponentB.vue')['default'] 137 | ComponentC: typeof import('./src/components/component-c.vue')['default'] 138 | } 139 | }` 140 | 141 | const imports = parseDeclaration(code) 142 | expect(imports).matchSnapshot() 143 | }) 144 | 145 | it('parseDeclaration - has icon component like ', async () => { 146 | const code = ` 147 | /* eslint-disable */ 148 | // generated by unplugin-vue-components 149 | // We suggest you to commit this file into source control 150 | // Read more: https://github.com/vuejs/core/pull/3399 151 | export {} 152 | 153 | /* prettier-ignore */ 154 | declare module 'vue' { 155 | export interface GlobalComponents { 156 | ComponentA: typeof import('./src/components/ComponentA.vue')['default'] 157 | 'IMdi:diceD12': typeof import('~icons/mdi/dice-d12')['default'] 158 | IMdiLightAlarm: typeof import('~icons/mdi-light/alarm')['default'] 159 | } 160 | }` 161 | 162 | const imports = parseDeclaration(code) 163 | expect(imports).matchSnapshot() 164 | }) 165 | 166 | it('parseDeclaration - with directives', async () => { 167 | const code = ` 168 | /* eslint-disable */ 169 | // generated by unplugin-vue-components 170 | // We suggest you to commit this file into source control 171 | // Read more: https://github.com/vuejs/core/pull/3399 172 | export {} 173 | 174 | /* prettier-ignore */ 175 | declare module 'vue' { 176 | export interface GlobalComponents { 177 | ComponentA: typeof import('./src/components/ComponentA.vue')['default'] 178 | 'IMdi:diceD12': typeof import('~icons/mdi/dice-d12')['default'] 179 | IMdiLightAlarm: typeof import('~icons/mdi-light/alarm')['default'] 180 | } 181 | 182 | export interface ComponentCustomProperties { 183 | vDirective: typeof import('foo') 184 | vLoading: typeof import('test/directive/Loading')['default'] 185 | vSome: typeof import('test/directive/Some')['default'] 186 | } 187 | }` 188 | 189 | const imports = parseDeclaration(code) 190 | expect(imports).matchSnapshot() 191 | }) 192 | }) 193 | -------------------------------------------------------------------------------- /packages/core/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'node:path' 2 | import minimatch from 'minimatch' 3 | import resolve from 'resolve' 4 | import { slash, toArray } from '@antfu/utils' 5 | import { 6 | getPackageInfo, 7 | isPackageExists, 8 | } from 'local-pkg' 9 | import type { FilterPattern } from '@rollup/pluginutils' 10 | import type { ComponentInfo, ImportInfo, ImportInfoLegacy, Options, ResolvedOptions } from './types' 11 | import type { Context } from './context' 12 | import { DISABLE_COMMENT } from './constants' 13 | 14 | export const isSSR = Boolean(process.env.SSR || process.env.SSG || process.env.VITE_SSR || process.env.VITE_SSG) 15 | 16 | export interface ResolveComponent { 17 | filename: string 18 | namespace?: string 19 | } 20 | 21 | export function pascalCase(str: string) { 22 | return capitalize(camelCase(str)) 23 | } 24 | 25 | export function camelCase(str: string) { 26 | return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : '')) 27 | } 28 | 29 | export function kebabCase(key: string) { 30 | const result = key.replace(/([A-Z])/g, ' $1').trim() 31 | return result.split(' ').join('-').toLowerCase() 32 | } 33 | 34 | export function capitalize(str: string) { 35 | return str.charAt(0).toUpperCase() + str.slice(1) 36 | } 37 | 38 | export function parseId(id: string) { 39 | const index = id.indexOf('?') 40 | if (index < 0) { 41 | return { path: id, query: {} } 42 | } 43 | else { 44 | const query = Object.fromEntries(new URLSearchParams(id.slice(index)) as any) 45 | return { 46 | path: id.slice(0, index), 47 | query, 48 | } 49 | } 50 | } 51 | 52 | export function isEmpty(value: any) { 53 | if (!value || value === null || value === undefined || (Array.isArray(value) && Object.keys(value).length <= 0)) 54 | return true 55 | 56 | else 57 | return false 58 | } 59 | 60 | export function matchGlobs(filepath: string, globs: string[]) { 61 | for (const glob of globs) { 62 | if (minimatch(slash(filepath), glob)) 63 | return true 64 | } 65 | return false 66 | } 67 | 68 | export function getTransformedPath(path: string, importPathTransform?: Options['importPathTransform']): string { 69 | if (importPathTransform) { 70 | const result = importPathTransform(path) 71 | if (result != null) 72 | path = result 73 | } 74 | 75 | return path 76 | } 77 | 78 | export function stringifyImport(info: ImportInfo | string) { 79 | if (typeof info === 'string') 80 | return `import '${info}'` 81 | if (!info.as) 82 | return `import '${info.from}'` 83 | // else if (info.name) 84 | // return `import { ${info.name} as ${info.as} } from '${info.from}'` 85 | else 86 | return `import ${info.as} from '${info.from}'` 87 | } 88 | 89 | export function normalizeComponetInfo(info: ImportInfo | ImportInfoLegacy | ComponentInfo): ComponentInfo { 90 | if ('path' in info) { 91 | return { 92 | from: info.path, 93 | as: info.name, 94 | name: info.importName, 95 | sideEffects: info.sideEffects, 96 | } 97 | } 98 | return info 99 | } 100 | 101 | export function stringifyComponentImport({ as: name, from: path, name: importName, sideEffects }: ComponentInfo, ctx: Context) { 102 | path = getTransformedPath(path, ctx.options.importPathTransform) 103 | 104 | const imports = [ 105 | stringifyImport({ as: name, from: path, name: importName }), 106 | ] 107 | 108 | if (sideEffects) 109 | toArray(sideEffects).forEach(i => imports.push(stringifyImport(i))) 110 | 111 | return imports.join(';') 112 | } 113 | 114 | export function getNameFromFilePath(filePath: string, options: ResolvedOptions): string { 115 | const { resolvedDirs, directoryAsNamespace, globalNamespaces, collapseSamePrefixes, root } = options 116 | 117 | const parsedFilePath = parse(slash(filePath)) 118 | 119 | let strippedPath = '' 120 | 121 | // remove include directories from filepath 122 | for (const dir of resolvedDirs) { 123 | if (parsedFilePath.dir.startsWith(dir)) { 124 | strippedPath = parsedFilePath.dir.slice(dir.length) 125 | break 126 | } 127 | } 128 | 129 | let folders = strippedPath.slice(1).split('/').filter(Boolean) 130 | let filename = parsedFilePath.name 131 | 132 | // set parent directory as filename if it is index 133 | if (filename === 'index' && !directoryAsNamespace) { 134 | // when use `globs` option, `resolvedDirs` will always empty, and `folders` will also empty 135 | if (isEmpty(folders)) 136 | folders = parsedFilePath.dir.slice(root.length + 1).split('/').filter(Boolean) 137 | 138 | filename = `${folders.slice(-1)[0]}` 139 | return filename 140 | } 141 | 142 | if (directoryAsNamespace) { 143 | // remove namesspaces from folder names 144 | if (globalNamespaces.some((name: string) => folders.includes(name))) 145 | folders = folders.filter(f => !globalNamespaces.includes(f)) 146 | 147 | folders = folders.map(f => f.replace(/[^a-zA-Z0-9\-]/g, '')) 148 | 149 | if (filename.toLowerCase() === 'index') 150 | filename = '' 151 | 152 | if (!isEmpty(folders)) { 153 | // add folders to filename 154 | let namespaced = [...folders, filename] 155 | 156 | if (collapseSamePrefixes) { 157 | const collapsed: string[] = [] 158 | 159 | for (const fileOrFolderName of namespaced) { 160 | let cumulativePrefix = '' 161 | let didCollapse = false 162 | 163 | for (const parentFolder of [...collapsed].reverse()) { 164 | cumulativePrefix = `${capitalize(parentFolder)}${cumulativePrefix}` 165 | 166 | if (pascalCase(fileOrFolderName).startsWith(pascalCase(cumulativePrefix))) { 167 | const collapseSamePrefix = fileOrFolderName.slice(cumulativePrefix.length) 168 | 169 | collapsed.push(collapseSamePrefix) 170 | 171 | didCollapse = true 172 | break 173 | } 174 | } 175 | 176 | if (!didCollapse) 177 | collapsed.push(fileOrFolderName) 178 | } 179 | 180 | namespaced = collapsed 181 | } 182 | 183 | filename = namespaced.filter(Boolean).join('-') 184 | } 185 | 186 | return filename 187 | } 188 | 189 | return filename 190 | } 191 | 192 | export function resolveAlias(filepath: string, alias: any) { 193 | const result = filepath 194 | if (Array.isArray(alias)) { 195 | for (const { find, replacement } of alias) 196 | result.replace(find, replacement) 197 | } 198 | return result 199 | } 200 | 201 | export async function getPkgVersion(pkgName: string, defaultVersion: string): Promise { 202 | try { 203 | const isExist = isPackageExists(pkgName) 204 | if (isExist) { 205 | const pkg = await getPackageInfo(pkgName) 206 | return pkg?.version ?? defaultVersion 207 | } 208 | else { 209 | return defaultVersion 210 | } 211 | } 212 | catch (err) { 213 | console.error(err) 214 | return defaultVersion 215 | } 216 | } 217 | 218 | export function shouldTransform(code: string) { 219 | if (code.includes(DISABLE_COMMENT)) 220 | return false 221 | return true 222 | } 223 | 224 | export function resolveImportPath(importName: string): string | undefined { 225 | return resolve.sync(importName, { 226 | preserveSymlinks: false, 227 | }) 228 | } 229 | 230 | export function isExclude(name: string, exclude?: FilterPattern): boolean { 231 | if (!exclude) 232 | return false 233 | 234 | if (typeof exclude === 'string') 235 | return name === exclude 236 | 237 | if (exclude instanceof RegExp) 238 | return !!name.match(exclude) 239 | 240 | if (Array.isArray(exclude)) { 241 | for (const item of exclude) { 242 | if (name === item || name.match(item)) 243 | return true 244 | } 245 | } 246 | return false 247 | } 248 | -------------------------------------------------------------------------------- /packages/core/src/context.ts: -------------------------------------------------------------------------------- 1 | import { relative } from 'node:path' 2 | import type fs from 'node:fs' 3 | import Debug from 'debug' 4 | import type { UpdatePayload, ViteDevServer } from 'vite' 5 | import { slash, throttle, toArray } from '@antfu/utils' 6 | import type { ComponentInfo, Options, ResolvedOptions, Transformer } from './types' 7 | import { DIRECTIVE_IMPORT_PREFIX } from './constants' 8 | import { getNameFromFilePath, isExclude, matchGlobs, normalizeComponetInfo, parseId, pascalCase, resolveAlias } from './utils' 9 | import { resolveOptions } from './options' 10 | import { searchComponents } from './fs/glob' 11 | import { writeDeclaration } from './declaration' 12 | import transformer from './transformer' 13 | 14 | const debug = { 15 | components: Debug('vite-plugin-uni-components:context:components'), 16 | search: Debug('vite-plugin-uni-components:context:search'), 17 | hmr: Debug('vite-plugin-uni-components:context:hmr'), 18 | decleration: Debug('vite-plugin-uni-components:decleration'), 19 | env: Debug('vite-plugin-uni-components:env'), 20 | } 21 | 22 | export class Context { 23 | options: ResolvedOptions 24 | transformer: Transformer = undefined! 25 | 26 | private _componentPaths = new Set() 27 | private _componentNameMap: Record = {} 28 | private _componentUsageMap: Record> = {} 29 | private _componentCustomMap: Record = {} 30 | private _directiveCustomMap: Record = {} 31 | private _server: ViteDevServer | undefined 32 | 33 | root = process.cwd() 34 | sourcemap: string | boolean = true 35 | alias: Record = {} 36 | 37 | constructor( 38 | private rawOptions: Options, 39 | ) { 40 | this.options = resolveOptions(rawOptions, this.root) 41 | this.generateDeclaration = throttle(500, this._generateDeclaration.bind(this), { noLeading: false }) 42 | this.setTransformer(this.options.transformer) 43 | } 44 | 45 | setRoot(root: string) { 46 | if (this.root === root) 47 | return 48 | debug.env('root', root) 49 | this.root = root 50 | this.options = resolveOptions(this.rawOptions, this.root) 51 | } 52 | 53 | setTransformer(name: Options['transformer']) { 54 | debug.env('transformer', name) 55 | this.transformer = transformer(this, name || 'vue3') 56 | } 57 | 58 | transform(code: string, id: string) { 59 | const { path, query } = parseId(id) 60 | return this.transformer(code, id, path, query) 61 | } 62 | 63 | setupViteServer(server: ViteDevServer) { 64 | if (this._server === server) 65 | return 66 | 67 | this._server = server 68 | this.setupWatcher(server.watcher) 69 | } 70 | 71 | setupWatcher(watcher: fs.FSWatcher) { 72 | const { globs } = this.options 73 | 74 | watcher 75 | .on('unlink', (path) => { 76 | if (!matchGlobs(path, globs)) 77 | return 78 | 79 | path = slash(path) 80 | this.removeComponents(path) 81 | this.onUpdate(path) 82 | }) 83 | watcher 84 | .on('add', (path) => { 85 | if (!matchGlobs(path, globs)) 86 | return 87 | 88 | path = slash(path) 89 | this.addComponents(path) 90 | this.onUpdate(path) 91 | }) 92 | } 93 | 94 | /** 95 | * Record the usage of components 96 | * @param path 97 | * @param paths paths of used components 98 | */ 99 | updateUsageMap(path: string, paths: string[]) { 100 | if (!this._componentUsageMap[path]) 101 | this._componentUsageMap[path] = new Set() 102 | 103 | paths.forEach((p) => { 104 | this._componentUsageMap[path].add(p) 105 | }) 106 | } 107 | 108 | addComponents(paths: string | string[]) { 109 | debug.components('add', paths) 110 | 111 | const size = this._componentPaths.size 112 | toArray(paths).forEach(p => this._componentPaths.add(p)) 113 | if (this._componentPaths.size !== size) { 114 | this.updateComponentNameMap() 115 | return true 116 | } 117 | return false 118 | } 119 | 120 | addCustomComponents(info: ComponentInfo) { 121 | if (info.as) 122 | this._componentCustomMap[info.as] = info 123 | } 124 | 125 | addCustomDirectives(info: ComponentInfo) { 126 | if (info.as) 127 | this._directiveCustomMap[info.as] = info 128 | } 129 | 130 | removeComponents(paths: string | string[]) { 131 | debug.components('remove', paths) 132 | 133 | const size = this._componentPaths.size 134 | toArray(paths).forEach(p => this._componentPaths.delete(p)) 135 | if (this._componentPaths.size !== size) { 136 | this.updateComponentNameMap() 137 | return true 138 | } 139 | return false 140 | } 141 | 142 | onUpdate(path: string) { 143 | this.generateDeclaration() 144 | 145 | if (!this._server) 146 | return 147 | 148 | const payload: UpdatePayload = { 149 | type: 'update', 150 | updates: [], 151 | } 152 | const timestamp = +new Date() 153 | const name = pascalCase(getNameFromFilePath(path, this.options)) 154 | 155 | Object.entries(this._componentUsageMap) 156 | .forEach(([key, values]) => { 157 | if (values.has(name)) { 158 | const r = `/${slash(relative(this.root, key))}` 159 | payload.updates.push({ 160 | acceptedPath: r, 161 | path: r, 162 | timestamp, 163 | type: 'js-update', 164 | }) 165 | } 166 | }) 167 | 168 | if (payload.updates.length) 169 | this._server.ws.send(payload) 170 | } 171 | 172 | private updateComponentNameMap() { 173 | this._componentNameMap = {} 174 | 175 | Array 176 | .from(this._componentPaths) 177 | .forEach((path) => { 178 | const name = pascalCase(getNameFromFilePath(path, this.options)) 179 | if (isExclude(name, this.options.excludeNames)) { 180 | debug.components('exclude', name) 181 | return 182 | } 183 | if (this._componentNameMap[name] && !this.options.allowOverrides) { 184 | console.warn(`[vite-plugin-uni-components] component "${name}"(${path}) has naming conflicts with other components, ignored.`) 185 | return 186 | } 187 | 188 | this._componentNameMap[name] = { 189 | as: name, 190 | from: path, 191 | } 192 | }) 193 | } 194 | 195 | async findComponent(name: string, type: 'component' | 'directive', excludePaths: string[] = []): Promise { 196 | // resolve from fs 197 | let info = this._componentNameMap[name] 198 | if (info && !excludePaths.includes(info.from) && !excludePaths.includes(info.from.slice(1))) 199 | return info 200 | 201 | // custom resolvers 202 | for (const resolver of this.options.resolvers) { 203 | if (resolver.type !== type) 204 | continue 205 | 206 | const result = await resolver.resolve(type === 'directive' ? name.slice(DIRECTIVE_IMPORT_PREFIX.length) : name) 207 | if (!result) 208 | continue 209 | 210 | if (typeof result === 'string') { 211 | info = { 212 | as: name, 213 | from: result, 214 | } 215 | } 216 | else { 217 | info = { 218 | as: name, 219 | ...normalizeComponetInfo(result), 220 | } 221 | } 222 | if (type === 'component') 223 | this.addCustomComponents(info) 224 | else if (type === 'directive') 225 | this.addCustomDirectives(info) 226 | return info 227 | } 228 | 229 | return undefined 230 | } 231 | 232 | normalizePath(path: string) { 233 | // @ts-expect-error backward compatibility 234 | return resolveAlias(path, this.viteConfig?.resolve?.alias || this.viteConfig?.alias || []) 235 | } 236 | 237 | relative(path: string) { 238 | if (path.startsWith('/') && !path.startsWith(this.root)) 239 | return slash(path.slice(1)) 240 | return slash(relative(this.root, path)) 241 | } 242 | 243 | _searched = false 244 | 245 | /** 246 | * This search for components in with the given options. 247 | * Will be called multiple times to ensure file loaded, 248 | * should normally run only once. 249 | */ 250 | searchGlob() { 251 | if (this._searched) 252 | return 253 | 254 | searchComponents(this) 255 | debug.search(this._componentNameMap) 256 | this._searched = true 257 | } 258 | 259 | _generateDeclaration(removeUnused = !this._server) { 260 | if (!this.options.dts) 261 | return 262 | 263 | debug.decleration('generating') 264 | return writeDeclaration(this, this.options.dts, removeUnused) 265 | } 266 | 267 | generateDeclaration 268 | 269 | get componentNameMap() { 270 | return this._componentNameMap 271 | } 272 | 273 | get componentCustomMap() { 274 | return this._componentCustomMap 275 | } 276 | 277 | get directiveCustomMap() { 278 | return this._directiveCustomMap 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /banner.svg: -------------------------------------------------------------------------------- 1 | 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 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 415 | vite-plugin-uni-components 416 | @uni-helper 417 | 418 | 419 | --------------------------------------------------------------------------------