diff --git a/package.json b/package.json index 4c1992d6..be35cec6 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "local-pkg": "^0.4.0", "magic-string": "^0.25.7", "minimatch": "^3.0.4", + "pnpm": "^6.23.6", "resolve": "^1.20.0", "unplugin": "^0.2.21" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd93b2a7..cf1e68d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,6 +28,7 @@ importers: local-pkg: ^0.4.0 magic-string: ^0.25.7 minimatch: ^3.0.4 + pnpm: ^6.23.6 resolve: ^1.20.0 rollup: ^2.60.0 ts-jest: ^27.0.7 @@ -45,6 +46,7 @@ importers: local-pkg: 0.4.0 magic-string: 0.25.7 minimatch: 3.0.4 + pnpm: 6.23.6 resolve: 1.20.0 unplugin: 0.2.21_rollup@2.60.0+vite@2.6.14 devDependencies: @@ -10215,6 +10217,12 @@ packages: - typescript dev: true + /pnpm/6.23.6: + resolution: {integrity: sha512-H6PIzO4W2zg2Jqms13uS5U3fU8yBdWk8rXcpJLjs/in+nutgVA8GGlasEAI2eAiTpc9FYJUkCklbJ4a/M36N0w==} + engines: {node: '>=12.17'} + hasBin: true + dev: false + /portfinder/1.0.28: resolution: {integrity: sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==} engines: {node: '>= 0.12.0'} diff --git a/src/core/context.ts b/src/core/context.ts index 7bdadd91..79a25767 100644 --- a/src/core/context.ts +++ b/src/core/context.ts @@ -8,6 +8,7 @@ import { pascalCase, getNameFromFilePath, resolveAlias, matchGlobs, parseId } fr import { resolveOptions } from './options' import { searchComponents } from './fs/glob' import { generateDeclaration } from './declaration' +import { generateIdeHelper } from './ideHelper' import transformer from './transformer' const debug = { @@ -15,6 +16,7 @@ const debug = { search: Debug('unplugin-vue-components:context:search'), hmr: Debug('unplugin-vue-components:context:hmr'), decleration: Debug('unplugin-vue-components:decleration'), + ideHelper: Debug('unplugin-vue-components:ideHelper'), env: Debug('unplugin-vue-components:env'), } @@ -37,6 +39,7 @@ export class Context { ) { this.options = resolveOptions(rawOptions, this.root) this.generateDeclaration = throttle(500, false, this.generateDeclaration.bind(this)) + this.generateIdeHelper = throttle(500, false, this.generateIdeHelper.bind(this)) this.setTransformer(this.options.transformer) } @@ -130,6 +133,7 @@ export class Context { onUpdate(path: string) { this.generateDeclaration() + this.generateIdeHelper() if (!this._server) return @@ -251,6 +255,14 @@ export class Context { generateDeclaration(this, this.options.root, this.options.dts) } + generateIdeHelper() { + if (!this.options.generateIdeHelper) + return + + debug.ideHelper('generating') + generateIdeHelper(this, this.options.root, this.options.generateIdeHelper) + } + get componentNameMap() { return this._componentNameMap } diff --git a/src/core/ideHelper.ts b/src/core/ideHelper.ts new file mode 100644 index 00000000..7a31ff35 --- /dev/null +++ b/src/core/ideHelper.ts @@ -0,0 +1,71 @@ +import { dirname, relative, isAbsolute } from 'path' +import { promises as fs, existsSync } from 'fs' +import { notNullish, slash } from '@antfu/utils' +import { Context } from './context' +import { getVueVersion } from './options' +import { getTransformedPath } from './utils' + +export async function generateIdeHelper(ctx: Context, root: string, filepath: string) { + const imports: Record = Object.fromEntries( + Object.values({ + ...ctx.componentNameMap, + ...ctx.componentCustomMap, + }) + .map(({ path, name, importName }) => { + if (!name) + return undefined + path = getTransformedPath(path, ctx) + const related = isAbsolute(path) + ? `./${relative(dirname(filepath), path)}` + : path + + let entry = 'import ' + if (importName) + entry += `{ ${importName} as ${name} }` + else + entry += name + entry += ` from '${slash(related)}';` + return [name, entry] + }) + .filter(notNullish), + ) + + if (!Object.keys(imports).length) + return + + const originalContent = existsSync(filepath) ? await fs.readFile(filepath, 'utf-8') : '' + + const lines = Object.entries(imports) + .sort((a, b) => a[0].localeCompare(b[0])) + + let code = `// generated by unplugin-vue-components +// We suggest you to NOT commit this file into source control +// Read more: https://github.com/antfu/unplugin-vue-components/issues/135 +` + + if (getVueVersion() === 'vue3') { + code += `import { createApp } from "vue"; + +${lines.map(line => line[1]).join('\n')} + +const app = createApp({}); + +${lines.map(line => `app.component('${line[0]}', ${line[0]})`).join('\n')} + +app.mount("body"); + +` + } + else { + code += `import Vue from "vue"; + +${lines.map(line => line[1]).join('\n')} + +${lines.map(line => `Vue.component('${line[0]}', ${line[0]});\nVue.component('Lazy${line[0]}', ${line[0]});`).join('\n')} + +` + } + + if (code !== originalContent) + await fs.writeFile(filepath, code, 'utf-8') +} diff --git a/src/core/options.ts b/src/core/options.ts index e1a78faa..9841468d 100644 --- a/src/core/options.ts +++ b/src/core/options.ts @@ -19,6 +19,8 @@ export const defaultOptions: Omit, 'include' | 'exclude' | 'tr importPathTransform: v => v, allowOverrides: false, + + generateIdeHelper: false, } function normalizeResolvers(resolvers: (ComponentResolver | ComponentResolver[])[]): ComponentResolverObject[] { @@ -60,6 +62,16 @@ export function resolveOptions(options: Options, root: string): ResolvedOptions ? options.dts : 'components.d.ts', ) + + resolved.generateIdeHelper = !options.generateIdeHelper + ? false + : resolve( + root, + typeof options.generateIdeHelper === 'string' + ? options.generateIdeHelper + : '.components.gen.js', + ) + resolved.root = root resolved.transformer = options.transformer || getVueVersion() || 'vue3' resolved.directives = (typeof options.directives === 'boolean') @@ -71,7 +83,7 @@ export function resolveOptions(options: Options, root: string): ResolvedOptions return resolved } -function getVueVersion() { +export function getVueVersion() { try { // eslint-disable-next-line @typescript-eslint/no-var-requires const vue = require('vue') diff --git a/src/core/transformer.ts b/src/core/transformer.ts index ee53c58a..bbc87506 100644 --- a/src/core/transformer.ts +++ b/src/core/transformer.ts @@ -15,7 +15,7 @@ export interface ResolveResult { replace: (resolved: string) => void } -export default function tranformer(ctx: Context, transformer: SupportedTransformer): Transformer { +export default function transformer(ctx: Context, transformer: SupportedTransformer): Transformer { return async(code, id, path) => { ctx.searchGlob() diff --git a/src/core/unplugin.ts b/src/core/unplugin.ts index 32365858..8f01fca3 100644 --- a/src/core/unplugin.ts +++ b/src/core/unplugin.ts @@ -26,6 +26,7 @@ export default createUnplugin((options = {}) => { try { const result = await ctx.transform(code, id) ctx.generateDeclaration() + ctx.generateIdeHelper() return result } catch (e) { @@ -44,6 +45,7 @@ export default createUnplugin((options = {}) => { if (options.dts) { ctx.searchGlob() ctx.generateDeclaration() + ctx.generateIdeHelper() } if (config.build.watch && config.command === 'build') diff --git a/src/types.ts b/src/types.ts index 9c9045b1..6250117c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -138,6 +138,15 @@ export interface Options { * @default undefined */ directives?: boolean + + /** + * Generate components.js helper for IntelliJ IDEs. + * + * Accept boolean or a path related to project root. + * + * @default false + */ + generateIdeHelper?: boolean | string } export type ResolvedOptions = Omit< @@ -151,6 +160,7 @@ Required, resolvedDirs: string[] globs: string[] dts: string | false + generateIdeHelper: string | false root: string }