diff --git a/example/runtime/src/app.js b/example/runtime/src/app.js index 85ec2fe0..e6473e78 100644 --- a/example/runtime/src/app.js +++ b/example/runtime/src/app.js @@ -1,3 +1,3 @@ -import tags from 'favicons-webpack-plugin/runtime/tags'; +import {tags} from 'favicons-webpack-plugin/runtime/tags'; console.log(tags); \ No newline at end of file diff --git a/runtime/tags.js b/runtime/tags.js index b79fdff8..fd4873c3 100644 --- a/runtime/tags.js +++ b/runtime/tags.js @@ -3,9 +3,22 @@ // // This is only a placeholder file for typescript and eslint // -// The real content for this file will be generated by runtime-loader.js +// The real content for this file will be generated by favicons-webpack-plugin/src/runtime-loader.js // +/** + * All tags from all favicons compilations inside a flat array + * + * @type {string[]} + * + * @example +```js +[ '' ] +``` + */ +var tags = []; +module.exports.tags = tags; + /** * All tags from all favicons compilations * @@ -16,5 +29,5 @@ { 'assets/': [ '' ] } ``` */ -var tags = {}; -module.exports = tags; \ No newline at end of file +var tagsPerPublicPath = {}; +module.exports.tagsPerPublicPath = tagsPerPublicPath; \ No newline at end of file diff --git a/src/index.js b/src/index.js index 4ff3530e..54c88b1e 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,4 @@ // @ts-check - const assert = require('assert'); const parse5 = require('parse5'); const path = require('path'); @@ -9,7 +8,21 @@ const url = require('url'); const { resolvePublicPath, replaceContentHash } = require('./hash'); const { webpackLogger } = require('./logger'); const runtimeLoader = require('./runtime-loader'); -const attachedCompilers = new WeakSet(); + +/** + * @type {WeakSet} + * Static set to track if a compiler has already a FaviconWebpackPlugin instance attached + */ +const attachedWebpackCompilers = new WeakSet(); + +/** + * @typedef {{ + tags: import('./html-tags').HtmlTagObject[], + publicPath: string, + assets: Array<{name: string, contents: import('webpack').sources.RawSource }>, + dependencies: string[], + }} FaviconCompilationResult + */ class FaviconsWebpackPlugin { /** @@ -36,11 +49,28 @@ class FaviconsWebpackPlugin { */ apply(compiler) { compiler.hooks.initialize.tap('FaviconsWebpackPlugin', () => { + if (!attachedWebpackCompilers.has(compiler)) { + attachedWebpackCompilers.add(compiler); + this.hookIntoCompilerOnce(compiler); + } this.hookIntoCompiler(compiler); }); } /** + * This hook is only executed once per compiler no matter how many + * FaviconsWebpackPlugins will be attached + * + * @param {import('webpack').Compiler} compiler + */ + hookIntoCompilerOnce(compiler) { + // Add one loader to add support for `import { tags ] from 'favicons-webpack-plugin/runtime/tags'` + compiler.options.module.rules.push(runtimeLoader.moduleRuleConfig); + } + + /** + * This hook is executed once per FaviconsWebpackPlugin instance + * * @param {import('webpack').Compiler} compiler */ hookIntoCompiler(compiler) { @@ -48,7 +78,7 @@ class FaviconsWebpackPlugin { const Compilation = webpack.Compilation; const NormalModule = webpack.NormalModule; const oracle = new Oracle(compiler.context); - /** @type {WeakMap}>>} */ + /** @type {WeakMap>}>>} */ const faviconCompilations = new WeakMap(); { @@ -69,12 +99,6 @@ class FaviconsWebpackPlugin { }); } - // Add one loader to add support for `import tags from 'favicons-webpack-plugin/runtime/tags'` - if (!attachedCompilers.has(compiler)) { - attachedCompilers.add(compiler); - compiler.options.module.rules.push(runtimeLoader.moduleRuleConfig); - } - if (this.options.logo === undefined) { const defaultLogo = path.resolve(compiler.context, 'logo.png'); try { @@ -132,7 +156,7 @@ class FaviconsWebpackPlugin { ); // Inject favicons information into runtime tags - // to allow `import tags from 'favicons-webpack-plugin/runtime/tags'` + // to allow `import { tags ] from 'favicons-webpack-plugin/runtime/tags'` const tagsFilePath = require.resolve('../runtime/tags.js'); const normalModuleHooks = NormalModule.getCompilationHooks(compilation); normalModuleHooks.loader.tap( @@ -292,8 +316,9 @@ class FaviconsWebpackPlugin { * @param {Buffer | string} baseManifest - the content of the file from options.manifest * @param {import('webpack').Compilation} compilation * @param {string} outputPath + * @returns {Promise} */ - generateFavicons(logo, baseManifest, compilation, outputPath) { + async generateFavicons(logo, baseManifest, compilation, outputPath) { const resolvedPublicPath = getResolvedPublicPath( logo.hash, compilation, @@ -305,6 +330,9 @@ class FaviconsWebpackPlugin { ? JSON.parse(baseManifest.toString() || '{}') : this.options.manifest || {}; + // File dependencies + const dependencies = (this.options.manifest === 'string' ? [this.options.manifest] : []).concat(this.options.logo); + switch (this.getCurrentCompilationMode(compilation.compiler)) { case 'light': if (!this.options.mode) { @@ -312,25 +340,25 @@ class FaviconsWebpackPlugin { 'generate only a single favicon for fast compilation time in development mode. This behaviour can be changed by setting the favicon mode option.' ); } - - return this.generateFaviconsLight( + const lightFaviconsCompilation = await this.generateFaviconsLight( logo.content, parsedBaseManifest, compilation, resolvedPublicPath, - outputPath + outputPath, ); + return {...lightFaviconsCompilation, dependencies}; case 'webapp': default: webpackLogger(compilation).log('generate favicons'); - - return this.generateFaviconsWebapp( + const webappFaviconsCompilation = await this.generateFaviconsWebapp( logo.content, parsedBaseManifest, compilation, resolvedPublicPath, - outputPath + outputPath, ); + return {...webappFaviconsCompilation, dependencies}; } } diff --git a/src/runtime-loader.js b/src/runtime-loader.js index d6d9fd4d..290caa6f 100644 --- a/src/runtime-loader.js +++ b/src/runtime-loader.js @@ -1,37 +1,39 @@ /// @ts-check const htmlTagObjectToString = require('./html-tags').htmlTagObjectToString; +/** + * Config used for the webpack config + */ +const moduleRuleConfig = Object.freeze({ + test: require.resolve('../runtime/tags.js'), + use: 'favicons-webpack-plugin/src/runtime-loader' +}); + +/** + * @typedef {{ + tags: import('./html-tags').HtmlTagObject[], + publicPath: string, + assets: Array<{name: string, contents: import('webpack').sources.RawSource }>, + dependencies: string[], + }} FaviconCompilationResult + */ + /** * The contextMap is a bridge which receives data from the * favicons-webpack-plugin during the NormalModule loader phase * see ./index.js * * @type { - WeakMap; - }>>> + WeakMap>> } */ const contextMap = new WeakMap(); -/** - * Config used for the webpack config - */ -const moduleRuleConfig = Object.freeze({ - test: require.resolve('../runtime/tags.js'), - use: 'favicons-webpack-plugin/src/runtime-loader' -}); - /** * the main loader is only a placeholder which will have no effect * as the pitch function returns * - * @this {{ async: () => ((err: Error | null, result: string) => void)}} + * @this {{ addDependency: (dependency: string) => void }} */ const loader = async function faviconsTagLoader() { const faviconCompilationPromisses = contextMap.get(this); @@ -41,6 +43,12 @@ const loader = async function faviconsTagLoader() { const faviconCompilations = await Promise.all(faviconCompilationPromisses); + faviconCompilations.forEach(({dependencies}) => { + dependencies.forEach((dependency) => { + this.addDependency(dependency); + }); + }); + const tagsOfFaviconCompilations = faviconCompilations.map( faviconCompilation => { const { tags, publicPath } = faviconCompilation; @@ -74,7 +82,20 @@ const loader = async function faviconsTagLoader() { {} ); - return `export default ${JSON.stringify(tagsByPublicPath)}`; + // create a flat array as it will be easier to use for all users + // who use only a single FaviconWebpackPlugin instance + const allTags = tagsOfFaviconCompilations.reduce( + (result, [key, value]) => { + result.push(...value); + return result; + }, + /** @type {string[]} */([]) + ); + + return `// Generated by favicons-webpack-plugin/runtime-loader +export const tags = ${JSON.stringify(allTags)}; +export const tagsPerPublicPath = ${JSON.stringify(tagsByPublicPath)}; +`; }; module.exports = Object.assign(loader, {