UNPKG

rsbuild-plugin-dts

Version:

Rsbuild plugin that supports emitting declaration files for TypeScript.

247 lines (246 loc) 13 kB
import { logger, rspack } from "@rsbuild/core"; import { fork } from "node:child_process"; import { extname, join, normalize, resolve as external_node_path_resolve } from "node:path"; import node_fs from "node:fs"; import { processDtsFiles, createRequireFromPackageJson, loadTypescript, getDtsEmitPath, clearTempDeclarationDir, cleanTsBuildInfoFile, prepareDtsContext, rewriteDtsExtensions, processSourceEntry, cleanDtsFiles, bundleDtsIfNeeded, color, loadTsconfigResultForExecutable, warnIfOutside, loadTsconfig } from "./188.js"; const readTypescriptVersion = (cwd)=>{ try { const currentRequire = createRequireFromPackageJson(cwd); const packageJsonPath = currentRequire.resolve("typescript/package.json"); const packageJson = JSON.parse(node_fs.readFileSync(packageJsonPath, 'utf-8')); return 'string' == typeof packageJson.version ? packageJson.version : void 0; } catch { return; } }; const parseTypescriptVersion = (version)=>{ const match = version?.match(/^(\d+)\.(\d+)/); if (!match) return; return { major: Number(match[1]), minor: Number(match[2]) }; }; const isTypeScriptVersionAtLeast7 = (version)=>{ const parsedVersion = parseTypescriptVersion(version); if (!parsedVersion) return false; return parsedVersion.major >= 7; }; function validateExplicitIsolatedDtsOptions(options) { if (true !== options.isolated) return; if (options.tsgo) throw new Error('Can not set "dts.isolated: true" when "dts.tsgo: true".'); if (options.build) throw new Error('Can not set "dts.isolated: true" when "dts.build: true".'); if (false === options.abortOnError) throw new Error('Can not set "dts.abortOnError: false" when "dts.isolated: true".'); } function resolveDtsGenerationBackend(options, typescriptVersion) { validateExplicitIsolatedDtsOptions(options); if (true === options.isolated) return 'isolated'; if (isTypeScriptVersionAtLeast7(typescriptVersion)) { if (false === options.tsgo) throw new Error('Can not set "dts.tsgo: false" when using TypeScript 7 or higher.'); return 'tsc-executable'; } if (true === options.tsgo) return 'tsgo-executable'; return 'api-old'; } const applyIsolatedDtsOptions = (isolatedDtsContext)=>{ const bundlerConfig = isolatedDtsContext.bundlerConfig; const RspackRslibPlugin = rspack.experiments.RslibPlugin; const rslibPlugin = bundlerConfig?.plugins?.find((plugin)=>plugin instanceof RspackRslibPlugin); if (!rslibPlugin?._args?.[0]) throw new Error('Can not enable "dts.isolated: true" without the built-in RslibPlugin.'); const emitDts = { rootDir: normalize(external_node_path_resolve(isolatedDtsContext.cwd, isolatedDtsContext.rootDir)), declarationDir: normalize(external_node_path_resolve(isolatedDtsContext.cwd, isolatedDtsContext.declarationDir)) }; rslibPlugin._args[0].emitDts = emitDts; if (rslibPlugin._args[0].emitDts !== emitDts) throw new Error('Failed to configure Rspack isolated declaration emission.'); }; async function createIsolatedDtsContext(dtsGenOptions, bundlerConfig) { const isolatedDtsContext = { ...dtsGenOptions, dtsExtension: dtsGenOptions.dtsExtension ?? '.d.ts', bundlerConfig, ...await prepareDtsContext(dtsGenOptions) }; applyIsolatedDtsOptions(isolatedDtsContext); return isolatedDtsContext; } async function processIsolatedDts(isolatedDtsContext, options = {}) { const { logSuccess = true } = options; await rewriteDtsExtensions(isolatedDtsContext.cwd, isolatedDtsContext.declarationDir, isolatedDtsContext.dtsExtension, isolatedDtsContext.bundle, isolatedDtsContext.tsConfigResult.options.declarationMap); await processDtsFiles(isolatedDtsContext.bundle, isolatedDtsContext.cwd, isolatedDtsContext.declarationDir, isolatedDtsContext.dtsExtension, isolatedDtsContext.redirect ?? { path: true, extension: false }, isolatedDtsContext.tsconfigPath, isolatedDtsContext.rootDir, isolatedDtsContext.paths, isolatedDtsContext.banner, isolatedDtsContext.footer); if (!logSuccess) return; if (isolatedDtsContext.bundle) { if (!isolatedDtsContext.isWatch) logger.info(`declaration files prepared with isolated declaration ${color.dim(`(${isolatedDtsContext.name})`)}`); try { await bundleDtsIfNeeded(isolatedDtsContext, isolatedDtsContext); } catch (error) { if (!isolatedDtsContext.isWatch) throw error; logger.error(error); } } else if (!isolatedDtsContext.isWatch) logger.ready(`declaration files generated with isolated declaration ${color.dim(`(${isolatedDtsContext.name})`)}`); } const PLUGIN_DTS_NAME = 'rsbuild:dts'; const pluginDts = (options = {})=>({ name: PLUGIN_DTS_NAME, setup (api) { const loggerLevel = api.logger.level; logger.level = loggerLevel; let apiExtractorOptions = {}; if (options.bundle && 'object' == typeof options.bundle) apiExtractorOptions = { ...options.bundle }; const bundle = !!options.bundle; options.abortOnError = options.abortOnError ?? true; options.build = options.build ?? false; options.redirect = options.redirect ?? {}; options.redirect.path = options.redirect.path ?? true; options.redirect.extension = options.redirect.extension ?? false; options.alias = options.alias ?? {}; let dtsPromise = Promise.resolve({ status: 'success' }); let promiseResult; let childProcesses = []; const typescriptVersion = readTypescriptVersion(api.context.rootPath); const dtsBackend = resolveDtsGenerationBackend(options, typescriptVersion); const tsApi = 'api-old' === dtsBackend ? loadTypescript(api.context.rootPath) : void 0; let dtsGenOptions; let isolatedDtsContext; api.modifyEnvironmentConfig((config, { mergeEnvironmentConfig })=>{ if (true !== options.isolated) return; return mergeEnvironmentConfig(config, { tools: { swc: { jsc: { experimental: { emitIsolatedDts: true } } } } }); }); api.onBeforeEnvironmentCompile(async ({ isWatch, isFirstCompile, environment, bundlerConfig })=>{ if ('api-old' === dtsBackend && !isFirstCompile) return; const { config } = environment; const dtsEntry = processSourceEntry(bundle, config.source?.entry); const cwd = api.context.rootPath; const configuredTsconfigPath = config.source.tsconfigPath ?? 'tsconfig.json'; let tsconfigPath; let tsConfigResult; if (tsApi) { tsconfigPath = tsApi.findConfigFile(cwd, tsApi.sys.fileExists.bind(tsApi.sys), configuredTsconfigPath); if (tsconfigPath) tsConfigResult = loadTsconfig(tsconfigPath, tsApi); } else { const loadedTsconfig = loadTsconfigResultForExecutable(cwd, configuredTsconfigPath); tsconfigPath = loadedTsconfig?.path; tsConfigResult = loadedTsconfig?.config; } if (!tsconfigPath || !tsConfigResult) { const error = new Error(`Failed to resolve tsconfig file ${color.cyan(`"${configuredTsconfigPath}"`)} from ${color.cyan(cwd)}. Please ensure that the file exists.`); error.stack = ''; throw error; } const { options: rawCompilerOptions } = tsConfigResult; const { declarationDir, outDir, composite, incremental } = rawCompilerOptions; const distPathRoot = 'string' == typeof config.output?.distPath ? config.output?.distPath : config.output?.distPath.root; const dtsEmitPath = getDtsEmitPath(options.distPath, declarationDir, distPathRoot); warnIfOutside(cwd, declarationDir, 'declarationDir'); warnIfOutside(cwd, outDir, 'outDir'); if (false !== config.output.cleanDistPath) await cleanDtsFiles(cwd, dtsEmitPath); if (bundle) await clearTempDeclarationDir(cwd); if ('isolated' !== dtsBackend && (composite || incremental || options.build)) await cleanTsBuildInfoFile(tsconfigPath, rawCompilerOptions); const { bundle: _bundle, isolated: _isolated, tsgo: _tsgo, ...rest } = options; dtsGenOptions = { ...rest, bundle, dtsEntry, dtsEmitPath, userExternals: config.output.externals, apiExtractorOptions, tsconfigPath, tsConfigResult, name: environment.name, cwd, isWatch, loggerLevel: loggerLevel, dtsBackend }; if ('isolated' === dtsBackend) { isolatedDtsContext = await createIsolatedDtsContext(dtsGenOptions, bundlerConfig); dtsPromise = Promise.resolve({ status: 'success' }); return; } const jsExtension = extname(import.meta.filename); const childProcess = fork(join(import.meta.dirname, `./dts${jsExtension}`), [], { stdio: 'inherit' }); childProcesses.push(childProcess); childProcess.once('close', ()=>{ childProcesses = childProcesses.filter((item)=>item !== childProcess); }); childProcess.send(dtsGenOptions); dtsPromise = new Promise((resolve)=>{ childProcess.on('message', (message)=>{ if ('success' === message) resolve({ status: 'success' }); else if ('error' === message) resolve({ status: 'error', errorMessage: `Error occurred in ${environment.name} declaration files generation.` }); }); }); }); api.onAfterBuild({ handler: async ({ isFirstCompile, stats })=>{ if ('api-old' === dtsBackend && !isFirstCompile) return; if (isolatedDtsContext) { try { await processIsolatedDts(isolatedDtsContext, { logSuccess: !stats?.hasErrors() }); promiseResult = { status: 'success' }; } catch (error) { logger.error(error); promiseResult = { status: 'error', errorMessage: `Error occurred in ${isolatedDtsContext.name} declaration files generation.` }; } return; } promiseResult = await dtsPromise; }, order: 'pre' }); api.onAfterBuild(({ isFirstCompile })=>{ if ('api-old' === dtsBackend && !isFirstCompile) return; if ('error' === promiseResult.status) { if (options.abortOnError) { const error = new Error(promiseResult.errorMessage); error.stack = ''; throw error; } if (promiseResult.errorMessage) logger.error(promiseResult.errorMessage); logger.warn('With `abortOnError` configuration currently disabled, type errors will not fail the build, but proper type declaration output cannot be guaranteed.'); } }); const killProcesses = ()=>{ for (const childProcess of childProcesses)if (!childProcess.killed) try { childProcess.kill(); } catch {} childProcesses = []; }; api.onCloseBuild(killProcesses); api.onCloseDevServer(killProcesses); } }); export { PLUGIN_DTS_NAME, pluginDts };