Skip to content

Commit 078fab7

Browse files
authored
Merge pull request #829 from timocov/fix825
Added cache for some FS operations while compile
2 parents b5dad13 + 453bb6a commit 078fab7

File tree

16 files changed

+298
-162
lines changed

16 files changed

+298
-162
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 5.1.0
4+
5+
* [feat: Added cache for some FS operations while compiling - `experimentalFileCaching`](https://github.com/TypeStrong/ts-loader/pull/829) - thanks @timocov!
6+
37
## 5.0.0
48

59
* [feat: Fixed issue with incorrect output path for declaration files](https://github.com/TypeStrong/ts-loader/pull/822) - thanks @JonWallsten! **BREAKING CHANGE**

README.md

+6-12
Original file line numberDiff line numberDiff line change
@@ -543,20 +543,14 @@ Extending `tsconfig.json`:
543543

544544
Note that changes in the extending file while not be respected by `ts-loader`. Its purpose is to satisfy the code editor.
545545

546-
### `LoaderOptionsPlugin`
546+
### experimentalFileCaching _(boolean) (default=false)_
547547

548-
[There's a known "gotcha"](https://github.com/TypeStrong/ts-loader/issues/283) if you are using webpack 2 with the `LoaderOptionsPlugin`. If you are faced with the `Cannot read property 'unsafeCache' of undefined` error then you probably need to supply a `resolve` object as below: (Thanks @jeffijoe!)
548+
By default whenever the TypeScript compiler needs to check that a file/directory exists or resolve symlinks it makes syscalls.
549+
It does not cache the result of these operations and this may result in many syscalls with the same arguments ([see comment](https://github.com/TypeStrong/ts-loader/issues/825#issue-354725524) with example).
550+
In some cases it may produce performance degradation.
549551

550-
```js
551-
new LoaderOptionsPlugin({
552-
debug: false,
553-
options: {
554-
resolve: {
555-
extensions: [".ts", ".tsx", ".js"]
556-
}
557-
}
558-
});
559-
```
552+
This flag enables caching for some FS-functions like `fileExists`, `realpath` and `directoryExists` for TypeScript compiler.
553+
Note that caches are cleared between compilations.
560554

561555
### Usage with Webpack watch
562556

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ts-loader",
3-
"version": "5.0.0",
3+
"version": "5.1.0",
44
"description": "TypeScript loader for webpack",
55
"main": "index.js",
66
"types": "dist/types/index.d.ts",

src/index.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,8 @@ const validLoaderOptions: ValidLoaderOptions[] = [
157157
'getCustomTransformers',
158158
'reportFiles',
159159
'experimentalWatchApi',
160-
'allowTsInNodeModules'
160+
'allowTsInNodeModules',
161+
'experimentalFileCaching'
161162
];
162163

163164
/**
@@ -210,7 +211,8 @@ function makeLoaderOptions(instanceName: string, loaderOptions: LoaderOptions) {
210211
reportFiles: [],
211212
// When the watch API usage stabilises look to remove this option and make watch usage the default behaviour when available
212213
experimentalWatchApi: false,
213-
allowTsInNodeModules: false
214+
allowTsInNodeModules: false,
215+
experimentalFileCaching: false
214216
} as Partial<LoaderOptions>,
215217
loaderOptions
216218
);

src/instances.ts

+18-7
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,12 @@ function successfulTypeScriptInstance(
247247
colors
248248
});
249249

250+
if (!loader._compiler.hooks) {
251+
throw new Error(
252+
"You may be using an old version of webpack; please check you're using at least version 4"
253+
);
254+
}
255+
250256
if (loaderOptions.experimentalWatchApi && compiler.createWatchProgram) {
251257
log.logInfo('Using watch api');
252258

@@ -266,17 +272,22 @@ function successfulTypeScriptInstance(
266272
.getProgram()
267273
.getProgram();
268274
} else {
269-
const servicesHost = makeServicesHost(scriptRegex, log, loader, instance);
275+
const servicesHost = makeServicesHost(
276+
scriptRegex,
277+
log,
278+
loader,
279+
instance,
280+
loaderOptions.experimentalFileCaching
281+
);
282+
270283
instance.languageService = compiler.createLanguageService(
271-
servicesHost,
284+
servicesHost.servicesHost,
272285
compiler.createDocumentRegistry()
273286
);
274-
}
275287

276-
if (!loader._compiler.hooks) {
277-
throw new Error(
278-
"You may be using an old version of webpack; please check you're using at least version 4"
279-
);
288+
if (servicesHost.clearCache !== null) {
289+
loader._compiler.hooks.watchRun.tap('ts-loader', servicesHost.clearCache);
290+
}
280291
}
281292

282293
loader._compiler.hooks.afterCompile.tapAsync(

src/interfaces.ts

+1
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ export interface LoaderOptions {
303303
| (() => typescript.CustomTransformers | undefined);
304304
experimentalWatchApi: boolean;
305305
allowTsInNodeModules: boolean;
306+
experimentalFileCaching: boolean;
306307
}
307308

308309
export interface TSFile {

src/servicesHost.ts

+66-7
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,23 @@ import {
1414
Webpack
1515
} from './interfaces';
1616

17+
export type Action = () => void;
18+
19+
export interface ServiceHostWhichMayBeCacheable {
20+
servicesHost: typescript.LanguageServiceHost;
21+
clearCache: Action | null;
22+
}
23+
1724
/**
1825
* Create the TypeScript language service
1926
*/
2027
export function makeServicesHost(
2128
scriptRegex: RegExp,
2229
log: logger.Logger,
2330
loader: Webpack,
24-
instance: TSInstance
25-
) {
31+
instance: TSInstance,
32+
enableFileCaching: boolean
33+
): ServiceHostWhichMayBeCacheable {
2634
const {
2735
compiler,
2836
compilerOptions,
@@ -52,9 +60,12 @@ export function makeServicesHost(
5260
const moduleResolutionHost: ModuleResolutionHost = {
5361
fileExists,
5462
readFile: readFileWithFallback,
55-
realpath: compiler.sys.realpath
63+
realpath: compiler.sys.realpath,
64+
directoryExists: compiler.sys.directoryExists
5665
};
5766

67+
const clearCache = enableFileCaching ? addCache(moduleResolutionHost) : null;
68+
5869
// loader.context seems to work fine on Linux / Mac regardless causes problems for @types resolution on Windows for TypeScript < 2.3
5970
const getCurrentDirectory = () => loader.context;
6071

@@ -97,13 +108,15 @@ export function makeServicesHost(
97108
/**
98109
* For @types expansion, these two functions are needed.
99110
*/
100-
directoryExists: compiler.sys.directoryExists,
111+
directoryExists: moduleResolutionHost.directoryExists,
101112

102113
useCaseSensitiveFileNames: () => compiler.sys.useCaseSensitiveFileNames,
103114

115+
realpath: moduleResolutionHost.realpath,
116+
104117
// The following three methods are necessary for @types resolution from TS 2.4.1 onwards see: https://github.com/Microsoft/TypeScript/issues/16772
105-
fileExists: compiler.sys.fileExists,
106-
readFile: compiler.sys.readFile,
118+
fileExists: moduleResolutionHost.fileExists,
119+
readFile: moduleResolutionHost.readFile,
107120
readDirectory: compiler.sys.readDirectory,
108121

109122
getCurrentDirectory,
@@ -137,7 +150,7 @@ export function makeServicesHost(
137150
getCustomTransformers: () => instance.transformers
138151
};
139152

140-
return servicesHost;
153+
return { servicesHost, clearCache };
141154
}
142155

143156
/**
@@ -509,3 +522,49 @@ function populateDependencyGraphs(
509522
] = true;
510523
});
511524
}
525+
526+
type CacheableFunction = Extract<
527+
keyof typescript.ModuleResolutionHost,
528+
'fileExists' | 'directoryExists' | 'realpath'
529+
>;
530+
const cacheableFunctions: CacheableFunction[] = [
531+
'fileExists',
532+
'directoryExists',
533+
'realpath'
534+
];
535+
536+
function addCache(servicesHost: typescript.ModuleResolutionHost) {
537+
const clearCacheFunctions: Action[] = [];
538+
539+
cacheableFunctions.forEach((functionToCache: CacheableFunction) => {
540+
const originalFunction = servicesHost[functionToCache];
541+
if (originalFunction !== undefined) {
542+
const cache = createCache<ReturnType<typeof originalFunction>>(originalFunction);
543+
servicesHost[
544+
functionToCache
545+
] = cache.getCached as typescript.ModuleResolutionHost[CacheableFunction];
546+
clearCacheFunctions.push(cache.clear);
547+
}
548+
});
549+
550+
return () => clearCacheFunctions.forEach(clear => clear());
551+
}
552+
553+
function createCache<TOut>(originalFunction: (arg: string) => TOut) {
554+
const cache = new Map<string, TOut>();
555+
return {
556+
clear: () => {
557+
cache.clear();
558+
},
559+
getCached: (arg: string) => {
560+
let res = cache.get(arg);
561+
if (res !== undefined) {
562+
return res;
563+
}
564+
565+
res = originalFunction(arg);
566+
cache.set(arg, res);
567+
return res;
568+
}
569+
};
570+
}

test/.prettierrc

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"overrides": [
3+
{
4+
"files": "*.js",
5+
"options": {
6+
"singleQuote": true
7+
}
8+
}
9+
]
10+
}

0 commit comments

Comments
 (0)