Skip to content

Commit d4c27d4

Browse files
authored
Improve fsImporter performance by adding a resolve cache (#707)
1 parent caae6bf commit d4c27d4

File tree

2 files changed

+60
-22
lines changed

2 files changed

+60
-22
lines changed

.changeset/twelve-trainers-tap.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'react-docgen': major
3+
---
4+
5+
Improve performance of file system importer.
6+
7+
The file system importer now also caches resolving of files in addition to parsing files.
8+
If the importer is used in an environment where files do change at runtime (like a watch
9+
command) then the caches will need to be cleared on every file change.

packages/react-docgen/src/importer/makeFsImporter.ts

Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,36 @@ import type { Importer, ImportPath } from './index.js';
99
import type FileState from '../FileState.js';
1010
import { resolveObjectPatternPropertyToValue } from '../utils/index.js';
1111

12+
// These extensions are sorted by priority
13+
// resolve() will check for files in the order these extensions are sorted
1214
const RESOLVE_EXTENSIONS = [
1315
'.js',
14-
'.jsx',
15-
'.cjs',
16-
'.mjs',
1716
'.ts',
1817
'.tsx',
18+
'.mjs',
19+
'.cjs',
1920
'.mts',
2021
'.cts',
22+
'.jsx',
2123
];
2224

2325
function defaultLookupModule(filename: string, basedir: string): string {
26+
const resolveOptions = {
27+
basedir,
28+
extensions: RESOLVE_EXTENSIONS,
29+
// we do not need to check core modules as we cannot import them anyway
30+
includeCoreModules: false,
31+
};
32+
2433
try {
25-
return resolve.sync(filename, {
26-
basedir,
27-
extensions: RESOLVE_EXTENSIONS,
28-
});
34+
return resolve.sync(filename, resolveOptions);
2935
} catch (error) {
3036
const ext = extname(filename);
3137
let newFilename: string;
3238

3339
// if we try to import a JavaScript file it might be that we are actually pointing to
3440
// a TypeScript file. This can happen in ES modules as TypeScript requires to import other
35-
// TypeScript files with JavaScript extensions
41+
// TypeScript files with .js extensions
3642
// https://www.typescriptlang.org/docs/handbook/esm-node.html#type-in-packagejson-and-new-extensions
3743
switch (ext) {
3844
case '.js':
@@ -49,8 +55,9 @@ function defaultLookupModule(filename: string, basedir: string): string {
4955
}
5056

5157
return resolve.sync(newFilename, {
52-
basedir,
53-
extensions: RESOLVE_EXTENSIONS,
58+
...resolveOptions,
59+
// we already know that there is an extension at this point, so no need to check other extensions
60+
extensions: [],
5461
});
5562
}
5663
}
@@ -62,13 +69,23 @@ interface TraverseState {
6269
resultPath?: NodePath | null;
6370
}
6471

72+
interface FsImporterCache {
73+
parseCache: Map<string, FileState>;
74+
resolveCache: Map<string, string | null>;
75+
}
76+
6577
// Factory for the resolveImports importer
78+
// If this resolver is used in an environment where the source files change (e.g. watch)
79+
// then the cache needs to be cleared on file changes.
6680
export default function makeFsImporter(
6781
lookupModule: (
6882
filename: string,
6983
basedir: string,
7084
) => string = defaultLookupModule,
71-
cache: Map<string, FileState> = new Map(),
85+
{ parseCache, resolveCache }: FsImporterCache = {
86+
parseCache: new Map(),
87+
resolveCache: new Map(),
88+
},
7289
): Importer {
7390
function resolveImportedValue(
7491
path: ImportPath,
@@ -87,36 +104,48 @@ export default function makeFsImporter(
87104

88105
// Resolve the imported module using the Node resolver
89106
const basedir = dirname(filename);
90-
let resolvedSource: string | undefined;
107+
const resolveCacheKey = `${basedir}|${source}`;
108+
let resolvedSource = resolveCache.get(resolveCacheKey);
109+
110+
// We haven't found it before, so no need to look again
111+
if (resolvedSource === null) {
112+
return null;
113+
}
114+
115+
// First time we try to resolve this file
116+
if (resolvedSource === undefined) {
117+
try {
118+
resolvedSource = lookupModule(source, basedir);
119+
} catch (error) {
120+
const { code } = error as NodeJS.ErrnoException;
91121

92-
try {
93-
resolvedSource = lookupModule(source, basedir);
94-
} catch (error) {
95-
const { code } = error as NodeJS.ErrnoException;
122+
if (code === 'MODULE_NOT_FOUND' || code === 'INVALID_PACKAGE_MAIN') {
123+
resolveCache.set(resolveCacheKey, null);
96124

97-
if (code === 'MODULE_NOT_FOUND' || code === 'INVALID_PACKAGE_MAIN') {
98-
return null;
125+
return null;
126+
}
127+
128+
throw error;
99129
}
100130

101-
throw error;
131+
resolveCache.set(resolveCacheKey, resolvedSource);
102132
}
103-
104133
// Prevent recursive imports
105134
if (seen.has(resolvedSource)) {
106135
return null;
107136
}
108137

109138
seen.add(resolvedSource);
110139

111-
let nextFile = cache.get(resolvedSource);
140+
let nextFile = parseCache.get(resolvedSource);
112141

113142
if (!nextFile) {
114143
// Read and parse the code
115144
const src = fs.readFileSync(resolvedSource, 'utf8');
116145

117146
nextFile = file.parse(src, resolvedSource);
118147

119-
cache.set(resolvedSource, nextFile);
148+
parseCache.set(resolvedSource, nextFile);
120149
}
121150

122151
return findExportedValue(nextFile, name, seen);

0 commit comments

Comments
 (0)