Skip to content

Commit

Permalink
feat: use webpack 5 cache system and remove child compilation
Browse files Browse the repository at this point in the history
BREAKING CHANGE: file based cache will only work if you configure webpacks filesystem cache
  • Loading branch information
jantimon committed Jan 6, 2021
1 parent 8bae0b1 commit c1dc12c
Show file tree
Hide file tree
Showing 22 changed files with 1,097 additions and 293 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
/.nyc_output/
/.vscode/
/example/**/public/
**/.wwp-cache/
**/.cache/
**/node_modules/
3 changes: 3 additions & 0 deletions example/no-inject/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ module.exports = (env, args) => {
publicPath: '/',
filename: 'app.js'
},
cache: {
type: 'filesystem',
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
Expand Down
246 changes: 246 additions & 0 deletions src/cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
/// @ts-check

// Import types
/** @typedef {ReturnType<import("webpack").Compiler['getCache']>} WebpackCacheFacade */
/** @typedef {import("webpack").Compilation} WebpackCompilation */
/** @typedef {any} Snapshot */

/** @typedef {{
tags: string[],
assets: Array<{
name: string,
contents: import('webpack').sources.RawSource
}>
}} FaviconsCompilationResult */

const path = require('path');
const {
replaceContentHash,
resolvePublicPath,
getContentHash
} = require('./hash');

/** @type {WeakMap<any, Snapshot>} */
const snapshots = new WeakMap();
/** @type {WeakMap<Snapshot, Promise<FaviconsCompilationResult>>} */
const faviconCache = new WeakMap();

/**
* Executes the generator function and caches the result in memory
* The cache will be invalidated after the logo source file was modified
*
* @param {import('./options').FaviconWebpackPlugionInternalOptions} faviconOptions
* @param {string} context - the compiler.context patth
* @param {WebpackCompilation} compilation - the current webpack compilation
* @param {any} pluginInstance - the plugin instance to use as cache key
* @param {(
logoSource: Buffer | string,
compilation: WebpackCompilation,
resolvedPublicPath: string,
outputPath: string
) => Promise<FaviconsCompilationResult>
} generator
*
* @returns {Promise<FaviconsCompilationResult>}
*/
function runCached(
faviconOptions,
context,
compilation,
pluginInstance,
generator
) {
const { logo } = faviconOptions;

const latestSnapShot = snapshots.get(pluginInstance);
const cachedFavicons = latestSnapShot && faviconCache.get(latestSnapShot);

if (latestSnapShot && cachedFavicons) {
return isSnapShotValid(latestSnapShot, compilation).then(isValid => {
// If the source files have changed clear all caches
// and try again
if (!isValid) {
faviconCache.delete(latestSnapShot);
return runCached(
faviconOptions,
context,
compilation,
pluginInstance,
generator
);
}
// If the cache is valid return the result directly from cache
return cachedFavicons;
});
}

// Store a snapshot of the filesystem
// to find out if the logo was changed
snapshots.set(
pluginInstance,
createSnapshot(
{
fileDependencies: [logo],
contextDependencies: [],
missingDependencies: []
},
compilation
)
);

// Start generating the favicons
const faviconsGenerationsPromise = runWithFileCache(
faviconOptions,
context,
compilation,
generator
);

// Store the promise of the favicon compilation in cache
faviconCache.set(
snapshots.get(pluginInstance) || latestSnapShot,
faviconsGenerationsPromise
);

return faviconsGenerationsPromise;
}

/**
* Create a snapshot
* @param {{fileDependencies: string[], contextDependencies: string[], missingDependencies: string[]}} fileDependencies
* @param {WebpackCompilation} mainCompilation
* @returns {Promise<Snapshot>}
*/
function createSnapshot(fileDependencies, mainCompilation) {
return new Promise((resolve, reject) => {
mainCompilation.fileSystemInfo.createSnapshot(
new Date().getTime(),
fileDependencies.fileDependencies,
fileDependencies.contextDependencies,
fileDependencies.missingDependencies,
{},
(err, snapshot) => {
if (err) {
return reject(err);
}
resolve(snapshot);
}
);
});
}

/**
* Executes the generator function and stores it in the webpack file cache
*
* @param {import('./options').FaviconWebpackPlugionInternalOptions} faviconOptions
* @param {string} context - the compiler.context patth
* @param {WebpackCompilation} compilation - the current webpack compilation
* @param {(
logoSource: Buffer | string,
compilation: WebpackCompilation,
resolvedPublicPath: string,
outputPath: string
) => Promise<FaviconsCompilationResult>
} generator
*
* @returns {Promise<FaviconsCompilationResult>}
*/
async function runWithFileCache(
faviconOptions,
context,
compilation,
generator
) {
const { logo } = faviconOptions;
const logoSource = await new Promise((resolve, reject) =>
compilation.inputFileSystem.readFile(
path.resolve(context, logo),
(error, fileBuffer) => {
if (error) {
reject(error);
} else {
resolve(fileBuffer);
}
}
)
);

const compilationOutputPath = compilation.outputOptions.path || '';
/**
* the relative output path to the folder where the favicon files should be generated to
* it might include tokens like [fullhash] or [contenthash]
*/
const relativeOutputPath = faviconOptions.outputPath
? path.relative(
compilationOutputPath,
path.resolve(compilationOutputPath, faviconOptions.outputPath)
)
: faviconOptions.prefix;

const logoContentHash = getContentHash(logoSource);
const executeGenerator = () => {
const outputPath = replaceContentHash(
compilation,
relativeOutputPath,
logoContentHash
);
const resolvedPublicPath = replaceContentHash(
compilation,
resolvePublicPath(
compilation,
faviconOptions.publicPath || compilation.outputOptions.publicPath,
faviconOptions.prefix
),
logoContentHash
);

return generator(logoSource, compilation, resolvedPublicPath, outputPath);
};

if (faviconOptions.cache === false) {
return executeGenerator();
}

const webpackCache = compilation.getCache('favicons-webpack-plugin');
// Cache invalidation token
const eTag = [
JSON.stringify(faviconOptions.publicPath),
JSON.stringify(faviconOptions.mode),
// Recompile filesystem cache if the user change the favicon options
JSON.stringify(faviconOptions.favicons),
// Recompile filesystem cache if the logo source changes:
logoContentHash
].join('\n');
// Use the webpack cache which supports filesystem caching to improve build speed
// See also https://webpack.js.org/configuration/other-options/#cache
// Create one cache for every output target
return webpackCache.providePromise(
relativeOutputPath,
eTag,
executeGenerator
);
}

/**
* Returns true if the files inside this snapshot
* have not been changed
*
* @param {Snapshot} snapshot
* @param {WebpackCompilation} mainCompilation
* @returns {Promise<boolean>}
*/
function isSnapShotValid(snapshot, mainCompilation) {
return new Promise((resolve, reject) => {
mainCompilation.fileSystemInfo.checkSnapshotValid(
snapshot,
(err, isValid) => {
if (err) {
reject(err);
}
resolve(Boolean(isValid));
}
);
});
}

module.exports = { runCached };
6 changes: 0 additions & 6 deletions src/compat.d.ts

This file was deleted.

Loading

0 comments on commit c1dc12c

Please sign in to comment.