a20b0cc6c0a51bba4a49e4fab5ad5a90b571387a702dedf3ccdaeab5e905980a9af92f9e8d00026c2791af840a3d0af78f181f60a31072b7d874d42fc7f4f2 11 KB

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