Skip to content

Commit d15d92f

Browse files
committed
feat: use the processAssets webpack api to add assets
1 parent 31739ec commit d15d92f

File tree

7 files changed

+829
-990
lines changed

7 files changed

+829
-990
lines changed

.node-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
10.18.0
1+
10.13.0

package-lock.json

Lines changed: 703 additions & 925 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,15 @@
5151
"diffable-html": "4.0.0",
5252
"eslint": "^6.8.0",
5353
"fs-extra": "^8.1.0",
54-
"html-webpack-plugin": "4.3.0",
54+
"html-webpack-plugin": "^5.0.0-alpha.7",
5555
"image-size": "0.8.3",
5656
"nyc": "^15.0.0",
5757
"prettier": "1.19.1",
5858
"standard-version": "8.0.2",
5959
"typescript": "3.7.4",
6060
"webpack": "^5.2.0",
61-
"webpack-cli": "^3.3.10",
62-
"webpack-dev-server": "^3.10.1",
61+
"webpack-cli": "4.1.0",
62+
"webpack-dev-server": "3.11.0",
6363
"webpack-merge": "^4.2.2"
6464
},
6565
"dependencies": {
@@ -86,6 +86,6 @@
8686
}
8787
},
8888
"engines": {
89-
"node": ">=8.9.4"
89+
"node": ">=10.13.0"
9090
}
9191
}

src/compiler.d.ts

Lines changed: 0 additions & 6 deletions
This file was deleted.

src/compiler.js

Lines changed: 70 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const path = require('path');
22
const findCacheDir = require('find-cache-dir');
33
const entryPlugin = require('webpack/lib/EntryPlugin');
4+
const Compilation = require('webpack').Compilation;
45

56
module.exports.run = (faviconOptions, context, compilation) => {
67
const {
@@ -12,7 +13,7 @@ module.exports.run = (faviconOptions, context, compilation) => {
1213
outputPath
1314
} = faviconOptions;
1415
// The entry file is just an empty helper
15-
const filename = '[fullhash]';
16+
const filename = 'favicon-[fullhash]';
1617
const publicPath = getPublicPath(
1718
publicPathOption,
1819
compilation.outputOptions.publicPath
@@ -21,13 +22,16 @@ module.exports.run = (faviconOptions, context, compilation) => {
2122
// Create an additional child compiler which takes the template
2223
// and turns it into an Node.JS html factory.
2324
// This allows us to use loaders during the compilation
24-
const compiler = compilation.createChildCompiler('favicons-webpack-plugin', {
25-
filename,
26-
publicPath,
27-
libraryTarget: 'var',
28-
iife: false
29-
});
30-
compiler.context = context;
25+
const childCompiler = compilation.createChildCompiler(
26+
'favicons-webpack-plugin',
27+
{
28+
filename,
29+
publicPath,
30+
libraryTarget: 'var',
31+
iife: false
32+
}
33+
);
34+
childCompiler.context = context;
3135

3236
const cacheDirectory =
3337
cache &&
@@ -53,41 +57,71 @@ module.exports.run = (faviconOptions, context, compilation) => {
5357
`!${cacheLoader}!${faviconsLoader}!${logo}`,
5458
path.basename(logo)
5559
);
56-
logoCompilationEntry.apply(compiler);
60+
logoCompilationEntry.apply(childCompiler);
5761

58-
// Compile and return a promise
59-
return new Promise((resolve, reject) => {
60-
compiler.runAsChild((err, [chunk] = [], { hash, errors = [] } = {}) => {
62+
/** @type {Promise<{ tags: Array<string>, assets: Array<{name: string, contents: string}> }>} */
63+
const compiledFavicons = new Promise((resolve, reject) => {
64+
/** @type {Array<import('webpack').sources.CachedSource>} extracted webpack assets */
65+
const extractedAssets = [];
66+
childCompiler.hooks.thisCompilation.tap(
67+
'FaviconsWebpackPlugin',
68+
compilation => {
69+
compilation.hooks.processAssets.tap(
70+
{
71+
name: 'FaviconsWebpackPlugin',
72+
stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS
73+
},
74+
assets => {
75+
Object.keys(assets)
76+
.filter(temporaryTemplateName =>
77+
temporaryTemplateName.startsWith('favicon-')
78+
)
79+
.forEach(temporaryTemplateName => {
80+
if (assets[temporaryTemplateName]) {
81+
extractedAssets.push(assets[temporaryTemplateName]);
82+
compilation.deleteAsset(temporaryTemplateName);
83+
}
84+
});
85+
if (extractedAssets.length > 1) {
86+
reject('Unexpected multiplication of favicon generations');
87+
88+
return;
89+
}
90+
const extractedAsset = extractedAssets[0];
91+
if (extractedAsset) {
92+
/**
93+
* @type {{ tags: Array<string>, assets: Array<{name: string, contents: string}> }}
94+
* The extracted result of the favicon-webpack-plugin loader
95+
*/
96+
const result = eval(extractedAsset.source().toString()); // eslint-disable-line
97+
if (result && result.assets) {
98+
resolve(result);
99+
}
100+
}
101+
}
102+
);
103+
}
104+
);
105+
childCompiler.runAsChild((err, result, { errors = [] } = {}) => {
61106
if (err || errors.length) {
62107
return reject(err || errors[0].error);
63108
}
64-
65-
// Replace [hash] placeholders in filename
66-
const result = extractAssetFromCompilation(
67-
compilation,
68-
compilation.getAssetPath(filename, { hash, chunk })
69-
);
70-
71-
for (const { name, contents } of result.assets) {
72-
const binaryContents = Buffer.from(contents, 'base64');
73-
compilation.assets[name] = {
74-
source: () => binaryContents,
75-
size: () => binaryContents.length
76-
};
77-
}
78-
79-
return resolve(result.tags);
109+
// If no error occured and this promise was not resolved inside the `processAssets` hook
110+
// reject the promise although it's unclear why it failed:
111+
reject('Could not extract assets');
80112
});
81113
});
82-
};
83-
84-
function extractAssetFromCompilation(compilation, assetPath) {
85-
const content = compilation.assets[assetPath].source();
86-
compilation.deleteAsset(assetPath);
87114

88-
/* eslint-disable no-eval */
89-
return eval(content);
90-
}
115+
return compiledFavicons.then(faviconCompilationResult => {
116+
return {
117+
assets: faviconCompilationResult.assets.map(({ name, contents }) => ({
118+
name,
119+
contents: Buffer.from(contents, 'base64')
120+
})),
121+
tags: faviconCompilationResult.tags
122+
};
123+
});
124+
};
91125

92126
/**
93127
* faviconsPublicPath always wins over compilerPublicPath

src/index.js

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ const path = require('path');
66
const child = require('./compiler');
77
const crypto = require('crypto');
88
const Oracle = require('./oracle');
9+
const Compilation = require('webpack').Compilation;
10+
const RawSource = require('webpack').sources.RawSource;
911

12+
/** @type {WeakMap<any, Promise<{tags: string[], assets: Array<{name: string, contents: Buffer | string}>}>>} */
1013
const faviconCompilations = new WeakMap();
1114

1215
class FaviconsWebpackPlugin {
@@ -102,9 +105,9 @@ class FaviconsWebpackPlugin {
102105
}
103106

104107
faviconCompilation
105-
.then(tags => {
108+
.then(faviconCompilation => {
106109
htmlPluginData.assetTags.meta.push(
107-
...tags
110+
...faviconCompilation.tags
108111
.map(tag => parse5.parseFragment(tag).childNodes[0])
109112
.map(({ tagName, attrs }) => ({
110113
tagName,
@@ -131,6 +134,25 @@ class FaviconsWebpackPlugin {
131134
}
132135
);
133136

137+
compiler.hooks.thisCompilation.tap('FaviconsWebpackPlugin', compilation => {
138+
compilation.hooks.processAssets.tapPromise(
139+
{
140+
name: 'FaviconsWebpackPlugin',
141+
stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS
142+
},
143+
async () => {
144+
const faviconCompilation = faviconCompilations.get(compilation);
145+
if (!faviconCompilation) {
146+
return;
147+
}
148+
const faviconAssets = (await faviconCompilation).assets;
149+
faviconAssets.forEach(({ name, contents }) => {
150+
compilation.emitAsset(name, new RawSource(contents, false));
151+
});
152+
}
153+
);
154+
});
155+
134156
// Make sure that the build waits for the favicon generation to complete
135157
compiler.hooks.afterCompile.tapPromise(
136158
'FaviconsWebpackPlugin',
@@ -157,6 +179,8 @@ class FaviconsWebpackPlugin {
157179
* The light mode will only add a favicon
158180
* this is very fast but also very limited
159181
* it is the default mode for development
182+
*
183+
* @returns {Promise<{tags: string[], assets: Array<{name: string, contents: Buffer | string}>}>}
160184
*/
161185
generateFaviconsLight(compilation) {
162186
return new Promise((resolve, reject) => {
@@ -180,31 +204,34 @@ class FaviconsWebpackPlugin {
180204
.createHash('sha256')
181205
.update(content.toString('utf8'))
182206
.digest('hex');
183-
const outputPath = compilation.getAssetPath(
184-
this.options.prefix,
185-
{
186-
hash,
187-
chunk: {
188-
hash
189-
}
207+
const outputPath = compilation.getAssetPath(this.options.prefix, {
208+
hash,
209+
chunk: {
210+
hash
190211
}
191-
);
212+
});
192213
const logoOutputPath = `${outputPath +
193214
(outputPath.substr(-1) === '/' ? '' : '/')}favicon${faviconExt}`;
194-
compilation.assets[logoOutputPath] = {
195-
source: () => content,
196-
size: () => content.length
197-
};
198-
resolve([`<link rel="icon" href="${publicPath}${logoOutputPath}">`]);
215+
resolve({
216+
assets: [
217+
{
218+
name: logoOutputPath,
219+
contents: content
220+
}
221+
],
222+
tags: [`<link rel="icon" href="${publicPath}${logoOutputPath}">`]
223+
});
199224
}
200225
);
201226
});
202227
}
203228

204229
/**
205-
* The webapp mode will add a variety of icons
230+
* The webapp mode will add a variety of icons
206231
* this is not as fast as the light mode but
207232
* supports all common browsers and devices
233+
*
234+
* @returns {Promise<{tags: string[], assets: Array<{name: string, contents: Buffer | string}>}>}
208235
*/
209236
generateFaviconsWebapp(compilation) {
210237
// Generate favicons using the npm favicons library

test/failure.test.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ test('should fail gracefully if the image stream is empty', async t => {
3535
plugins: [new FaviconsWebpackPlugin({ logo: empty })]
3636
});
3737
} catch (err) {
38-
t.is(err.message, 'Invalid image buffer');
38+
const errorMessage = err.message
39+
.split('\n')
40+
.find(errorLine => errorLine.startsWith('Error:'));
41+
t.is(errorMessage, 'Error: Invalid image buffer');
3942
}
4043
});
4144

@@ -50,7 +53,10 @@ test('should fail gracefully if logo is not a valid image file', async t => {
5053
plugins: [new FaviconsWebpackPlugin({ logo: invalid })]
5154
});
5255
} catch (err) {
53-
t.is(err.message, 'Invalid image buffer');
56+
const errorMessage = err.message
57+
.split('\n')
58+
.find(errorLine => errorLine.startsWith('Error:'));
59+
t.is(errorMessage, 'Error: Invalid image buffer');
5460
}
5561
});
5662

0 commit comments

Comments
 (0)