UNPKG

6.65 kBJavaScriptView Raw
1// @ts-check
2
3// Import types
4/** @typedef {import("webpack").Compilation} WebpackCompilation */
5/** @typedef {Parameters<WebpackCompilation['fileSystemInfo']['checkSnapshotValid']>[0]} Snapshot */
6
7const path = require('path');
8const { getContentHash } = require('./hash');
9
10/**
11 * Executes asynchronous function with a callback-style calling convention and returns a promise.
12 *
13 * @template T, E
14 *
15 * @param {(cb: (error: E, result: T) => void) => void} func
16 * @returns {Promise<T, E>}
17 */
18function asPromise(func) {
19 return new Promise((resolve, reject) => {
20 /** @type {(error: E, result: T) => void} */
21 const cb = (err, result) => {
22 if (err) {
23 reject(err);
24 } else {
25 resolve(result);
26 }
27 };
28 func(cb);
29 });
30}
31
32/** @type {WeakMap<any, Promise<Snapshot>>} */
33const snapshots = new WeakMap();
34/** @type {WeakMap<Promise<Snapshot>, Promise<any>>} */
35const faviconCache = new WeakMap();
36
37/**
38 * Executes the generator function and caches the result in memory
39 * The cache will be invalidated after the logo source file was modified
40 *
41 * @template TResult
42 *
43 * @param {string[]} absoluteFilePaths - file paths used used by the generator
44 * @param {any} pluginInstance - the plugin instance to use as cache key
45 * @param {boolean} useWebpackCache - Support webpack built in cache
46 * @param {WebpackCompilation} compilation - the current webpack compilation
47 * @param {string[]} eTags - eTags to verify the string
48 * @param {(files: { filePath: string, hash: string, content: Buffer }[]) => string} idGenerator
49 * @param {(files: { filePath: string, hash: string, content: Buffer }[], id: string) => Promise<TResult>} generator
50 *
51 * @returns {Promise<TResult>}
52 */
53function runCached(
54 absoluteFilePaths,
55 pluginInstance,
56 useWebpackCache,
57 compilation,
58 eTags,
59 idGenerator,
60 generator
61) {
62 const latestSnapShot = snapshots.get(pluginInstance);
63
64 /** @type {Promise<TResult> | undefined} */
65 const cachedFavicons = latestSnapShot && faviconCache.get(latestSnapShot);
66
67 if (latestSnapShot && cachedFavicons) {
68 return isSnapShotValid(latestSnapShot, compilation).then((isValid) => {
69 // If the source files have changed clear all caches
70 // and try again
71 if (!isValid) {
72 faviconCache.delete(latestSnapShot);
73
74 return runCached(
75 absoluteFilePaths,
76 pluginInstance,
77 useWebpackCache,
78 compilation,
79 eTags,
80 idGenerator,
81 generator
82 );
83 }
84
85 // If the cache is valid return the result directly from cache
86 return cachedFavicons;
87 });
88 }
89
90 // Store a snapshot of the filesystem
91 // to find out if the logo was changed
92 const newSnapShot = createSnapshot(
93 {
94 fileDependencies: absoluteFilePaths.filter(Boolean),
95 contextDependencies: [],
96 missingDependencies: [],
97 },
98 compilation
99 );
100 snapshots.set(pluginInstance, newSnapShot);
101
102 // Start generating the favicons
103 const faviconsGenerationsPromise = useWebpackCache
104 ? runWithFileCache(
105 absoluteFilePaths,
106 compilation,
107 idGenerator,
108 eTags,
109 generator
110 )
111 : readFiles(absoluteFilePaths, compilation).then((fileContents) =>
112 generator(fileContents, idGenerator(fileContents))
113 );
114
115 // Store the promise of the favicon compilation in cache
116 faviconCache.set(newSnapShot, faviconsGenerationsPromise);
117
118 return faviconsGenerationsPromise;
119}
120
121/**
122 * Create a snapshot
123 * @param {{fileDependencies: string[], contextDependencies: string[], missingDependencies: string[]}} fileDependencies
124 * @param {WebpackCompilation} mainCompilation
125 * @returns {Promise<Snapshot>}
126 */
127async function createSnapshot(fileDependencies, mainCompilation) {
128 const snapshot = await asPromise((cb) =>
129 mainCompilation.fileSystemInfo.createSnapshot(
130 new Date().getTime(),
131 fileDependencies.fileDependencies,
132 fileDependencies.contextDependencies,
133 fileDependencies.missingDependencies,
134 {},
135 cb
136 )
137 );
138
139 if (!snapshot) {
140 throw new Error('Could not create Snapshot');
141 }
142
143 return snapshot;
144}
145
146/**
147 *
148 * Use the webpack cache which supports filesystem caching to improve build speed
149 * See also https://webpack.js.org/configuration/other-options/#cache
150 * Create one cache for every output target
151 *
152 * Executes the generator function and stores it in the webpack file cache
153 * @template TResult
154 *
155 * @param {string[]} files - the file pathes to be watched for changes
156 * @param {WebpackCompilation} compilation - the current webpack compilation
157 * @param {(files: { filePath: string, hash: string, content: Buffer }[]) => string} idGenerator
158 * @param {string[]} eTags - eTags to verify the string
159 * @param {(files: { filePath: string, hash: string, content: Buffer }[], id: string) => Promise<TResult>} generator
160 *
161 * @returns {Promise<TResult>}
162 */
163async function runWithFileCache(
164 files,
165 compilation,
166 idGenerator,
167 eTags,
168 generator
169) {
170 const fileSources = await readFiles(files, compilation);
171 const webpackCache = compilation.getCache('favicons-webpack-plugin');
172 // Cache invalidation token
173 const eTag = [...eTags, fileSources.map(({ hash }) => hash)].join(' ');
174 const cacheId = idGenerator(fileSources);
175
176 return webpackCache.providePromise(cacheId, eTag, () =>
177 generator(fileSources, cacheId)
178 );
179}
180
181/**
182 * readFiles and get content hashes
183 *
184 * @param {string[]} absoluteFilePaths
185 * @param {WebpackCompilation} compilation
186 * @returns {Promise<{filePath: string, hash: string, content: Buffer}[]>}
187 */
188function readFiles(absoluteFilePaths, compilation) {
189 return Promise.all(
190 absoluteFilePaths.map(async (absoluteFilePath) => {
191 if (!absoluteFilePath) {
192 return { filePath: absoluteFilePath, hash: '', content: '' };
193 }
194
195 const content = await asPromise((cb) =>
196 compilation.inputFileSystem.readFile(
197 path.resolve(compilation.compiler.context, absoluteFilePath),
198 cb
199 )
200 );
201
202 return {
203 filePath: absoluteFilePath,
204 hash: getContentHash(content),
205 content,
206 };
207 })
208 );
209}
210
211/**
212 * Returns true if the files inside this snapshot
213 * have not been changed
214 *
215 * @param {Promise<Snapshot>} snapshotPromise
216 * @param {WebpackCompilation} mainCompilation
217 * @returns {Promise<boolean>}
218 */
219async function isSnapShotValid(snapshotPromise, mainCompilation) {
220 const snapshot = await snapshotPromise;
221 const isValid = await asPromise((cb) =>
222 mainCompilation.fileSystemInfo.checkSnapshotValid(snapshot, cb)
223 );
224
225 return Boolean(isValid);
226}
227
228module.exports = { runCached };