Skip to content

Commit

Permalink
feat: make tag information accessible during runtime
Browse files Browse the repository at this point in the history
  • Loading branch information
jantimon committed Feb 7, 2021
1 parent 7b812e8 commit 8b61457
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 65 deletions.
8 changes: 8 additions & 0 deletions example/runtime/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ module.exports = (env, args) => {
template: './src/index.html',
}),
new FaviconsWebpackPlugin('./src/favicon.png'),
new FaviconsWebpackPlugin({
logo: './src/favicon.png',
manifest: {
"name": "Runtime Loader Example",
},
prefix: 'favicons/[contenthash]/' ,
inject: false
}),
],
stats: "errors-only"
};
Expand Down
11 changes: 10 additions & 1 deletion runtime/tags.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@
// The real content for this file will be generated by runtime-loader.js
//

/** @type {{[publicPath: string]: string[]}} */
/**
* All tags from all favicons compilations
*
* @type {{[publicPath: string]: string[]}}
*
* @example
```js
{ 'assets/': [ '<link rel="icon" href="assets/favicon.png">' ] }
```
*/
var tags = {};
module.exports = tags;
47 changes: 47 additions & 0 deletions src/html-tags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* @typedef {{
attributes: {
[attributeName: string]: string | boolean;
};
tagName: string;
innerHTML?: string | undefined;
voidTag: boolean;
meta: any
}
} HtmlTagObject
*/

/**
* Turn a tag definition into a html string
* @param {HtmlTagObject} tagDefinition
* A tag element according to the htmlWebpackPlugin object notation
*
* @param xhtml {boolean}
* Wether the generated html should add closing slashes to be xhtml compliant
*/
function htmlTagObjectToString(tagDefinition, xhtml) {
const attributes = Object.keys(tagDefinition.attributes || {})
.filter(function(attributeName) {
return tagDefinition.attributes[attributeName] !== false;
})
.map(function(attributeName) {
if (tagDefinition.attributes[attributeName] === true) {
return xhtml
? attributeName + '="' + attributeName + '"'
: attributeName;
}
return (
attributeName + '="' + tagDefinition.attributes[attributeName] + '"'
);
});
return (
'<' +
[tagDefinition.tagName].concat(attributes).join(' ') +
(tagDefinition.voidTag && xhtml ? '/' : '') +
'>' +
(tagDefinition.innerHTML || '') +
(tagDefinition.voidTag ? '' : '</' + tagDefinition.tagName + '>')
);
}

module.exports = { htmlTagObjectToString };
125 changes: 79 additions & 46 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const url = require('url');
const { resolvePublicPath, replaceContentHash } = require('./hash');
const { webpackLogger } = require('./logger');
const runtimeLoader = require('./runtime-loader');
const attachedCompilers = new WeakSet();

class FaviconsWebpackPlugin {
/**
Expand Down Expand Up @@ -45,6 +46,7 @@ class FaviconsWebpackPlugin {
hookIntoCompiler(compiler) {
const webpack = compiler.webpack;
const Compilation = webpack.Compilation;
const NormalModule = webpack.NormalModule;
const oracle = new Oracle(compiler.context);
/** @type {WeakMap<any, Promise<{tags: string[], assets: Array<{name: string, contents: import('webpack').sources.RawSource}>}>>} */
const faviconCompilations = new WeakMap();
Expand All @@ -67,8 +69,11 @@ class FaviconsWebpackPlugin {
});
}

// Add a lower to add support for `import tags from 'favicons-webpack-plugin/runtime/tags'`
compiler.options.module.rules.push(runtimeLoader.moduleRuleConfig);
// 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');
Expand Down Expand Up @@ -97,7 +102,6 @@ class FaviconsWebpackPlugin {
compiler.hooks.make.tapPromise(
'FaviconsWebpackPlugin',
async compilation => {

const faviconCompilation = runCached(
[
this.options.logo,
Expand Down Expand Up @@ -127,13 +131,21 @@ class FaviconsWebpackPlugin {
)
);

// Allow runtime-loader to access the compilation
// Inject favicons information into runtime tags
// to allow `import tags from 'favicons-webpack-plugin/runtime/tags'`
const tagsFilePath = require.resolve('../runtime/tags.js');
compiler.webpack.NormalModule.getCompilationHooks(compilation).loader.tap('FaviconsWebpackPlugin', (loaderContext, normalModule) => {
if (normalModule.resource === tagsFilePath) {
runtimeLoader.contextMap.set(loaderContext, faviconCompilation);
}
});
const normalModuleHooks = NormalModule.getCompilationHooks(compilation);
normalModuleHooks.loader.tap(
'FaviconsWebpackPlugin',
(loaderContext, normalModule) => {
if (normalModule.resource === tagsFilePath) {
const faviconCompilations =
runtimeLoader.contextMap.get(loaderContext) || new Set();
faviconCompilations.add(faviconCompilation);
runtimeLoader.contextMap.set(loaderContext, faviconCompilations);
}
}
);

// Watch for changes to the logo
compilation.fileDependencies.add(this.options.logo);
Expand Down Expand Up @@ -204,40 +216,28 @@ class FaviconsWebpackPlugin {
: /** @param {string} url */ url => url;

htmlPluginData.assetTags.meta.push(
...faviconCompilation.tags
.map(tag => parse5.parseFragment(tag).childNodes[0])
.map(({ tagName, attrs }) => {
const htmlTag = {
tagName,
voidTag: true,
meta: { plugin: 'favicons-webpack-plugin' },
attributes: attrs.reduce(
(obj, { name, value }) =>
Object.assign(obj, { [name]: value }),
{}
)
};
// Prefix link tags
if (typeof htmlTag.attributes.href === 'string') {
htmlTag.attributes.href = pathReplacer(
htmlTag.attributes.href
);
}
// Prefix meta tags
if (
htmlTag.tagName === 'meta' &&
[
'msapplication-TileImage',
'msapplication-config'
].includes(htmlTag.attributes.name)
) {
htmlTag.attributes.content = pathReplacer(
htmlTag.attributes.content
);
}

return htmlTag;
})
...faviconCompilation.tags.map(htmlTag => {
// Prefix link tags
if (typeof htmlTag.attributes.href === 'string') {
htmlTag.attributes.href = pathReplacer(
htmlTag.attributes.href
);
}
// Prefix meta tags
if (
htmlTag.tagName === 'meta' &&
[
'msapplication-TileImage',
'msapplication-config'
].includes(htmlTag.attributes.name)
) {
htmlTag.attributes.content = pathReplacer(
htmlTag.attributes.content
);
}

return htmlTag;
})
);

htmlWebpackPluginCallback(null, htmlPluginData);
Expand Down Expand Up @@ -356,7 +356,19 @@ class FaviconsWebpackPlugin {
const faviconName = `favicon${faviconExt}`;
const RawSource = compilation.compiler.webpack.sources.RawSource;

const tags = [`<link rel="icon" href="${faviconName}">`];
/** @type {{tagName: string, voidTag: boolean, meta: any, attributes: {[key:string]: string}}[]} */
const tags = [
{
tagName: 'link',
voidTag: true,
meta: { plugin: 'favicons-webpack-plugin' },
attributes: {
rel: 'icon',
href: faviconName
}
}
];

const assets = [
{
name: path.join(outputPath, faviconName),
Expand All @@ -366,7 +378,15 @@ class FaviconsWebpackPlugin {

// If the manifest is not empty add it also to the light mode
if (Object.keys(baseManifest).length > 0) {
tags.push('<link rel="manifest" href="manifest.json">');
tags.push({
tagName: 'link',
voidTag: true,
meta: { plugin: 'favicons-webpack-plugin' },
attributes: {
rel: 'manifest',
href: 'manifest.json'
}
});
assets.push({
name: path.join(outputPath, 'manifest.json'),
contents: new RawSource(
Expand Down Expand Up @@ -414,7 +434,7 @@ class FaviconsWebpackPlugin {
const RawSource = compilation.compiler.webpack.sources.RawSource;
const favicons = loadFaviconsLibrary();
// Generate favicons using the npm favicons library
const { html: tags, images, files } = await favicons(logoSource, {
const { html, images, files } = await favicons(logoSource, {
// Generate all assets relative to the root directory
// to allow relative manifests and to set the final public path
// once it has been provided by the html-webpack-plugin
Expand Down Expand Up @@ -444,6 +464,19 @@ class FaviconsWebpackPlugin {
contents: new RawSource(contents, false)
}));

/** @type {{tagName: string, voidTag: boolean, meta: any, attributes: {[key:string]: string}}[]} */
const tags = html
.map(tag => parse5.parseFragment(tag).childNodes[0])
.map(({ tagName, attrs }) => ({
tagName,
voidTag: true,
meta: { plugin: 'favicons-webpack-plugin' },
attributes: attrs.reduce(
(obj, { name, value }) => Object.assign(obj, { [name]: value }),
{}
)
}));

return { assets, tags, publicPath: resolvedPublicPath };
}

Expand Down
69 changes: 51 additions & 18 deletions src/runtime-loader.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
/// @ts-check
const htmlTagObjectToString = require('./html-tags').htmlTagObjectToString;

/**
* The contextMap is a bridge which receives data from the
* favicons-webpack-plugin during the NormalModule loader phase
* see ./index.js
*
* @type {
WeakMap<any, Promise<{
WeakMap<any, Set<Promise<{
publicPath: string;
assets: {
name: string;
contents: import('webpack').sources.RawSource;
}[];
tags: string[];
}>>
tags: Array<import('./html-tags').HtmlTagObject>;
}>>>
}
*/
const contextMap = new WeakMap();
Expand All @@ -22,31 +23,63 @@ 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',
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
*
* as the pitch function returns
*
* @this {{ async: () => ((err: Error | null, result: string) => void)}}
*/
const loader = function () {
const callback = this.async();
const faviconCompilation = contextMap.get(this);
if (!faviconCompilation) {
const loader = async function faviconsTagLoader() {
const faviconCompilationPromisses = contextMap.get(this);
if (!faviconCompilationPromisses) {
throw new Error('broken contextMap');
}

faviconCompilation.then(({tags}) => {
callback(null, `export default ${JSON.stringify(tags)}`);
});
}

const faviconCompilations = await Promise.all(faviconCompilationPromisses);

const tagsOfFaviconCompilations = faviconCompilations.map(
faviconCompilation => {
const { tags, publicPath } = faviconCompilation;
// Inject public path into tags tags into strings
const tagsWithPublicPath = tags.map(tag => {
if (!tag.attributes.href) {
return tag;
}
return {
...tag,
attributes: {
...tag.attributes,
href: publicPath + tag.attributes.href
}
};
});
// Convert tags to string
const htmlTags = tagsWithPublicPath.map(tag =>
htmlTagObjectToString(tag, false)
);
return /** @type {[string, string[]]} */ ([publicPath, htmlTags]);
}
);

// naive Object.fromEntries implementation as Object.entries requires node 12
const tagsByPublicPath = tagsOfFaviconCompilations.reduce(
(result, [key, value]) => {
result[key] = value;
return result;
},
{}
);

return `export default ${JSON.stringify(tagsByPublicPath)}`;
};

module.exports = Object.assign(loader, {
// Use the loader as pitch loader to overrule all other loaders
pitch: loader,
contextMap,
moduleRuleConfig
});
});

0 comments on commit 8b61457

Please sign in to comment.