From cc5aff9b9906fc87f6012fb66566fd1937a9381e Mon Sep 17 00:00:00 2001 From: rk_zhang <40221744+qmhc@users.noreply.github.com> Date: Wed, 3 Nov 2021 18:49:11 +0800 Subject: [PATCH] fix: support require for cjs exports fix #39 --- example/vite.config.ts | 4 +- src/compile.ts | 2 +- src/index.ts | 389 +---------------------------------------- src/plugin.ts | 387 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 394 insertions(+), 388 deletions(-) create mode 100644 src/plugin.ts diff --git a/example/vite.config.ts b/example/vite.config.ts index 91032b6..7379cf9 100644 --- a/example/vite.config.ts +++ b/example/vite.config.ts @@ -2,7 +2,7 @@ import { resolve } from 'path' import { existsSync, readdirSync, lstatSync, rmdirSync, unlinkSync } from 'fs' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' -import dts from '../src' +import { dtsPlugin } from '../src/plugin' emptyDir(resolve(__dirname, 'types')) @@ -26,7 +26,7 @@ export default defineConfig({ } }, plugins: [ - dts({ + dtsPlugin({ outputDir: 'types', // include: ['src/index.ts'], exclude: ['src/ignore'], diff --git a/src/compile.ts b/src/compile.ts index 39f63e4..255b38e 100644 --- a/src/compile.ts +++ b/src/compile.ts @@ -4,7 +4,7 @@ const exportDefaultRE = /export\s+default/ const exportDefaultClassRE = /(?:(?:^|\n|;)\s*)export\s+default\s+class\s+([\w$]+)/ let index = 1 -let compiler: typeof import('@vue/compiler-sfc') +let compiler: typeof import('vue/compiler-sfc') function requireCompiler() { if (!compiler) { diff --git a/src/index.ts b/src/index.ts index 9946f29..290fa25 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,387 +1,6 @@ -import { resolve, dirname, relative } from 'path' -import fs from 'fs-extra' -import os from 'os' -import chalk from 'chalk' -import glob from 'fast-glob' -import { debug } from 'debug' -import { Project } from 'ts-morph' -import { normalizePath } from 'vite' -import { readConfigFile } from 'typescript' -import { - normalizeGlob, - transformDynamicImport, - transformAliasImport, - removePureImport -} from './transform' -import { compileVueCode } from './compile' -import { - isNativeObj, - isPromise, - mergeObjects, - ensureAbsolute, - ensureArray, - runParallel -} from './utils' +import { dtsPlugin } from './plugin' -import type { Plugin, Alias, Logger } from 'vite' -import type { ts, Diagnostic } from 'ts-morph' +export default dtsPlugin -interface TransformWriteFile { - filePath?: string, - content?: string -} - -export interface PluginOptions { - include?: string | string[], - exclude?: string | string[], - root?: string, - outputDir?: string, - compilerOptions?: ts.CompilerOptions | null, - tsConfigFilePath?: string, - cleanVueFileName?: boolean, - staticImport?: boolean, - clearPureImport?: boolean, - insertTypesEntry?: boolean, - copyDtsFiles?: boolean, - noEmitOnError?: boolean, - skipDiagnostics?: boolean, - logDiagnostics?: boolean, - afterDiagnostic?: (diagnostics: Diagnostic[]) => void | Promise, - beforeWriteFile?: (filePath: string, content: string) => void | TransformWriteFile, - afterBuild?: () => void | Promise -} - -const noneExport = 'export {};\n' -const vueRE = /\.vue$/ -const tsRE = /\.tsx?$/ -const jsRE = /\.jsx?$/ -const dtsRE = /\.d\.tsx?$/ -const tjsRE = /\.(t|j)sx?$/ -const watchExtensionRE = /\.(vue|(t|j)sx?)$/ -// eslint-disable-next-line @typescript-eslint/no-empty-function -const noop = () => {} - -const bundleDebug = debug('vite-plugin-dts:bundle') - -export default function dtsPlugin(options: PluginOptions = {}): Plugin { - const { - tsConfigFilePath = 'tsconfig.json', - cleanVueFileName = false, - staticImport = false, - clearPureImport = true, - insertTypesEntry = false, - noEmitOnError = false, - skipDiagnostics = true, - logDiagnostics = false, - afterDiagnostic = noop, - beforeWriteFile = noop, - afterBuild = noop - } = options - - const compilerOptions = options.compilerOptions ?? {} - - let root: string - let aliases: Alias[] - let entries: string[] - let logger: Logger - let project: Project - let tsConfigPath: string - let outputDir: string - let isBundle = false - - const sourceDtsFiles = new Set() - - let hasJsVue = false - let allowJs = false - - return { - name: 'vite:dts', - - apply: 'build', - - enforce: 'pre', - - config(config) { - if (isBundle) return - - const aliasOptions = (config.resolve && config.resolve.alias) ?? [] - - if (isNativeObj(aliasOptions)) { - aliases = Object.entries(aliasOptions).map(([key, value]) => { - return { find: key, replacement: value } - }) - } else { - aliases = ensureArray(aliasOptions) - } - }, - - configResolved(config) { - if (isBundle) return - - logger = config.logger - - if (!config.build.lib) { - logger.warn( - chalk.yellow( - `\n${chalk.cyan( - '[vite:dts]' - )} You building not a library that may not need to generate declaration files.\n` - ) - ) - } - - root = ensureAbsolute(options.root ?? '', config.root) - tsConfigPath = ensureAbsolute(tsConfigFilePath, root) - - outputDir = options.outputDir - ? ensureAbsolute(options.outputDir, root) - : ensureAbsolute(config.build.outDir, root) - - if (!outputDir) { - logger.error( - chalk.red( - `\n${chalk.cyan( - '[vite:dts]' - )} Can not resolve declaration directory, please check your vite config and plugin options.\n` - ) - ) - - return - } - - compilerOptions.rootDir ||= root - - project = new Project({ - compilerOptions: mergeObjects(compilerOptions, { - noEmitOnError, - outDir: '.', - // #27 declarationDir option will make no declaration file generated - declarationDir: null, - declaration: true, - noEmit: false, - emitDeclarationOnly: true - }), - tsConfigFilePath: tsConfigPath, - skipAddingFilesFromTsConfig: true - }) - - allowJs = project.getCompilerOptions().allowJs ?? false - }, - - buildStart(inputOptions) { - if (insertTypesEntry) { - entries = Array.isArray(inputOptions.input) - ? inputOptions.input - : Object.values(inputOptions.input) - } - }, - - transform(code, id) { - if (vueRE.test(id)) { - const { content, ext } = compileVueCode(code) - - if (content) { - if (ext === 'js' || ext === 'jsx') hasJsVue = true - - project.createSourceFile(`${id}.${ext || 'js'}`, content, { overwrite: true }) - } - } else if (!id.includes('.vue?vue') && (tsRE.test(id) || (allowJs && jsRE.test(id)))) { - project.addSourceFileAtPath(id) - } - - return null - }, - - watchChange(id) { - if (watchExtensionRE.test(id)) { - isBundle = false - - if (project) { - const sourceFile = project.getSourceFile(normalizePath(id)) - - sourceFile && project.removeSourceFile(sourceFile) - } - } - }, - - async closeBundle() { - if (!outputDir || !project || isBundle) return - - logger.info(chalk.green(`\n${chalk.cyan('[vite:dts]')} Start generate declaration files...`)) - bundleDebug('start') - - isBundle = true - - sourceDtsFiles.clear() - - const startTime = Date.now() - const tsConfig: { - include?: string[], - exclude?: string[] - } = readConfigFile(tsConfigPath, project.getFileSystem().readFileSync).config ?? {} - - const include = options.include ?? tsConfig.include - const exclude = options.exclude ?? tsConfig.exclude - - bundleDebug('read config') - - const includedFileSet = new Set() - - if (include && include.length) { - const files = await glob(ensureArray(include).map(normalizeGlob), { - cwd: root, - absolute: true, - ignore: ensureArray(exclude ?? ['node_modules/**']).map(normalizeGlob) - }) - - files.forEach(file => { - includedFileSet.add( - dtsRE.test(file) ? file : `${tjsRE.test(file) ? file.replace(tjsRE, '') : file}.d.ts` - ) - - if (dtsRE.test(file)) { - sourceDtsFiles.add(file) - } - }) - - if (hasJsVue) { - if (!allowJs) { - logger.warn( - chalk.yellow( - `${chalk.cyan( - '[vite:dts]' - )} Some js files are referenced, but you may not enable the 'allowJs' option.` - ) - ) - } - - project.compilerOptions.set({ allowJs: true }) - } - - bundleDebug('collect files') - } - - project.resolveSourceFileDependencies() - bundleDebug('resolve') - - if (!skipDiagnostics) { - const diagnostics = project.getPreEmitDiagnostics() - - if (diagnostics?.length && logDiagnostics) { - logger.warn(project.formatDiagnosticsWithColorAndContext(diagnostics)) - } - - if (typeof afterDiagnostic === 'function') { - const result = afterDiagnostic(diagnostics) - - isPromise(result) && (await result) - } - - bundleDebug('diagnostics') - } - - const service = project.getLanguageService() - const outputFiles = project - .getSourceFiles() - .map(sourceFile => { - return service.getEmitOutput(sourceFile, true).getOutputFiles() - }) - .flat() - - bundleDebug('emit') - - await runParallel(os.cpus().length, outputFiles, async outputFile => { - let filePath = outputFile.getFilePath() as string - let content = outputFile.getText() - - const isMapFile = filePath.endsWith('.map') - - if ( - !includedFileSet.has(isMapFile ? filePath.slice(0, -4) : filePath) || - (clearPureImport && content === noneExport) - ) - return - - if (!isMapFile && content !== noneExport) { - content = clearPureImport ? removePureImport(content) : content - content = transformAliasImport(filePath, content, aliases) - content = staticImport ? transformDynamicImport(content) : content - } - - filePath = resolve( - outputDir, - relative(root, cleanVueFileName ? filePath.replace('.vue.d.ts', '.d.ts') : filePath) - ) - - if (typeof beforeWriteFile === 'function') { - const result = beforeWriteFile(filePath, content) - - if (result && isNativeObj(result)) { - filePath = result.filePath ?? filePath - content = result.content ?? content - } - } - - await fs.mkdir(dirname(filePath), { recursive: true }) - await fs.writeFile( - filePath, - cleanVueFileName ? content.replace(/['"](.+)\.vue['"]/g, '"$1"') : content, - 'utf8' - ) - }) - - bundleDebug('output') - - await Promise.all( - Array.from(sourceDtsFiles).map(async filePath => { - const targetPath = resolve(outputDir, relative(root, filePath)) - - await fs.mkdir(dirname(targetPath), { recursive: true }) - await fs.copyFile(filePath, targetPath) - }) - ) - - bundleDebug('copy dts') - - if (insertTypesEntry) { - const pkgPath = resolve(root, 'package.json') - const pkg = fs.existsSync(pkgPath) ? JSON.parse(await fs.readFile(pkgPath, 'utf-8')) : {} - const typesPath = pkg.types ? resolve(root, pkg.types) : resolve(outputDir, 'index.d.ts') - - if (!fs.existsSync(typesPath)) { - const content = - entries - .map(entry => { - let filePath = normalizePath( - relative(dirname(typesPath), resolve(outputDir, relative(root, entry))) - ) - - filePath = filePath.replace(tsRE, '') - filePath = /^\.\.?\//.test(filePath) ? filePath : `./${filePath}` - - return `export * from '${filePath}'` - }) - .join('\n') + '\n' - - await fs.writeFile(typesPath, content, 'utf-8') - } - - bundleDebug('insert index') - } - - if (typeof afterBuild === 'function') { - const result = afterBuild() - - isPromise(result) && (await result) - } - - bundleDebug('finish') - - logger.info( - chalk.green( - `${chalk.cyan('[vite:dts]')} Declaration files built in ${Date.now() - startTime}ms.\n` - ) - ) - } - } -} +module.exports = dtsPlugin +;(dtsPlugin as any)['default'] = dtsPlugin diff --git a/src/plugin.ts b/src/plugin.ts new file mode 100644 index 0000000..398fa5f --- /dev/null +++ b/src/plugin.ts @@ -0,0 +1,387 @@ +import { resolve, dirname, relative } from 'path' +import fs from 'fs-extra' +import os from 'os' +import chalk from 'chalk' +import glob from 'fast-glob' +import { debug } from 'debug' +import { Project } from 'ts-morph' +import { normalizePath } from 'vite' +import { readConfigFile } from 'typescript' +import { + normalizeGlob, + transformDynamicImport, + transformAliasImport, + removePureImport +} from './transform' +import { compileVueCode } from './compile' +import { + isNativeObj, + isPromise, + mergeObjects, + ensureAbsolute, + ensureArray, + runParallel +} from './utils' + +import type { Plugin, Alias, Logger } from 'vite' +import type { ts, Diagnostic } from 'ts-morph' + +interface TransformWriteFile { + filePath?: string, + content?: string +} + +export interface PluginOptions { + include?: string | string[], + exclude?: string | string[], + root?: string, + outputDir?: string, + compilerOptions?: ts.CompilerOptions | null, + tsConfigFilePath?: string, + cleanVueFileName?: boolean, + staticImport?: boolean, + clearPureImport?: boolean, + insertTypesEntry?: boolean, + copyDtsFiles?: boolean, + noEmitOnError?: boolean, + skipDiagnostics?: boolean, + logDiagnostics?: boolean, + afterDiagnostic?: (diagnostics: Diagnostic[]) => void | Promise, + beforeWriteFile?: (filePath: string, content: string) => void | TransformWriteFile, + afterBuild?: () => void | Promise +} + +const noneExport = 'export {};\n' +const vueRE = /\.vue$/ +const tsRE = /\.tsx?$/ +const jsRE = /\.jsx?$/ +const dtsRE = /\.d\.tsx?$/ +const tjsRE = /\.(t|j)sx?$/ +const watchExtensionRE = /\.(vue|(t|j)sx?)$/ +// eslint-disable-next-line @typescript-eslint/no-empty-function +const noop = () => {} + +const bundleDebug = debug('vite-plugin-dts:bundle') + +export function dtsPlugin(options: PluginOptions = {}): Plugin { + const { + tsConfigFilePath = 'tsconfig.json', + cleanVueFileName = false, + staticImport = false, + clearPureImport = true, + insertTypesEntry = false, + noEmitOnError = false, + skipDiagnostics = true, + logDiagnostics = false, + afterDiagnostic = noop, + beforeWriteFile = noop, + afterBuild = noop + } = options + + const compilerOptions = options.compilerOptions ?? {} + + let root: string + let aliases: Alias[] + let entries: string[] + let logger: Logger + let project: Project + let tsConfigPath: string + let outputDir: string + let isBundle = false + + const sourceDtsFiles = new Set() + + let hasJsVue = false + let allowJs = false + + return { + name: 'vite:dts', + + apply: 'build', + + enforce: 'pre', + + config(config) { + if (isBundle) return + + const aliasOptions = (config.resolve && config.resolve.alias) ?? [] + + if (isNativeObj(aliasOptions)) { + aliases = Object.entries(aliasOptions).map(([key, value]) => { + return { find: key, replacement: value } + }) + } else { + aliases = ensureArray(aliasOptions) + } + }, + + configResolved(config) { + if (isBundle) return + + logger = config.logger + + if (!config.build.lib) { + logger.warn( + chalk.yellow( + `\n${chalk.cyan( + '[vite:dts]' + )} You building not a library that may not need to generate declaration files.\n` + ) + ) + } + + root = ensureAbsolute(options.root ?? '', config.root) + tsConfigPath = ensureAbsolute(tsConfigFilePath, root) + + outputDir = options.outputDir + ? ensureAbsolute(options.outputDir, root) + : ensureAbsolute(config.build.outDir, root) + + if (!outputDir) { + logger.error( + chalk.red( + `\n${chalk.cyan( + '[vite:dts]' + )} Can not resolve declaration directory, please check your vite config and plugin options.\n` + ) + ) + + return + } + + compilerOptions.rootDir ||= root + + project = new Project({ + compilerOptions: mergeObjects(compilerOptions, { + noEmitOnError, + outDir: '.', + // #27 declarationDir option will make no declaration file generated + declarationDir: null, + declaration: true, + noEmit: false, + emitDeclarationOnly: true + }), + tsConfigFilePath: tsConfigPath, + skipAddingFilesFromTsConfig: true + }) + + allowJs = project.getCompilerOptions().allowJs ?? false + }, + + buildStart(inputOptions) { + if (insertTypesEntry) { + entries = Array.isArray(inputOptions.input) + ? inputOptions.input + : Object.values(inputOptions.input) + } + }, + + transform(code, id) { + if (vueRE.test(id)) { + const { content, ext } = compileVueCode(code) + + if (content) { + if (ext === 'js' || ext === 'jsx') hasJsVue = true + + project.createSourceFile(`${id}.${ext || 'js'}`, content, { overwrite: true }) + } + } else if (!id.includes('.vue?vue') && (tsRE.test(id) || (allowJs && jsRE.test(id)))) { + project.addSourceFileAtPath(id) + } + + return null + }, + + watchChange(id) { + if (watchExtensionRE.test(id)) { + isBundle = false + + if (project) { + const sourceFile = project.getSourceFile(normalizePath(id)) + + sourceFile && project.removeSourceFile(sourceFile) + } + } + }, + + async closeBundle() { + if (!outputDir || !project || isBundle) return + + logger.info(chalk.green(`\n${chalk.cyan('[vite:dts]')} Start generate declaration files...`)) + bundleDebug('start') + + isBundle = true + + sourceDtsFiles.clear() + + const startTime = Date.now() + const tsConfig: { + include?: string[], + exclude?: string[] + } = readConfigFile(tsConfigPath, project.getFileSystem().readFileSync).config ?? {} + + const include = options.include ?? tsConfig.include + const exclude = options.exclude ?? tsConfig.exclude + + bundleDebug('read config') + + const includedFileSet = new Set() + + if (include && include.length) { + const files = await glob(ensureArray(include).map(normalizeGlob), { + cwd: root, + absolute: true, + ignore: ensureArray(exclude ?? ['node_modules/**']).map(normalizeGlob) + }) + + files.forEach(file => { + includedFileSet.add( + dtsRE.test(file) ? file : `${tjsRE.test(file) ? file.replace(tjsRE, '') : file}.d.ts` + ) + + if (dtsRE.test(file)) { + sourceDtsFiles.add(file) + } + }) + + if (hasJsVue) { + if (!allowJs) { + logger.warn( + chalk.yellow( + `${chalk.cyan( + '[vite:dts]' + )} Some js files are referenced, but you may not enable the 'allowJs' option.` + ) + ) + } + + project.compilerOptions.set({ allowJs: true }) + } + + bundleDebug('collect files') + } + + project.resolveSourceFileDependencies() + bundleDebug('resolve') + + if (!skipDiagnostics) { + const diagnostics = project.getPreEmitDiagnostics() + + if (diagnostics?.length && logDiagnostics) { + logger.warn(project.formatDiagnosticsWithColorAndContext(diagnostics)) + } + + if (typeof afterDiagnostic === 'function') { + const result = afterDiagnostic(diagnostics) + + isPromise(result) && (await result) + } + + bundleDebug('diagnostics') + } + + const service = project.getLanguageService() + const outputFiles = project + .getSourceFiles() + .map(sourceFile => { + return service.getEmitOutput(sourceFile, true).getOutputFiles() + }) + .flat() + + bundleDebug('emit') + + await runParallel(os.cpus().length, outputFiles, async outputFile => { + let filePath = outputFile.getFilePath() as string + let content = outputFile.getText() + + const isMapFile = filePath.endsWith('.map') + + if ( + !includedFileSet.has(isMapFile ? filePath.slice(0, -4) : filePath) || + (clearPureImport && content === noneExport) + ) + return + + if (!isMapFile && content !== noneExport) { + content = clearPureImport ? removePureImport(content) : content + content = transformAliasImport(filePath, content, aliases) + content = staticImport ? transformDynamicImport(content) : content + } + + filePath = resolve( + outputDir, + relative(root, cleanVueFileName ? filePath.replace('.vue.d.ts', '.d.ts') : filePath) + ) + + if (typeof beforeWriteFile === 'function') { + const result = beforeWriteFile(filePath, content) + + if (result && isNativeObj(result)) { + filePath = result.filePath ?? filePath + content = result.content ?? content + } + } + + await fs.mkdir(dirname(filePath), { recursive: true }) + await fs.writeFile( + filePath, + cleanVueFileName ? content.replace(/['"](.+)\.vue['"]/g, '"$1"') : content, + 'utf8' + ) + }) + + bundleDebug('output') + + await Promise.all( + Array.from(sourceDtsFiles).map(async filePath => { + const targetPath = resolve(outputDir, relative(root, filePath)) + + await fs.mkdir(dirname(targetPath), { recursive: true }) + await fs.copyFile(filePath, targetPath) + }) + ) + + bundleDebug('copy dts') + + if (insertTypesEntry) { + const pkgPath = resolve(root, 'package.json') + const pkg = fs.existsSync(pkgPath) ? JSON.parse(await fs.readFile(pkgPath, 'utf-8')) : {} + const typesPath = pkg.types ? resolve(root, pkg.types) : resolve(outputDir, 'index.d.ts') + + if (!fs.existsSync(typesPath)) { + const content = + entries + .map(entry => { + let filePath = normalizePath( + relative(dirname(typesPath), resolve(outputDir, relative(root, entry))) + ) + + filePath = filePath.replace(tsRE, '') + filePath = /^\.\.?\//.test(filePath) ? filePath : `./${filePath}` + + return `export * from '${filePath}'` + }) + .join('\n') + '\n' + + await fs.writeFile(typesPath, content, 'utf-8') + } + + bundleDebug('insert index') + } + + if (typeof afterBuild === 'function') { + const result = afterBuild() + + isPromise(result) && (await result) + } + + bundleDebug('finish') + + logger.info( + chalk.green( + `${chalk.cyan('[vite:dts]')} Declaration files built in ${Date.now() - startTime}ms.\n` + ) + ) + } + } +}