diff --git a/src/builder/bundless/dts/index.ts b/src/builder/bundless/dts/index.ts index 5c4f30b8..657a406f 100644 --- a/src/builder/bundless/dts/index.ts +++ b/src/builder/bundless/dts/index.ts @@ -1,4 +1,4 @@ -import { chalk, fsExtra, winPath } from '@umijs/utils'; +import { chalk, fsExtra, lodash, winPath } from '@umijs/utils'; import fs from 'fs'; import path from 'path'; import tsPathsTransformer from 'typescript-transform-paths'; @@ -40,13 +40,29 @@ export function getTsconfig(cwd: string) { } } +/** + * 文件在缓存中的索引 + * + * format: {path:contenthash} + * @private + */ +function getFileCacheKey(file: string): string { + return [file, getContentHash(fs.readFileSync(file, 'utf-8'))].join(':'); +} + +type Output = { + file: string; + content: string; + sourceFile: string; +}; + /** * get declarations for specific files */ export default async function getDeclarations( inputFiles: string[], opts: { cwd: string }, -) { +): Promise { const cache = getCache('bundless-dts'); const enableCache = process.env.FATHER_CACHE !== 'none'; const tscCacheDir = path.join(opts.cwd, getCachePath(), 'tsc'); @@ -55,7 +71,7 @@ export default async function getDeclarations( fsExtra.ensureDirSync(tscCacheDir); } - const output: { file: string; content: string; sourceFile: string }[] = []; + const output: Output[] = []; // use require() rather than import(), to avoid jest runner to fail // ref: https://github.com/nodejs/node/issues/35889 const ts: typeof import('typescript') = require('typescript'); @@ -117,16 +133,17 @@ export default async function getDeclarations( } const tsHost = ts.createIncrementalCompilerHost(tsconfig.options); + + const ofFileCacheKey = lodash.memoize(getFileCacheKey, lodash.identity); + const cacheKeys = inputFiles.reduce>( (ret, file) => ({ ...ret, - // format: {path:contenthash} - [file]: [file, getContentHash(fs.readFileSync(file, 'utf-8'))].join( - ':', - ), + [file]: ofFileCacheKey(file), }), {}, ); + const cacheRets: Record = {}; tsHost.writeFile = (fileName, content, _a, _b, sourceFiles) => { @@ -143,9 +160,17 @@ export default async function getDeclarations( sourceFile, }; + const cacheKey = cacheKeys[sourceFile] ?? ofFileCacheKey(sourceFile); + + // 通过 cache 判断该输出是否属于本项目的有效 build + const existInCache = () => + !lodash.isEmpty(cache.getSync(cacheKey, null)); + // only collect dts for input files, to avoid output error in watch mode // ref: https://github.com/umijs/father-next/issues/43 - if (inputFiles.includes(sourceFile)) { + const shouldOutput = inputFiles.includes(sourceFile) || existInCache(); + + if (shouldOutput) { const index = output.findIndex( (out) => out.file === ret.file && out.sourceFile === ret.sourceFile, ); @@ -159,25 +184,17 @@ export default async function getDeclarations( // group cache by file (d.ts & d.ts.map) // always save cache even if it's not input file, to avoid cache miss // because it probably can be used in next bundless run - const cacheKey = - cacheKeys[sourceFile] || - [ - sourceFile, - getContentHash(fs.readFileSync(sourceFile, 'utf-8')), - ].join(':'); - cacheRets[cacheKey] ??= []; cacheRets[cacheKey].push(ret); } }; + const inputCacheKey = inputFiles.map(ofFileCacheKey).join(':'); // use cache first - inputFiles.forEach((file) => { - const cacheRet = cache.getSync(cacheKeys[file], ''); - if (cacheRet) { - output.push(...cacheRet); - } - }); + // 因为上一次处理结果的 output 可能超过 inputFiles + // 所以优先使用缓存结果 避免 ts 增量处理而跳过的输出 + const outputCached = cache.getSync(inputCacheKey, null); + (outputCached ?? []).forEach((ret: Output) => output.push(ret)); const incrProgram = ts.createIncrementalProgram({ rootNames: tsconfig.fileNames, @@ -225,7 +242,10 @@ export default async function getDeclarations( .getPreEmitDiagnostics(incrProgram.getProgram()) .concat(result.diagnostics) // omit error for files which not included by build - .filter((d) => !d.file || inputFiles.includes(d.file.fileName)); + .filter((d) => { + const file = d.file; + return !file || output.some((it) => it.sourceFile === file.fileName); + }); /* istanbul ignore if -- @preserve */ if (diagnostics.length) { @@ -256,7 +276,8 @@ export default async function getDeclarations( }); throw new Error('Declaration generation failed.'); } - } + cache.setSync(inputCacheKey, output); + } return output; } diff --git a/src/builder/bundless/index.ts b/src/builder/bundless/index.ts index 9f781990..01a5cfc7 100644 --- a/src/builder/bundless/index.ts +++ b/src/builder/bundless/index.ts @@ -30,6 +30,14 @@ function replacePathExt(filePath: string, ext: string) { return path.join(parsed.dir, `${parsed.name}${ext}`); } +// create parent directory if not exists +// TODO maybe can import fsExtra from @umijs/utils +function ensureDirSync(dir: string) { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } +} + /** * transform specific files */ @@ -41,27 +49,37 @@ async function transformFiles( watch?: true; }, ) { + // get config and dist path info for specific item + const itemPathInfo = (fileInWatch: string) => { + const config = opts.configProvider.getConfigForFile(fileInWatch); + + if (config) { + const itemDistPath = path.join( + config.output!, + path.relative(config.input, fileInWatch), + ); + const itemDistAbsPath = path.join(opts.cwd, itemDistPath); + const itemDistDir = path.dirname(itemDistAbsPath); + + return { config, itemDistPath, itemDistAbsPath, itemDistDir }; + } else { + return null; + } + }; try { let count = 0; const declarationFileMap = new Map(); // process all matched items for (let item of files) { - const config = opts.configProvider.getConfigForFile(item); + const pathInfo = itemPathInfo(item); const itemAbsPath = path.join(opts.cwd, item); - if (config) { - let itemDistPath = path.join( - config.output!, - path.relative(config.input, item), - ); - let itemDistAbsPath = path.join(opts.cwd, itemDistPath); - const parentPath = path.dirname(itemDistAbsPath); + if (pathInfo) { + const { config, itemDistDir: parentPath } = pathInfo; + let { itemDistPath, itemDistAbsPath } = pathInfo; - // create parent directory if not exists - if (!fs.existsSync(parentPath)) { - fs.mkdirSync(parentPath, { recursive: true }); - } + ensureDirSync(parentPath); // get result from loaders const result = await runLoaders(itemAbsPath, { @@ -113,10 +131,6 @@ async function transformFiles( } if (declarationFileMap.size) { - logger.quietExpect.event( - `Generate declaration file${declarationFileMap.size > 1 ? 's' : ''}...`, - ); - const declarations = await getDeclarations( [...declarationFileMap.keys()], { @@ -124,12 +138,29 @@ async function transformFiles( }, ); - declarations.forEach((item) => { - fs.writeFileSync( - path.join(declarationFileMap.get(item.sourceFile)!, item.file), - item.content, - 'utf-8', - ); + const dtsFiles = declarations + // filterMap: filter out declarations with unrecognized distDir and mapping it + .flatMap(({ sourceFile, ...declaration }) => { + // prioritize using declarationFileMap + // if not available, try to recalculate itemDistDir + const distDir = + declarationFileMap.get(sourceFile) ?? + itemPathInfo(path.relative(opts.cwd, sourceFile))?.itemDistDir; + + return distDir + ? (() => { + ensureDirSync(distDir); + return [{ distDir, declaration }]; + })() + : []; + }); + + logger.quietExpect.event( + `Generate declaration file${dtsFiles.length > 1 ? 's' : ''}...`, + ); + + dtsFiles.forEach(({ distDir, declaration: { file, content } }) => { + fs.writeFileSync(path.join(distDir, file), content, 'utf-8'); }); } diff --git a/src/utils.ts b/src/utils.ts index b860a6b1..29130d5c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -21,6 +21,7 @@ export function getCache(ns: string): (typeof caches)['0'] { // return fake cache if cache disabled if (process.env.FATHER_CACHE === 'none') { const deferrer = () => Promise.resolve(); + // FIXME: getSync should support second parameter return { set: deferrer, get: deferrer, setSync() {}, getSync() {} } as any; } return (caches[ns] ??= Cache({ basePath: path.join(getCachePath(), ns) }));