compilationCache.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.addToCompilationCache = addToCompilationCache;
  6. exports.belongsToNodeModules = belongsToNodeModules;
  7. exports.clearCompilationCache = clearCompilationCache;
  8. exports.collectAffectedTestFiles = collectAffectedTestFiles;
  9. exports.currentFileDepsCollector = currentFileDepsCollector;
  10. exports.dependenciesForTestFile = dependenciesForTestFile;
  11. exports.fileDependenciesForTest = fileDependenciesForTest;
  12. exports.getFromCompilationCache = getFromCompilationCache;
  13. exports.getUserData = getUserData;
  14. exports.installSourceMapSupportIfNeeded = installSourceMapSupportIfNeeded;
  15. exports.internalDependenciesForTestFile = internalDependenciesForTestFile;
  16. exports.serializeCompilationCache = serializeCompilationCache;
  17. exports.setExternalDependencies = setExternalDependencies;
  18. exports.startCollectingFileDeps = startCollectingFileDeps;
  19. exports.stopCollectingFileDeps = stopCollectingFileDeps;
  20. var _fs = _interopRequireDefault(require("fs"));
  21. var _os = _interopRequireDefault(require("os"));
  22. var _path = _interopRequireDefault(require("path"));
  23. var _utilsBundle = require("../utilsBundle");
  24. var _globals = require("../common/globals");
  25. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  26. /**
  27. * Copyright (c) Microsoft Corporation.
  28. *
  29. * Licensed under the Apache License, Version 2.0 (the "License");
  30. * you may not use this file except in compliance with the License.
  31. * You may obtain a copy of the License at
  32. *
  33. * http://www.apache.org/licenses/LICENSE-2.0
  34. *
  35. * Unless required by applicable law or agreed to in writing, software
  36. * distributed under the License is distributed on an "AS IS" BASIS,
  37. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  38. * See the License for the specific language governing permissions and
  39. * limitations under the License.
  40. */
  41. // Assumptions for the compilation cache:
  42. // - Files in the temp directory we work with can disappear at any moment, either some of them or all together.
  43. // - Multiple workers can be trying to read from the compilation cache at the same time.
  44. // - There is a single invocation of the test runner at a time.
  45. //
  46. // Therefore, we implement the following logic:
  47. // - Never assume that file is present, always try to read it to determine whether it's actually present.
  48. // - Never write to the cache from worker processes to avoid "multiple writers" races.
  49. // - Since we perform all static imports in the runner beforehand, most of the time
  50. // workers should be able to read from the cache.
  51. // - For workers-only dynamic imports or some cache problems, we will re-transpile files in
  52. // each worker anew.
  53. const cacheDir = process.env.PWTEST_CACHE_DIR || (() => {
  54. if (process.platform === 'win32') return _path.default.join(_os.default.tmpdir(), `playwright-transform-cache`);
  55. // Use `geteuid()` instead of more natural `os.userInfo().username`
  56. // since `os.userInfo()` is not always available.
  57. // Note: `process.geteuid()` is not available on windows.
  58. // See https://github.com/microsoft/playwright/issues/22721
  59. return _path.default.join(_os.default.tmpdir(), `playwright-transform-cache-` + process.geteuid());
  60. })();
  61. const sourceMaps = new Map();
  62. const memoryCache = new Map();
  63. // Dependencies resolved by the loader.
  64. const fileDependencies = new Map();
  65. // Dependencies resolved by the external bundler.
  66. const externalDependencies = new Map();
  67. let sourceMapSupportInstalled = false;
  68. function installSourceMapSupportIfNeeded() {
  69. if (sourceMapSupportInstalled) return;
  70. sourceMapSupportInstalled = true;
  71. Error.stackTraceLimit = 200;
  72. _utilsBundle.sourceMapSupport.install({
  73. environment: 'node',
  74. handleUncaughtExceptions: false,
  75. retrieveSourceMap(source) {
  76. if (!sourceMaps.has(source)) return null;
  77. const sourceMapPath = sourceMaps.get(source);
  78. try {
  79. return {
  80. map: JSON.parse(_fs.default.readFileSync(sourceMapPath, 'utf-8')),
  81. url: source
  82. };
  83. } catch {
  84. return null;
  85. }
  86. }
  87. });
  88. }
  89. function _innerAddToCompilationCache(filename, entry) {
  90. sourceMaps.set(entry.moduleUrl || filename, entry.sourceMapPath);
  91. memoryCache.set(filename, entry);
  92. }
  93. function getFromCompilationCache(filename, hash, moduleUrl) {
  94. // First check the memory cache by filename, this cache will always work in the worker,
  95. // because we just compiled this file in the loader.
  96. const cache = memoryCache.get(filename);
  97. if (cache !== null && cache !== void 0 && cache.codePath) {
  98. try {
  99. return {
  100. cachedCode: _fs.default.readFileSync(cache.codePath, 'utf-8')
  101. };
  102. } catch {
  103. // Not able to read the file - fall through.
  104. }
  105. }
  106. // Then do the disk cache, this cache works between the Playwright Test runs.
  107. const cachePath = calculateCachePath(filename, hash);
  108. const codePath = cachePath + '.js';
  109. const sourceMapPath = cachePath + '.map';
  110. const dataPath = cachePath + '.data';
  111. try {
  112. const cachedCode = _fs.default.readFileSync(codePath, 'utf8');
  113. _innerAddToCompilationCache(filename, {
  114. codePath,
  115. sourceMapPath,
  116. dataPath,
  117. moduleUrl
  118. });
  119. return {
  120. cachedCode
  121. };
  122. } catch {}
  123. return {
  124. addToCache: (code, map, data) => {
  125. if ((0, _globals.isWorkerProcess)()) return;
  126. _fs.default.mkdirSync(_path.default.dirname(cachePath), {
  127. recursive: true
  128. });
  129. if (map) _fs.default.writeFileSync(sourceMapPath, JSON.stringify(map), 'utf8');
  130. if (data.size) _fs.default.writeFileSync(dataPath, JSON.stringify(Object.fromEntries(data.entries()), undefined, 2), 'utf8');
  131. _fs.default.writeFileSync(codePath, code, 'utf8');
  132. _innerAddToCompilationCache(filename, {
  133. codePath,
  134. sourceMapPath,
  135. dataPath,
  136. moduleUrl
  137. });
  138. }
  139. };
  140. }
  141. function serializeCompilationCache() {
  142. return {
  143. sourceMaps: [...sourceMaps.entries()],
  144. memoryCache: [...memoryCache.entries()],
  145. fileDependencies: [...fileDependencies.entries()].map(([filename, deps]) => [filename, [...deps]]),
  146. externalDependencies: [...externalDependencies.entries()].map(([filename, deps]) => [filename, [...deps]])
  147. };
  148. }
  149. function clearCompilationCache() {
  150. sourceMaps.clear();
  151. memoryCache.clear();
  152. }
  153. function addToCompilationCache(payload) {
  154. for (const entry of payload.sourceMaps) sourceMaps.set(entry[0], entry[1]);
  155. for (const entry of payload.memoryCache) memoryCache.set(entry[0], entry[1]);
  156. for (const entry of payload.fileDependencies) fileDependencies.set(entry[0], new Set(entry[1]));
  157. for (const entry of payload.externalDependencies) externalDependencies.set(entry[0], new Set(entry[1]));
  158. }
  159. function calculateCachePath(filePath, hash) {
  160. const fileName = _path.default.basename(filePath, _path.default.extname(filePath)).replace(/\W/g, '') + '_' + hash;
  161. return _path.default.join(cacheDir, hash[0] + hash[1], fileName);
  162. }
  163. // Since ESM and CJS collect dependencies differently,
  164. // we go via the global state to collect them.
  165. let depsCollector;
  166. function startCollectingFileDeps() {
  167. depsCollector = new Set();
  168. }
  169. function stopCollectingFileDeps(filename) {
  170. if (!depsCollector) return;
  171. depsCollector.delete(filename);
  172. for (const dep of depsCollector) {
  173. if (belongsToNodeModules(dep)) depsCollector.delete(dep);
  174. }
  175. fileDependencies.set(filename, depsCollector);
  176. depsCollector = undefined;
  177. }
  178. function currentFileDepsCollector() {
  179. return depsCollector;
  180. }
  181. function setExternalDependencies(filename, deps) {
  182. const depsSet = new Set(deps.filter(dep => !belongsToNodeModules(dep) && dep !== filename));
  183. externalDependencies.set(filename, depsSet);
  184. }
  185. function fileDependenciesForTest() {
  186. return fileDependencies;
  187. }
  188. function collectAffectedTestFiles(dependency, testFileCollector) {
  189. testFileCollector.add(dependency);
  190. for (const [testFile, deps] of fileDependencies) {
  191. if (deps.has(dependency)) testFileCollector.add(testFile);
  192. }
  193. for (const [testFile, deps] of externalDependencies) {
  194. if (deps.has(dependency)) testFileCollector.add(testFile);
  195. }
  196. }
  197. function internalDependenciesForTestFile(filename) {
  198. return fileDependencies.get(filename);
  199. }
  200. function dependenciesForTestFile(filename) {
  201. const result = new Set();
  202. for (const dep of fileDependencies.get(filename) || []) result.add(dep);
  203. for (const dep of externalDependencies.get(filename) || []) result.add(dep);
  204. return result;
  205. }
  206. // These two are only used in the dev mode, they are specifically excluding
  207. // files from packages/playwright*. In production mode, node_modules covers
  208. // that.
  209. const kPlaywrightInternalPrefix = _path.default.resolve(__dirname, '../../../playwright');
  210. const kPlaywrightCoveragePrefix = _path.default.resolve(__dirname, '../../../../tests/config/coverage.js');
  211. function belongsToNodeModules(file) {
  212. if (file.includes(`${_path.default.sep}node_modules${_path.default.sep}`)) return true;
  213. if (file.startsWith(kPlaywrightInternalPrefix) && file.endsWith('.js')) return true;
  214. if (file.startsWith(kPlaywrightCoveragePrefix) && file.endsWith('.js')) return true;
  215. return false;
  216. }
  217. async function getUserData(pluginName) {
  218. const result = new Map();
  219. for (const [fileName, cache] of memoryCache) {
  220. if (!cache.dataPath) continue;
  221. if (!_fs.default.existsSync(cache.dataPath)) continue;
  222. const data = JSON.parse(await _fs.default.promises.readFile(cache.dataPath, 'utf8'));
  223. if (data[pluginName]) result.set(fileName, data[pluginName]);
  224. }
  225. return result;
  226. }