Skip to content

Commit 3394c31

Browse files
committed
fix: favicon plugin causes a crash without an error explaining if no link tag is included
1 parent 770e559 commit 3394c31

8 files changed

Lines changed: 127 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Change log
22

3+
## 3.4.3 (2023-12-18)
4+
5+
- fix: favicon plugin causes a crash without an error explaining if no link tag is included
6+
37
## 3.4.2 (2023-12-08)
48

59
- fix: watching changes in template function imported in JS

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "html-bundler-webpack-plugin",
3-
"version": "3.4.2",
3+
"version": "3.4.3",
44
"description": "HTML bundler plugin for webpack handles a template as an entry point, extracts CSS and JS from their sources referenced in HTML, supports template engines like Eta, EJS, Handlebars, Nunjucks.",
55
"keywords": [
66
"html",

plugins/favicons-bundler-plugin/index.js

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
const fs = require('fs/promises');
22
const path = require('path');
3+
const { black, blueBright, yellow } = require('ansis');
34
const HtmlBundlerPlugin = require('../../src/');
45
const { favicons, config } = require('favicons');
6+
const { outToConsole } = require('../../src/Common/Helpers');
57

68
class FaviconsBundlerPlugin {
79
pluginName = 'favicons-bundler-plugin';
@@ -117,19 +119,40 @@ class FaviconsBundlerPlugin {
117119
});
118120

119121
// save favicon images on disk
120-
compiler.hooks.afterEmit.tapPromise(pluginName, () => {
121-
if (this.faviconResponse?.images.length > 0) {
122-
const { images } = this.faviconResponse;
123-
const outputPath = HtmlBundlerPlugin.option.getWebpackOutputPath();
124-
const saveDir = path.join(outputPath, this.options.faviconsConfig.path);
125-
126-
return fs
127-
.mkdir(saveDir, { recursive: true })
128-
.then(() =>
129-
Promise.all(images.map(async (image) => await fs.writeFile(path.join(saveDir, image.name), image.contents)))
130-
);
131-
}
132-
});
122+
compiler.hooks.afterEmit.tapPromise(
123+
pluginName,
124+
() =>
125+
new Promise((resolve, reject) => {
126+
if (this.faviconResponse?.images.length > 0) {
127+
const { images } = this.faviconResponse;
128+
const outputPath = HtmlBundlerPlugin.option.getWebpackOutputPath();
129+
const saveDir = path.join(outputPath, this.options.faviconsConfig.path);
130+
131+
return fs
132+
.mkdir(saveDir, { recursive: true })
133+
.then(() =>
134+
Promise.all(
135+
images.map(async (image) => await fs.writeFile(path.join(saveDir, image.name), image.contents))
136+
).then(resolve)
137+
);
138+
} else {
139+
this.warningFaviconNotFound();
140+
resolve();
141+
}
142+
})
143+
);
144+
}
145+
146+
warningFaviconNotFound() {
147+
const header = `\n${black.bgYellow` ${this.pluginName} `}${black.bgAnsi(227)` WARNING `} `;
148+
149+
let warning = `Favicon file is not found!
150+
If the ${blueBright(
151+
this.constructor.name
152+
)} is used, at last one favicon source file should be defined in the template, e.g.:
153+
${yellow`<link rel="icon" href="path/to/source/favicon.png">`}`;
154+
155+
outToConsole(header + warning);
133156
}
134157
}
135158

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>Test</title>
5+
<!-- test: missing favicon in resolvable href if FaviconsBundlerPlugin is used -->
6+
<link rel="icon" />
7+
<link x-href="./mario.png" rel="icon" />
8+
</head>
9+
<body>
10+
<h1>Hello World!</h1>
11+
</body>
12+
</html>
21.3 KB
Loading
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const path = require('path');
2+
const HtmlBundlerPlugin = require('../../../');
3+
const { FaviconsBundlerPlugin } = require('../../../plugins');
4+
5+
module.exports = {
6+
mode: 'production',
7+
//mode: 'development',
8+
9+
output: {
10+
path: path.join(__dirname, 'dist/'),
11+
},
12+
13+
plugins: [
14+
new HtmlBundlerPlugin({
15+
entry: {
16+
index: './src/index.html',
17+
},
18+
}),
19+
// test custom bundler plugin
20+
new FaviconsBundlerPlugin({
21+
enabled: 'auto', // true, false, auto - generate favicons in production mode only
22+
// favicons configuration options, see https://github.com/itgalaxy/favicons#usage
23+
faviconOptions: {
24+
path: '/img/favicons',
25+
appName: 'My App',
26+
icons: {
27+
android: false, // Create Android homescreen icon.
28+
appleIcon: false, // Create Apple touch icons.
29+
appleStartup: false, // Create Apple startup images.
30+
favicons: true, // Create regular favicons.
31+
windows: false, // Create Windows 8 tile icons.
32+
yandex: false, // Create Yandex browser icon.
33+
},
34+
},
35+
}),
36+
],
37+
38+
module: {
39+
rules: [
40+
{
41+
test: /\.(png|jpe?g|ico|svg)$/,
42+
type: 'asset/resource',
43+
generator: {
44+
filename: 'img/[name].[hash:8][ext]',
45+
},
46+
},
47+
],
48+
},
49+
};

test/exception.test.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { exceptionContain, watchExceptionContain } from './utils/helpers';
1+
import { exceptionContain, watchExceptionContain, stdoutContain } from './utils/helpers';
22
import { loadModule } from '../src/Common/FileUtils';
33

44
beforeAll(() => {
@@ -180,3 +180,10 @@ describe('css loader exceptions', () => {
180180
return exceptionContain('msg-exception-css-loader', containString);
181181
});
182182
});
183+
184+
describe('warnings', () => {
185+
test('plugin-favicons', () => {
186+
const containString = `Favicon file is not found`;
187+
return stdoutContain('msg-warning-plugin-favicons-no-href', containString);
188+
});
189+
});

test/utils/helpers.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,23 @@ export const exceptionContain = (relTestCasePath, containString) => {
118118
).rejects.toContain(containString);
119119
};
120120

121+
export const stdoutContain = (relTestCasePath, containString) => {
122+
const stdout = jest.spyOn(process.stdout, 'write').mockImplementation(() => {});
123+
124+
return expect(
125+
compile(PATHS, relTestCasePath, {}).then(() => {
126+
const { calls } = stdout.mock;
127+
let output = calls.length > 0 ? calls[0][0] : '';
128+
output = ansis.strip(output);
129+
130+
stdout.mockClear();
131+
stdout.mockRestore();
132+
133+
return Promise.resolve(output);
134+
})
135+
).resolves.toContain(containString);
136+
};
137+
121138
export const watchExceptionContain = function (relTestCasePath, containString) {
122139
return expect(
123140
watch(PATHS, relTestCasePath, {}, (watching) => {

0 commit comments

Comments
 (0)