diff --git a/.travis.yml b/.travis.yml index 1d47c04..58b185b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,3 @@ language: node_js node_js: - - '14.17.0' \ No newline at end of file + - '18.17.0' \ No newline at end of file diff --git a/index.js b/index.js index 49dade9..7e974bb 100644 --- a/index.js +++ b/index.js @@ -2,12 +2,15 @@ const fs = require('fs'); const path = require('path'); const { sources } = require('webpack'); -const getAttributes = (markup) => markup.match(/([^\r\n\t\f\v= '"]+)(?:=(["'])?((?:.(?!\2?\s+(?:\S+)=|\2))+.)\2?)?/g).slice(1, -1); -const getTagType = (markup) => { if (markup.indexOf('meta') !== -1) { return 'meta'; } return 'link'; }; +const getAttributes = (markup) => + markup.match(/([^\r\n\t\f\v= '"]+)(?:=(["'])?((?:.(?!\2?\s+(?:\S+)=|\2))+.)\2?)?/g).slice(1, -1); + +const getTagType = (markup) => (markup.includes('meta') ? 'meta' : 'link'); class WebpackFavicons { constructor(options, callback) { - this.options = Object.assign({ + // Setting default options, user options will override + this.options = { src: false, path: '', appName: null, // Your application's name. `string` @@ -15,43 +18,44 @@ class WebpackFavicons { appDescription: null, // Your application's description. `string` developerName: null, // Your (or your developer's) name. `string` developerURL: null, // Your (or your developer's) URL. `string` - dir: "auto", // Primary text direction for name, short_name, and description - lang: "en-US", // Primary language for name and short_name - background: "#fff", // Background colour for flattened icons. `string` - theme_color: "#fff", // Theme color user for example in Android's task switcher. `string` - appleStatusBarStyle: "black-translucent", // Style for Apple status bar: "black-translucent", "default", "black". `string` - display: "standalone", // Preferred display mode: "fullscreen", "standalone", "minimal-ui" or "browser". `string` - orientation: "any", // Default orientation: "any", "natural", "portrait" or "landscape". `string` - scope: '', // set of URLs that the browser considers within your app - start_url: "/?homescreen=1", // Start URL when launching the application from a device. `string` - version: "1.0", // Your application's version string. `string` + dir: 'auto', // Primary text direction for name, short_name, and description + lang: 'en-US', // Primary language for name and short_name + background: '#fff', // Background color for flattened icons. `string` + theme_color: '#fff', // Theme color used in Android's task switcher. `string` + appleStatusBarStyle: 'black-translucent', // Style for Apple status bar: "black-translucent", "default", "black". `string` + display: 'standalone', // Preferred display mode: "fullscreen", "standalone", "minimal-ui" or "browser". `string` + orientation: 'any', // Default orientation: "any", "natural", "portrait" or "landscape". `string` + scope: '', // Set of URLs that the browser considers within your app + start_url: '/?homescreen=1', // Start URL when launching the application from a device. `string` + version: '1.0', // Your application's version string. `string` logging: false, // Print logs to console? `boolean` - pixel_art: false, // Keeps pixels "sharp" when scaling up, for pixel art. Only supported in offline mode. + pixel_art: false, // Keeps pixels "sharp" when scaling up, for pixel art. Only supported in offline mode. loadManifestWithCredentials: false, // Browsers don't send cookies when fetching a manifest, enable this to fix that. `boolean` - icons: { favicons: true } - }, options); - - this.options.icons = Object.assign({ - android: false, // Create Android homescreen icon. `boolean` or `{ offset, background, mask, overlayGlow, overlayShadow }` or an array of sources - appleIcon: false, // Create Apple touch icons. `boolean` or `{ offset, background, mask, overlayGlow, overlayShadow }` or an array of sources - appleStartup: false, // Create Apple startup images. `boolean` or `{ offset, background, mask, overlayGlow, overlayShadow }` or an array of sources - coast: false, // Create Opera Coast icon. `boolean` or `{ offset, background, mask, overlayGlow, overlayShadow }` or an array of sources - favicons: true, // Create regular favicons. `boolean` or `{ offset, background, mask, overlayGlow, overlayShadow }` or an array of sources - firefox: false, // Create Firefox OS icons. `boolean` or `{ offset, background, mask, overlayGlow, overlayShadow }` or an array of sources - windows: false, // Create Windows 8 tile icons. `boolean` or `{ offset, background, mask, overlayGlow, overlayShadow }` or an array of sources - yandex: false // Create Yandex browser icon. `boolean` or `{ offset, background, mask, overlayGlow, overlayShadow }` or an array of sources - }, this.options.icons); + icons: { favicons: true }, // Specify which icons to generate + ...options, + }; + + // Merging user-specified icon options + this.options.icons = { + android: false, // Create Android homescreen icon. `boolean` or `{ offset, background, mask, overlayGlow, overlayShadow }` or an array of sources + appleIcon: false, // Create Apple touch icons. `boolean` or `{ offset, background, mask, overlayGlow, overlayShadow }` or an array of sources + appleStartup: false, // Create Apple startup images. `boolean` or `{ offset, background, mask, overlayGlow, overlayShadow }` or an array of sources + coast: false, // Create Opera Coast icon. `boolean` or `{ offset, background, mask, overlayGlow, overlayShadow }` or an array of sources + favicons: true, // Create regular favicons. `boolean` or `{ offset, background, mask, overlayGlow, overlayShadow }` or an array of sources + firefox: false, // Create Firefox OS icons. `boolean` or `{ offset, background, mask, overlayGlow, overlayShadow }` or an array of sources + windows: false, // Create Windows 8 tile icons. `boolean` or `{ offset, background, mask, overlayGlow, overlayShadow }` or an array of sources + yandex: false, // Create Yandex browser icon. `boolean` or `{ offset, background, mask, overlayGlow, overlayShadow }` or an array of sources + ...this.options.icons, + }; this.callback = callback; } apply(compiler) { - let { output } = compiler.options; + const { output } = compiler.options; - /* Ensure our ouput directory exists */ - if (!fs.existsSync(output.path)){ - fs.mkdirSync(output.path, { recursive: true }); - } + // Ensure the output directory exists + if (!fs.existsSync(output.path)) fs.mkdirSync(output.path, { recursive: true }); if (this.options.src && output.path) { // HTML link tag injections @@ -59,124 +63,110 @@ class WebpackFavicons { compilation.hooks.processAssets.tapPromise( { name: 'WebpackFavicons', - stage: compilation.PROCESS_ASSETS_STAGE_ADDITIONAL, // see below for more stages - additionalAssets: false + stage: compilation.PROCESS_ASSETS_STAGE_ADDITIONAL, + additionalAssets: false, }, - (assets) => import('favicons').then( - (module) => module.favicons(this.options.src, this.options).then( - (response) => { - // Check/Run plugin callback - if (typeof this.callback === 'function') { - response = Object.assign({ ...response }, this.callback(response)); - } - - //////// if HtmlWebpackPlugin found ////////// - try { - require('html-webpack-plugin').getCompilationHooks(compilation).alterAssetTags.tapAsync( - { name: 'WebpackFavicons' }, - (data, callback) => { - // Loop over favicon's response HTML tags - Object.keys(response.html).map((i) => { - // Collect HTML attributes into key|value object - - let attrs = getAttributes(response.html[i]); - let type = getTagType(response.html[i]); - - const attributes = {}; - - Object.keys(attrs).map((j) => { - const parts = attrs[j].split('='); - const key = parts[0]; - const value = parts[1].slice(1, -1); - - attributes[key] = value; - - if ( - key === 'href' - && compiler.options.output.publicPath !== 'auto' - ) { - attributes[key] = path.normalize(`${compiler.options.output.publicPath}/${value}`).replace(/\\/g, '/'); - } - }); - - // Push HTML object data into HtmlWebpackPlugin meta template - data.assetTags.meta.push({ - tagName: type, - voidTag: true, - meta: { plugin: 'WebpackFavicons' }, - attributes - }); - }); - - // Run required callback with altered data - callback(null, data); - } - ); - } catch (err) { console.error(err); } - - //////// if CopyWebpackPlugin found ////////// - Object.keys(assets).map((i) => { - // Only alter .html files - if (i.indexOf('.html') === -1) { return false; } - - // Prepend output.plublicPath to favicon href paths by hand - if (compiler.options.output.publicPath !== 'auto') { - response.html = Object.keys(response.html).map( - (i) => response.html[i].replace( - /href="(.*?)"/g, - (match, p1, string) => `href="${path.normalize(`${compiler.options.output.publicPath}/${p1}`)}"`.replace(/\\/g, '/') - ) - ); - } - - // Inject favicon into .html document(s) - let HTML = compilation.getAsset(i).source.source().toString(); - compilation.updateAsset( - i, - new sources.RawSource( - HTML.replace( - /([\s\S]*?)<\/head>/, - `$1\r ${response.html.join('\r ')}\r ` - ) - ) - ); - }); - - // Adds generated images to build - if (response.images) { - Object.keys(response.images).map((i) => { - let image = response.images[i]; - assets[path.normalize(`/${this.options.path}/${image.name}`)] = { - source: () => image.contents, - size: () => image.contents.length - }; - }); - } - - // Adds manifest json and xml files to build - if (response.files) { - Object.keys(response.files).map((i) => { - let file = response.files[i]; - assets[path.normalize(`/${this.options.path}/${file.name}`)] = { - source: () => file.contents, - size: () => file.contents.length - }; - }); - } - - return assets; - }, - (error) => { - // If we have parsing error lets stop - console.error(error.message); - return; + async (assets) => { + this.ticketTest += 1; + try { + // Generate favicons using the `favicons` module + const { favicons } = await import('favicons'); + let response = await favicons(this.options.src, this.options); + + // Check/Run plugin callback + if (typeof this.callback === 'function') { + response = { ...response, ...this.callback(response) }; } + + // Inject generated favicon tags into HTML files + this.injectIntoHtml(compilation, response, assets); + + // Adds generated images, JSON, and XML files to the build + this.addAssets(response, assets); + } catch (error) { + // If there is a parsing error, stop execution + console.error(error.message); + } + + return assets; + } + ); + }); + } + } + + // Function to handle injection into HTML files + injectIntoHtml = (compilation, response, assets) => { + // If HtmlWebpackPlugin is found, inject link tags + require('html-webpack-plugin').getCompilationHooks(compilation).alterAssetTags.tapAsync( + { name: 'WebpackFavicons' }, + (data, callback) => { + // Loop over each HTML tag in the favicon response + response.html.forEach((markup) => { + const attributes = getAttributes(markup).reduce((acc, attr) => { + const [key, value] = attr.split('='); + acc[key] = value ? value.slice(1, -1) : ''; + + // Prepend output.publicPath to href paths + if (key === 'href' && compilation.compiler.options.output.publicPath !== 'auto') { + acc[key] = path.normalize(`${compilation.compiler.options.output.publicPath}/${acc[key]}`).replace(/\\/g, '/'); + } + return acc; + }, {}); + + // Push HTML object data into HtmlWebpackPlugin meta template + data.assetTags.meta.push({ + tagName: getTagType(markup), + voidTag: true, + meta: { plugin: 'WebpackFavicons' }, + attributes, + }); + }); + + // Run required callback with altered data + callback(null, data); + } + ); + + // If CopyWebpackPlugin is found, inject link tags manually + Object.keys(assets) + .filter((filename) => filename.endsWith('.html')) + .forEach((filename) => { + const { publicPath } = compilation.compiler.options.output; + response.html = response.html.map((markup) => + markup.replace(/href="(.*?)"/g, (_, p1) => `href="${path.normalize(`${publicPath}/${p1}`)}"`.replace(/\\/g, '/')) + ); + + // Inject favicon into .html document(s) + const HTML = compilation.getAsset(filename).source.source().toString(); + compilation.updateAsset( + filename, + new sources.RawSource( + HTML.replace( + /([\s\S]*?)<\/head>/, + `$1${response.html.join('\r ')}` ) ) - ); + ); }); - } - } + }; + + // Function to add generated images, JSON, and XML files to the build + addAssets = (response, assets) => { + const addFiles = (files) => + files.forEach((file) => { + assets[`/${this.options.path}/${file.name}`] = { + source: () => file.contents, + size: () => file.contents.length, + }; + }); + + // Adds generated images to build + if (response.images) addFiles(response.images); + + // Adds manifest JSON and XML files to build + if (response.files) addFiles(response.files); + }; } -module.exports = WebpackFavicons; \ No newline at end of file +module.exports = WebpackFavicons; diff --git a/package-lock.json b/package-lock.json index 620ed5f..bd4a341 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "webpack-favicons", - "version": "1.5.1", + "version": "1.5.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "webpack-favicons", - "version": "1.5.1", + "version": "1.5.4", "license": "MIT", "dependencies": { "favicons": "7.2.0" diff --git a/package.json b/package.json index d4397b1..4a3fd2e 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "webpack html favicon", "webpack favicons" ], - "version": "1.5.1", + "version": "1.5.4", "description": "Webpack plugin to generate favicons for devices and browsers", "repository": "drolsen/webpack-favicons", "bugs": { diff --git a/test/ava.test.js b/test/ava.test.js index 1ddc65c..5b6c4aa 100644 --- a/test/ava.test.js +++ b/test/ava.test.js @@ -80,6 +80,9 @@ test('minimal-test (no WebpackFavicons "icon": {} configuration)', t => { test('full-test (builds lots of favicon types)', t => { let fullTest = true; + const webmanifestTestData = fs.readFileSync(path.resolve(__dirname, '../dist/full/assets/manifest.webmanifest'), 'utf8'); + const yandexBrowserManifestTestData = fs.readFileSync(path.resolve(__dirname, '../dist/full/assets/yandex-browser-manifest.json'), 'utf8'); + if (!fs.existsSync(path.resolve(__dirname, '../dist/full/assets/android-chrome-144x144.png'))){ fullTest = false; } @@ -118,7 +121,32 @@ test('full-test (builds lots of favicon types)', t => { if (!fs.existsSync(path.resolve(__dirname, '../dist/full/assets/yandex-browser-50x50.png'))){ fullTest = false; - } + } + + if (!fs.existsSync(path.resolve(__dirname, '../dist/full/assets/browserconfig.xml'))){ + fullTest = false; + } + + if (!fs.existsSync(path.resolve(__dirname, '../dist/full/assets/manifest.webmanifest'))){ + fullTest = false; + } + + if (!fs.existsSync(path.resolve(__dirname, '../dist/full/assets/yandex-browser-manifest.json'))){ + fullTest = false; + } + + if ( + webmanifestTestData.toString().indexOf('"background_color": "#fff"') === -1 + || webmanifestTestData.toString().indexOf('"theme_color": "#fff"') === -1 + || webmanifestTestData.toString().indexOf('"name": "Webpack Favicons"') === -1 + || webmanifestTestData.toString().indexOf('"description": "Favicon Generator for Webpack 5",') === -1 + ) { + fullTest = false; + } + + if (yandexBrowserManifestTestData.toString().indexOf('"color": "#fff"') === -1) { + fullTest = false; + } if (fullTest) { t.pass(); diff --git a/test/basic.config.js b/test/basic.config.js index 95b40de..c478073 100644 --- a/test/basic.config.js +++ b/test/basic.config.js @@ -6,7 +6,8 @@ const path = require('path'); module.exports = { entry: path.resolve(__dirname, 'test.js'), output: { - path: path.resolve(__dirname, '../dist/basic'), + path: path.resolve(__dirname, '../dist/basic'), + publicPath: '/~media/', filename: 'test.js', pathinfo: false }, @@ -23,9 +24,18 @@ module.exports = { } }] }, + devtool: false, optimization: { minimize: false }, + stats: 'none', + cache: { + type: 'filesystem', + cacheDirectory: path.resolve(__dirname, '../node_modules/.cache/WebpackFavicons/basic'), + buildDependencies: { + config: [__filename] // Invalidate cache if config changes + }, + }, plugins: [ new CleanWebpackPlugin({ 'cleanOnceBeforeBuildPatterns': [path.resolve('./dist')] @@ -34,7 +44,9 @@ module.exports = { 'title': 'Basic Test', 'template': './test/test.html', 'filename': './test.html', - 'minify': false + 'minify': false, + 'cache': true, // Enable cache + 'parallel': true, // Enable parallel processing if available }), new WebpackFavicons({ 'src': 'assets/favicon.svg', diff --git a/test/callback.config.js b/test/callback.config.js index be87bb5..da6e3bb 100644 --- a/test/callback.config.js +++ b/test/callback.config.js @@ -24,15 +24,21 @@ module.exports = { } }] }, + devtool: false, optimization: { minimize: false }, + stats: 'none', + cache: { + type: 'filesystem', + cacheDirectory: path.resolve(__dirname, '../node_modules/.cache/WebpackFavicons/callback'), + buildDependencies: { + config: [__filename] // Invalidate cache if config changes + }, + }, plugins: [ - new CleanWebpackPlugin({ - 'cleanOnceBeforeBuildPatterns': [path.resolve(__dirname, '../dist/callback')] - }), new HtmlWebpackPlugin({ - 'title': 'Basic Test', + 'title': 'Callback Test', 'template': './test/test.html', 'filename': './test.html', 'minify': false diff --git a/test/copy.config.js b/test/copy.config.js index 04b26fc..9c7690d 100644 --- a/test/copy.config.js +++ b/test/copy.config.js @@ -7,6 +7,7 @@ module.exports = { entry: path.resolve(__dirname, 'test.js'), output: { path: path.resolve(__dirname, '../dist/copy'), + publicPath: '/~media/', filename: 'test.js', pathinfo: false }, @@ -23,13 +24,19 @@ module.exports = { } }] }, + devtool: false, optimization: { minimize: false }, + stats: 'none', + cache: { + type: 'filesystem', + cacheDirectory: path.resolve(__dirname, '../node_modules/.cache/WebpackFavicons/copy'), + buildDependencies: { + config: [__filename] // Invalidate cache if config changes + }, + }, plugins: [ - new CleanWebpackPlugin({ - 'cleanOnceBeforeBuildPatterns': [path.resolve('./dist/copy/')] - }), new CopyPlugin({ patterns: [ { from: 'test/test.html', to: './' } diff --git a/test/full.config.js b/test/full.config.js index 6370c53..f7f1514 100644 --- a/test/full.config.js +++ b/test/full.config.js @@ -24,24 +24,32 @@ module.exports = { } }] }, + devtool: false, optimization: { minimize: false }, + stats: 'none', + cache: { + type: 'filesystem', + cacheDirectory: path.resolve(__dirname, '../node_modules/.cache/WebpackFavicons/full'), + buildDependencies: { + config: [__filename] // Invalidate cache if config changes + }, + }, plugins: [ - new CleanWebpackPlugin({ - 'cleanOnceBeforeBuildPatterns': [path.resolve(__dirname, '../dist/full')] - }), new HtmlWebpackPlugin({ - 'title': 'Basic Test', + 'title': 'Full Test', 'template': './test/test.html', 'filename': './test.html', 'minify': false }), new WebpackFavicons({ + 'appName': 'Webpack Favicons', + 'appDescription': 'Favicon Generator for Webpack 5', 'src': 'assets/favicon.svg', 'path': 'assets', - 'background': '#000', - 'theme_color': '#000', + 'background': '#fff', + 'theme_color': '#fff', 'icons': { 'android': true, 'appleIcon': true, diff --git a/test/hybridcopy.config.js b/test/hybridcopy.config.js index 23ace7b..1da9f08 100644 --- a/test/hybridcopy.config.js +++ b/test/hybridcopy.config.js @@ -8,6 +8,7 @@ module.exports = { entry: path.resolve(__dirname, 'test.js'), output: { path: path.resolve(__dirname, '../dist/hybrid'), + publicPath: '/~media/', filename: 'test.js', pathinfo: false }, @@ -24,13 +25,19 @@ module.exports = { } }] }, + devtool: false, optimization: { minimize: false }, + stats: 'none', + cache: { + type: 'filesystem', + cacheDirectory: path.resolve(__dirname, '../node_modules/.cache/WebpackFavicons/hybridcopy'), + buildDependencies: { + config: [__filename] // Invalidate cache if config changes + }, + }, plugins: [ - new CleanWebpackPlugin({ - 'cleanOnceBeforeBuildPatterns': [path.resolve('./dist/hybrid/')] - }), new CopyPlugin({ patterns: [ { from: 'test/test.html', to: './' } diff --git a/test/minimal.config.js b/test/minimal.config.js index c874943..6a32aad 100644 --- a/test/minimal.config.js +++ b/test/minimal.config.js @@ -7,6 +7,7 @@ module.exports = { entry: path.resolve(__dirname, 'test.js'), output: { path: path.resolve(__dirname, '../dist/minimal'), + publicPath: '/~media/', filename: 'test.js', pathinfo: false }, @@ -23,15 +24,21 @@ module.exports = { } }] }, + devtool: false, optimization: { minimize: false }, + stats: 'none', + cache: { + type: 'filesystem', + cacheDirectory: path.resolve(__dirname, '../node_modules/.cache/WebpackFavicons/minimal'), + buildDependencies: { + config: [__filename] // Invalidate cache if config changes + }, + }, plugins: [ - new CleanWebpackPlugin({ - 'cleanOnceBeforeBuildPatterns': [path.resolve('./dist/minimal')] - }), new HtmlWebpackPlugin({ - 'title': 'Basic Test', + 'title': 'Minimal Test', 'template': './test/test.html', 'filename': './test.html', 'minify': false diff --git a/test/mixed-path.config.js b/test/mixed-path.config.js index 66ab3e5..d1ffbba 100644 --- a/test/mixed-path.config.js +++ b/test/mixed-path.config.js @@ -24,15 +24,21 @@ module.exports = { } }] }, + devtool: false, optimization: { minimize: false }, + stats: 'none', + cache: { + type: 'filesystem', + cacheDirectory: path.resolve(__dirname, '../node_modules/.cache/WebpackFavicons/mixedPath'), + buildDependencies: { + config: [__filename] // Invalidate cache if config changes + }, + }, plugins: [ - new CleanWebpackPlugin({ - 'cleanOnceBeforeBuildPatterns': [path.resolve(__dirname, '../dist/mixed')] - }), new HtmlWebpackPlugin({ - 'title': 'Basic Test', + 'title': 'Mixed Path Test', 'template': './test/test.html', 'filename': './test.html', 'minify': false diff --git a/test/nested.config.js b/test/nested.config.js index b0a8245..2e42191 100644 --- a/test/nested.config.js +++ b/test/nested.config.js @@ -7,6 +7,7 @@ module.exports = { entry: path.resolve(__dirname, 'test.js'), output: { path: path.resolve(__dirname, '../dist/nested'), + publicPath: '/~media/', filename: 'test.js', pathinfo: false }, @@ -23,15 +24,21 @@ module.exports = { } }] }, + devtool: false, optimization: { minimize: false }, + stats: 'none', + cache: { + type: 'filesystem', + cacheDirectory: path.resolve(__dirname, '../node_modules/.cache/WebpackFavicons/nested'), + buildDependencies: { + config: [__filename] // Invalidate cache if config changes + }, + }, plugins: [ - new CleanWebpackPlugin({ - 'cleanOnceBeforeBuildPatterns': [path.resolve('./dist/nested')] - }), new HtmlWebpackPlugin({ - 'title': 'Basic Test', + 'title': 'Nested Test', 'template': './test/test.html', 'filename': './test.html', 'minify': false diff --git a/test/public-path.config.js b/test/public-path.config.js index 467c535..2a5fe47 100644 --- a/test/public-path.config.js +++ b/test/public-path.config.js @@ -24,15 +24,21 @@ module.exports = { } }] }, + devtool: false, optimization: { minimize: false }, + stats: 'none', + cache: { + type: 'filesystem', + cacheDirectory: path.resolve(__dirname, '../node_modules/.cache/WebpackFavicons/publicPath'), + buildDependencies: { + config: [__filename] // Invalidate cache if config changes + }, + }, plugins: [ - new CleanWebpackPlugin({ - 'cleanOnceBeforeBuildPatterns': [path.resolve('./dist/public')] - }), new HtmlWebpackPlugin({ - 'title': 'Basic Test', + 'title': 'Public Path Test', 'template': './test/test.html', 'filename': './test.html', 'minify': false