1 | // @ts-check
|
2 |
|
3 | // Import types
|
4 | /** @typedef {import("webpack").Compilation} WebpackCompilation */
|
5 | /** @typedef {Parameters<WebpackCompilation['fileSystemInfo']['checkSnapshotValid']>[0]} Snapshot */
|
6 |
|
7 | const path = require('path');
|
8 | const { 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 | */
|
18 | function 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>>} */
|
33 | const snapshots = new WeakMap();
|
34 | /** @type {WeakMap<Promise<Snapshot>, Promise<any>>} */
|
35 | const 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 | */
|
53 | function 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 | */
|
127 | async 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 | */
|
163 | async 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 | */
|
188 | function 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 | */
|
219 | async 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 |
|
228 | module.exports = { runCached };
|