Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: declaration generation error (#733) #743

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
67 changes: 44 additions & 23 deletions src/builder/bundless/dts/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<Output[]> {
const cache = getCache('bundless-dts');
const enableCache = process.env.FATHER_CACHE !== 'none';
const tscCacheDir = path.join(opts.cwd, getCachePath(), 'tsc');
Expand All @@ -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');
Expand Down Expand Up @@ -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<Record<string, string>>(
(ret, file) => ({
...ret,
// format: {path:contenthash}
[file]: [file, getContentHash(fs.readFileSync(file, 'utf-8'))].join(
':',
),
[file]: ofFileCacheKey(file),
}),
{},
);

const cacheRets: Record<string, typeof output> = {};

tsHost.writeFile = (fileName, content, _a, _b, sourceFiles) => {
Expand All @@ -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,
);
Expand All @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -256,7 +276,8 @@ export default async function getDeclarations(
});
throw new Error('Declaration generation failed.');
}
}

cache.setSync(inputCacheKey, output);
}
return output;
}
75 changes: 53 additions & 22 deletions src/builder/bundless/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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<string, string>();

// 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, {
Expand Down Expand Up @@ -113,23 +131,36 @@ async function transformFiles(
}

if (declarationFileMap.size) {
logger.quietExpect.event(
`Generate declaration file${declarationFileMap.size > 1 ? 's' : ''}...`,
);

const declarations = await getDeclarations(
[...declarationFileMap.keys()],
{
cwd: opts.cwd,
},
);

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');
});
}

Expand Down
1 change: 1 addition & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) }));
Expand Down