UNPKG

11.7 kBPlain TextView Raw
1import path from 'path';
2import { VirtualStats } from './virtual-stats';
3import type { Compiler } from 'webpack';
4
5let inode = 45000000;
6const ALL = 'all';
7const STATIC = 'static';
8const DYNAMIC = 'dynamic';
9
10type AvailableModules = typeof ALL | typeof STATIC | typeof DYNAMIC;
11
12function checkActivation(instance) {
13 if (!instance._compiler) {
14 throw new Error('You must use this plugin only after creating webpack instance!');
15 }
16}
17
18function getModulePath(filePath, compiler) {
19 return path.isAbsolute(filePath) ? filePath : path.join(compiler.context, filePath);
20}
21
22function createWebpackData(result) {
23 return (backendOrStorage) => {
24 // In Webpack v5, this variable is a "Backend", and has the data stored in a field
25 // _data. In V4, the `_` prefix isn't present.
26 if (backendOrStorage._data) {
27 const curLevelIdx = backendOrStorage._currentLevel;
28 const curLevel = backendOrStorage._levels[curLevelIdx];
29 return {
30 result,
31 level: curLevel,
32 };
33 }
34 // Webpack 4
35 return [null, result];
36 };
37}
38
39function getData(storage, key) {
40 // Webpack 5
41 if (storage._data instanceof Map) {
42 return storage._data.get(key);
43 } else if (storage._data) {
44 return storage.data[key];
45 } else if (storage.data instanceof Map) {
46 // Webpack v4
47 return storage.data.get(key);
48 } else {
49 return storage.data[key];
50 }
51}
52
53function setData(backendOrStorage, key, valueFactory) {
54 const value = valueFactory(backendOrStorage);
55
56 // Webpack v5
57 if (backendOrStorage._data instanceof Map) {
58 backendOrStorage._data.set(key, value);
59 } else if (backendOrStorage._data) {
60 backendOrStorage.data[key] = value;
61 } else if (backendOrStorage.data instanceof Map) {
62 // Webpack 4
63 backendOrStorage.data.set(key, value);
64 } else {
65 backendOrStorage.data[key] = value;
66 }
67}
68
69function getStatStorage(fileSystem) {
70 if (fileSystem._statStorage) {
71 // Webpack v4
72 return fileSystem._statStorage;
73 } else if (fileSystem._statBackend) {
74 // webpack v5
75 return fileSystem._statBackend;
76 } else {
77 // Unknown version?
78 throw new Error("Couldn't find a stat storage");
79 }
80}
81
82function getFileStorage(fileSystem) {
83 if (fileSystem._readFileStorage) {
84 // Webpack v4
85 return fileSystem._readFileStorage;
86 } else if (fileSystem._readFileBackend) {
87 // Webpack v5
88 return fileSystem._readFileBackend;
89 } else {
90 throw new Error("Couldn't find a readFileStorage");
91 }
92}
93
94function getReadDirBackend(fileSystem) {
95 if (fileSystem._readdirBackend) {
96 return fileSystem._readdirBackend;
97 } else if (fileSystem._readdirStorage) {
98 return fileSystem._readdirStorage;
99 } else {
100 throw new Error("Couldn't find a readDirStorage from Webpack Internals");
101 }
102}
103
104function getRealpathBackend(fileSystem) {
105 if (fileSystem._realpathBackend) {
106 return fileSystem._realpathBackend;
107 }
108
109 // Nothing, because not all version of webpack support it
110}
111
112class VirtualModulesPlugin {
113 private _staticModules: Record<string, string> | null;
114 private _compiler: Compiler | null = null;
115 private _watcher: any = null;
116
117 public constructor(modules?: Record<string, string>) {
118 this._staticModules = modules || null;
119 }
120
121 public getModuleList(filter: AvailableModules = ALL) {
122 let modules = {};
123 const shouldGetStaticModules = filter === ALL || filter === STATIC;
124 const shouldGetDynamicModules = filter === ALL || filter === DYNAMIC;
125
126 if (shouldGetStaticModules) {
127 // Get static modules
128 modules = {
129 ...modules,
130 ...this._staticModules,
131 };
132 }
133
134 if (shouldGetDynamicModules) {
135 // Get dynamic modules
136 const finalInputFileSystem: any = this._compiler?.inputFileSystem;
137 const virtualFiles = finalInputFileSystem?._virtualFiles ?? {};
138
139 const dynamicModules: Record<string, string> = {};
140 Object.keys(virtualFiles).forEach((key: string) => {
141 dynamicModules[key] = virtualFiles[key].contents;
142 });
143
144 modules = {
145 ...modules,
146 ...dynamicModules,
147 };
148 }
149
150 return modules;
151 }
152
153 public writeModule(filePath: string, contents: string): void {
154 if (!this._compiler) {
155 throw new Error(`Plugin has not been initialized`);
156 }
157
158 checkActivation(this);
159
160 const len = contents ? contents.length : 0;
161 const time = Date.now();
162 const date = new Date(time);
163
164 const stats = new VirtualStats({
165 dev: 8675309,
166 nlink: 0,
167 uid: 1000,
168 gid: 1000,
169 rdev: 0,
170 blksize: 4096,
171 ino: inode++,
172 mode: 33188,
173 size: len,
174 blocks: Math.floor(len / 4096),
175 atime: date,
176 mtime: date,
177 ctime: date,
178 birthtime: date,
179 });
180 const modulePath = getModulePath(filePath, this._compiler);
181
182 if (process.env.WVM_DEBUG)
183 // eslint-disable-next-line no-console
184 console.log(this._compiler.name, 'Write virtual module:', modulePath, contents);
185
186 // When using the WatchIgnorePlugin (https://github.com/webpack/webpack/blob/52184b897f40c75560b3630e43ca642fcac7e2cf/lib/WatchIgnorePlugin.js),
187 // the original watchFileSystem is stored in `wfs`. The following "unwraps" the ignoring
188 // wrappers, giving us access to the "real" watchFileSystem.
189 let finalWatchFileSystem = this._watcher && this._watcher.watchFileSystem;
190
191 while (finalWatchFileSystem && finalWatchFileSystem.wfs) {
192 finalWatchFileSystem = finalWatchFileSystem.wfs;
193 }
194
195 let finalInputFileSystem: any = this._compiler.inputFileSystem;
196 while (finalInputFileSystem && finalInputFileSystem._inputFileSystem) {
197 finalInputFileSystem = finalInputFileSystem._inputFileSystem;
198 }
199
200 finalInputFileSystem._writeVirtualFile(modulePath, stats, contents);
201 if (
202 finalWatchFileSystem &&
203 finalWatchFileSystem.watcher &&
204 (finalWatchFileSystem.watcher.fileWatchers.size || finalWatchFileSystem.watcher.fileWatchers.length)
205 ) {
206 const fileWatchers =
207 finalWatchFileSystem.watcher.fileWatchers instanceof Map
208 ? Array.from(finalWatchFileSystem.watcher.fileWatchers.values())
209 : finalWatchFileSystem.watcher.fileWatchers;
210 for (let fileWatcher of fileWatchers) {
211 if ('watcher' in fileWatcher) {
212 fileWatcher = fileWatcher.watcher;
213 }
214 if (fileWatcher.path === modulePath) {
215 if (process.env.DEBUG)
216 // eslint-disable-next-line no-console
217 console.log(this._compiler.name, 'Emit file change:', modulePath, time);
218 delete fileWatcher.directoryWatcher._cachedTimeInfoEntries;
219 fileWatcher.emit('change', time, null);
220 }
221 }
222 }
223 }
224
225 public apply(compiler: Compiler) {
226 this._compiler = compiler;
227
228 const afterEnvironmentHook = () => {
229 let finalInputFileSystem: any = compiler.inputFileSystem;
230 while (finalInputFileSystem && finalInputFileSystem._inputFileSystem) {
231 finalInputFileSystem = finalInputFileSystem._inputFileSystem;
232 }
233
234 if (!finalInputFileSystem._writeVirtualFile) {
235 const originalPurge = finalInputFileSystem.purge;
236
237 finalInputFileSystem.purge = () => {
238 originalPurge.apply(finalInputFileSystem, []);
239 if (finalInputFileSystem._virtualFiles) {
240 Object.keys(finalInputFileSystem._virtualFiles).forEach((file) => {
241 const data = finalInputFileSystem._virtualFiles[file];
242 finalInputFileSystem._writeVirtualFile(file, data.stats, data.contents);
243 });
244 }
245 };
246
247 finalInputFileSystem._writeVirtualFile = (file, stats, contents) => {
248 const statStorage = getStatStorage(finalInputFileSystem);
249 const fileStorage = getFileStorage(finalInputFileSystem);
250 const readDirStorage = getReadDirBackend(finalInputFileSystem);
251 const realPathStorage = getRealpathBackend(finalInputFileSystem);
252
253 finalInputFileSystem._virtualFiles = finalInputFileSystem._virtualFiles || {};
254 finalInputFileSystem._virtualFiles[file] = { stats: stats, contents: contents };
255 setData(statStorage, file, createWebpackData(stats));
256 setData(fileStorage, file, createWebpackData(contents));
257 const segments = file.split(/[\\/]/);
258 let count = segments.length - 1;
259 const minCount = segments[0] ? 1 : 0;
260 while (count > minCount) {
261 const dir = segments.slice(0, count).join(path.sep) || path.sep;
262 try {
263 finalInputFileSystem.readdirSync(dir);
264 } catch (e) {
265 const time = Date.now();
266 const dirStats = new VirtualStats({
267 dev: 8675309,
268 nlink: 0,
269 uid: 1000,
270 gid: 1000,
271 rdev: 0,
272 blksize: 4096,
273 ino: inode++,
274 mode: 16877,
275 size: stats.size,
276 blocks: Math.floor(stats.size / 4096),
277 atime: time,
278 mtime: time,
279 ctime: time,
280 birthtime: time,
281 });
282
283 setData(readDirStorage, dir, createWebpackData([]));
284 if (realPathStorage) {
285 setData(realPathStorage, dir, createWebpackData(dir));
286 }
287 setData(statStorage, dir, createWebpackData(dirStats));
288 }
289 let dirData = getData(getReadDirBackend(finalInputFileSystem), dir);
290 // Webpack v4 returns an array, webpack v5 returns an object
291 dirData = dirData[1] || dirData.result;
292 const filename = segments[count];
293 if (dirData.indexOf(filename) < 0) {
294 const files = dirData.concat([filename]).sort();
295 setData(getReadDirBackend(finalInputFileSystem), dir, createWebpackData(files));
296 } else {
297 break;
298 }
299 count--;
300 }
301 };
302 }
303 };
304 const afterResolversHook = () => {
305 if (this._staticModules) {
306 for (const [filePath, contents] of Object.entries(this._staticModules)) {
307 this.writeModule(filePath, contents);
308 }
309 this._staticModules = null;
310 }
311 };
312
313 // The webpack property is not exposed in webpack v4
314 const version = typeof (compiler as any).webpack === 'undefined' ? 4 : 5;
315
316 const watchRunHook = (watcher, callback) => {
317 this._watcher = watcher.compiler || watcher;
318 const virtualFiles = (compiler as any).inputFileSystem._virtualFiles;
319 const fts = compiler.fileTimestamps as any;
320
321 if (virtualFiles && fts && typeof fts.set === 'function') {
322 Object.keys(virtualFiles).forEach((file) => {
323 const mtime = +virtualFiles[file].stats.mtime;
324 // fts is
325 // Map<string, number> in webpack 4
326 // Map<string, { safeTime: number; timestamp: number; }> in webpack 5
327 fts.set(
328 file,
329 version === 4
330 ? mtime
331 : {
332 safeTime: mtime,
333 timestamp: mtime,
334 }
335 );
336 });
337 }
338 callback();
339 };
340
341 if (compiler.hooks) {
342 compiler.hooks.afterEnvironment.tap('VirtualModulesPlugin', afterEnvironmentHook);
343 compiler.hooks.afterResolvers.tap('VirtualModulesPlugin', afterResolversHook);
344 compiler.hooks.watchRun.tapAsync('VirtualModulesPlugin', watchRunHook);
345 } else {
346 (compiler as any).plugin('after-environment', afterEnvironmentHook);
347 (compiler as any).plugin('after-resolvers', afterResolversHook);
348 (compiler as any).plugin('watch-run', watchRunHook);
349 }
350 }
351}
352
353export = VirtualModulesPlugin;