├── .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 |
2 |
3 | This should be import async
4 |
5 |
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 |
6 | Component D:
7 |
8 |
--------------------------------------------------------------------------------
/playground/src/components/book/index.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 | Index Component: book/index.vue
9 |
10 |
--------------------------------------------------------------------------------
/playground/src/components/ComponentA.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 | Component A: {{ msg }}
9 |
10 |
--------------------------------------------------------------------------------
/playground/src/components/component-c.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 | Component C: {{ msg }}
9 |
10 |
--------------------------------------------------------------------------------
/playground/src/components/ComponentB.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 | Component B: {{ msg }}
9 |
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 |
13 |
14 |
{{ data.label }}
15 |
16 |
17 |
18 |
19 |
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 | [](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 |
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 |
14 |
15 |
Basic (4)
16 |
17 |
18 |
19 |
20 | Recursive Components
21 |
22 |
23 |
24 |
25 |
26 |
Ano UI (2)
27 |
Button
28 |
29 |
30 |
31 |
Uni UI (2)
32 |
38 | test
39 |
40 |
41 |
Wot Design Uni
42 |
43 | Button
44 |
45 |
46 |
47 |
Uv UI
48 |
49 | Button
50 |
51 |
52 |
53 |
uView Pro UI
54 |
55 | Button
56 |
57 |
58 |
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 |
419 |
--------------------------------------------------------------------------------