Skip to content

Commit f0abae5

Browse files
clydinalan-agius4
authored andcommitted
fix(@ngtools/webpack): improve resource loader caching
This change reduces the complexity of the component resource loader caching as well as moves the caching check earlier in the workflow to further minimize the amount of processing needed.
1 parent a5bf224 commit f0abae5

File tree

1 file changed

+61
-70
lines changed

1 file changed

+61
-70
lines changed

packages/ngtools/webpack/src/resource_loader.ts

Lines changed: 61 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -22,39 +22,39 @@ const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
2222
interface CompilationOutput {
2323
outputName: string;
2424
source: string;
25+
success: boolean;
2526
}
2627

2728
export class WebpackResourceLoader {
2829
private _parentCompilation: any;
29-
private _context = '';
3030
private _fileDependencies = new Map<string, Set<string>>();
3131
private _reverseDependencies = new Map<string, Set<string>>();
32-
private _cachedSources = new Map<string, string>();
33-
private _cachedEvaluatedSources = new Map<string, RawSource>();
3432

35-
public changedFiles?: Iterable<string>;
33+
private cache = new Map<string, string>();
34+
private modifiedResources = new Set<string>();
3635

37-
update(parentCompilation: import('webpack').compilation.Compilation, changedFiles?: Iterable<string>) {
36+
update(
37+
parentCompilation: import('webpack').compilation.Compilation,
38+
changedFiles?: Iterable<string>,
39+
) {
3840
this._parentCompilation = parentCompilation;
39-
this._context = parentCompilation.context;
4041

41-
// Update changed file list
42-
this.changedFiles = changedFiles;
42+
// Update resource cache and modified resources
43+
this.modifiedResources.clear();
44+
if (changedFiles) {
45+
for (const changedFile of changedFiles) {
46+
for (const affectedResource of this.getAffectedResources(changedFile)) {
47+
this.cache.delete(normalizePath(affectedResource));
48+
this.modifiedResources.add(affectedResource);
49+
}
50+
}
51+
} else {
52+
this.cache.clear();
53+
}
4354
}
4455

4556
getModifiedResourceFiles() {
46-
const modifiedResources = new Set<string>();
47-
if (!this.changedFiles) {
48-
return modifiedResources;
49-
}
50-
51-
for (const changedFile of this.changedFiles) {
52-
this.getAffectedResources(
53-
changedFile,
54-
).forEach((affected: string) => modifiedResources.add(affected));
55-
}
56-
57-
return modifiedResources;
57+
return this.modifiedResources;
5858
}
5959

6060
getResourceDependencies(filePath: string) {
@@ -70,7 +70,6 @@ export class WebpackResourceLoader {
7070
}
7171

7272
private async _compile(filePath: string): Promise<CompilationOutput> {
73-
7473
if (!this._parentCompilation) {
7574
throw new Error('WebpackResourceLoader cannot be used without parentCompilation');
7675
}
@@ -83,39 +82,25 @@ export class WebpackResourceLoader {
8382
}
8483

8584
const outputOptions = { filename: filePath };
86-
const relativePath = path.relative(this._context || '', filePath);
85+
const context = this._parentCompilation.context;
86+
const relativePath = path.relative(context || '', filePath);
8787
const childCompiler = this._parentCompilation.createChildCompiler(relativePath, outputOptions);
88-
childCompiler.context = this._context;
88+
childCompiler.context = context;
8989

9090
new NodeTemplatePlugin(outputOptions).apply(childCompiler);
9191
new NodeTargetPlugin().apply(childCompiler);
92-
new SingleEntryPlugin(this._context, filePath).apply(childCompiler);
92+
new SingleEntryPlugin(context, filePath).apply(childCompiler);
9393
new LibraryTemplatePlugin('resource', 'var').apply(childCompiler);
9494

9595
childCompiler.hooks.thisCompilation.tap('ngtools-webpack', (compilation: any) => {
96-
compilation.hooks.additionalAssets.tapAsync('ngtools-webpack',
97-
(callback: (err?: Error) => void) => {
98-
if (this._cachedEvaluatedSources.has(compilation.fullHash)) {
99-
const cachedEvaluatedSource = this._cachedEvaluatedSources.get(compilation.fullHash);
100-
compilation.assets[filePath] = cachedEvaluatedSource;
101-
callback();
102-
96+
compilation.hooks.additionalAssets.tapPromise('ngtools-webpack', async () => {
97+
const asset = compilation.assets[filePath];
98+
if (!asset) {
10399
return;
104100
}
105101

106-
const asset = compilation.assets[filePath];
107-
if (asset) {
108-
this._evaluate({ outputName: filePath, source: asset.source() })
109-
.then(output => {
110-
const evaluatedSource = new RawSource(output);
111-
this._cachedEvaluatedSources.set(compilation.fullHash, evaluatedSource);
112-
compilation.assets[filePath] = evaluatedSource;
113-
callback();
114-
})
115-
.catch(err => callback(err));
116-
} else {
117-
callback();
118-
}
102+
const output = await this._evaluate(filePath, asset.source());
103+
compilation.assets[filePath] = new RawSource(output);
119104
});
120105
});
121106

@@ -139,13 +124,13 @@ export class WebpackResourceLoader {
139124
this._parentCompilation.errors.push(...errors);
140125
}
141126

142-
Object.keys(childCompilation.assets).forEach(assetName => {
127+
Object.keys(childCompilation.assets).forEach((assetName) => {
143128
// Add all new assets to the parent compilation, with the exception of
144129
// the file we're loading and its sourcemap.
145130
if (
146-
assetName !== filePath
147-
&& assetName !== `${filePath}.map`
148-
&& this._parentCompilation.assets[assetName] == undefined
131+
assetName !== filePath &&
132+
assetName !== `${filePath}.map` &&
133+
this._parentCompilation.assets[assetName] == undefined
149134
) {
150135
this._parentCompilation.assets[assetName] = childCompilation.assets[assetName];
151136
}
@@ -163,34 +148,40 @@ export class WebpackResourceLoader {
163148
}
164149
}
165150

166-
const compilationHash = childCompilation.fullHash;
167-
const maybeSource = this._cachedSources.get(compilationHash);
168-
if (maybeSource) {
169-
return { outputName: filePath, source: maybeSource };
170-
} else {
171-
const source = childCompilation.assets[filePath].source();
172-
this._cachedSources.set(compilationHash, source);
151+
const finalOutput = childCompilation.assets[filePath]?.source();
173152

174-
return { outputName: filePath, source };
175-
}
153+
return { outputName: filePath, source: finalOutput ?? '', success: !errors?.length };
176154
}
177155

178-
private async _evaluate({ outputName, source }: CompilationOutput): Promise<string> {
179-
// Evaluate code
180-
const context: { resource?: string | { default?: string } } = {};
181-
vm.runInNewContext(source, context, { filename: outputName });
156+
private async _evaluate(filename: string, source: string): Promise<string> {
157+
// Evaluate code
158+
const context: { resource?: string | { default?: string } } = {};
159+
vm.runInNewContext(source, context, { filename });
182160

183-
if (typeof context.resource === 'string') {
184-
return context.resource;
185-
} else if (typeof context.resource?.default === 'string') {
186-
return context.resource.default;
187-
}
161+
if (typeof context.resource === 'string') {
162+
return context.resource;
163+
} else if (typeof context.resource?.default === 'string') {
164+
return context.resource.default;
165+
}
188166

189-
throw new Error(`The loader "${outputName}" didn't return a string.`);
167+
throw new Error(`The loader "${filename}" didn't return a string.`);
190168
}
191169

192-
get(filePath: string): Promise<string> {
193-
return this._compile(filePath)
194-
.then((result: CompilationOutput) => result.source);
170+
async get(filePath: string): Promise<string> {
171+
const normalizedFile = normalizePath(filePath);
172+
let data = this.cache.get(normalizedFile);
173+
174+
if (data === undefined) {
175+
// cache miss so compile resource
176+
const compilationResult = await this._compile(filePath);
177+
data = compilationResult.source;
178+
179+
// Only cache if compilation was successful
180+
if (compilationResult.success) {
181+
this.cache.set(normalizedFile, data);
182+
}
183+
}
184+
185+
return data;
195186
}
196187
}

0 commit comments

Comments
 (0)