rsbuild-plugin-dts
Version:
Rsbuild plugin that supports emitting declaration files for TypeScript.
247 lines (246 loc) • 13 kB
JavaScript
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 };