projectUtils.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.buildDependentProjects = buildDependentProjects;
  6. exports.buildProjectsClosure = buildProjectsClosure;
  7. exports.buildTeardownToSetupsMap = buildTeardownToSetupsMap;
  8. exports.collectFilesForProject = collectFilesForProject;
  9. exports.filterProjects = filterProjects;
  10. var _fs = _interopRequireDefault(require("fs"));
  11. var _path = _interopRequireDefault(require("path"));
  12. var _utilsBundle = require("playwright-core/lib/utilsBundle");
  13. var _util = require("util");
  14. var _util2 = require("../util");
  15. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  16. /**
  17. * Copyright Microsoft Corporation. All rights reserved.
  18. *
  19. * Licensed under the Apache License, Version 2.0 (the "License");
  20. * you may not use this file except in compliance with the License.
  21. * You may obtain a copy of the License at
  22. *
  23. * http://www.apache.org/licenses/LICENSE-2.0
  24. *
  25. * Unless required by applicable law or agreed to in writing, software
  26. * distributed under the License is distributed on an "AS IS" BASIS,
  27. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  28. * See the License for the specific language governing permissions and
  29. * limitations under the License.
  30. */
  31. const readFileAsync = (0, _util.promisify)(_fs.default.readFile);
  32. const readDirAsync = (0, _util.promisify)(_fs.default.readdir);
  33. function filterProjects(projects, projectNames) {
  34. if (!projectNames) return [...projects];
  35. const projectsToFind = new Set();
  36. const unknownProjects = new Map();
  37. projectNames.forEach(n => {
  38. const name = n.toLocaleLowerCase();
  39. projectsToFind.add(name);
  40. unknownProjects.set(name, n);
  41. });
  42. const result = projects.filter(project => {
  43. const name = project.project.name.toLocaleLowerCase();
  44. unknownProjects.delete(name);
  45. return projectsToFind.has(name);
  46. });
  47. if (unknownProjects.size) {
  48. const names = projects.map(p => p.project.name).filter(name => !!name);
  49. if (!names.length) throw new Error(`No named projects are specified in the configuration file`);
  50. const unknownProjectNames = Array.from(unknownProjects.values()).map(n => `"${n}"`).join(', ');
  51. throw new Error(`Project(s) ${unknownProjectNames} not found. Available named projects: ${names.map(name => `"${name}"`).join(', ')}`);
  52. }
  53. return result;
  54. }
  55. function buildTeardownToSetupsMap(projects) {
  56. const result = new Map();
  57. for (const project of projects) {
  58. if (project.teardown) {
  59. const setups = result.get(project.teardown) || [];
  60. setups.push(project);
  61. result.set(project.teardown, setups);
  62. }
  63. }
  64. return result;
  65. }
  66. function buildProjectsClosure(projects, hasTests) {
  67. const result = new Map();
  68. const visit = (depth, project) => {
  69. if (depth > 100) {
  70. const error = new Error('Circular dependency detected between projects.');
  71. error.stack = '';
  72. throw error;
  73. }
  74. if (depth === 0 && hasTests && !hasTests(project)) return;
  75. if (result.get(project) !== 'dependency') result.set(project, depth ? 'dependency' : 'top-level');
  76. for (const dep of project.deps) visit(depth + 1, dep);
  77. if (project.teardown) visit(depth + 1, project.teardown);
  78. };
  79. for (const p of projects) visit(0, p);
  80. return result;
  81. }
  82. function buildDependentProjects(forProjects, projects) {
  83. const reverseDeps = new Map(projects.map(p => [p, []]));
  84. for (const project of projects) {
  85. for (const dep of project.deps) reverseDeps.get(dep).push(project);
  86. }
  87. const result = new Set();
  88. const visit = (depth, project) => {
  89. if (depth > 100) {
  90. const error = new Error('Circular dependency detected between projects.');
  91. error.stack = '';
  92. throw error;
  93. }
  94. result.add(project);
  95. for (const reverseDep of reverseDeps.get(project)) visit(depth + 1, reverseDep);
  96. if (project.teardown) visit(depth + 1, project.teardown);
  97. };
  98. for (const forProject of forProjects) visit(0, forProject);
  99. return result;
  100. }
  101. async function collectFilesForProject(project, fsCache = new Map()) {
  102. const extensions = new Set(['.js', '.ts', '.mjs', '.mts', '.cjs', '.cts', '.jsx', '.tsx', '.mjsx', '.mtsx', '.cjsx', '.ctsx']);
  103. const testFileExtension = file => extensions.has(_path.default.extname(file));
  104. const allFiles = await cachedCollectFiles(project.project.testDir, project.respectGitIgnore, fsCache);
  105. const testMatch = (0, _util2.createFileMatcher)(project.project.testMatch);
  106. const testIgnore = (0, _util2.createFileMatcher)(project.project.testIgnore);
  107. const testFiles = allFiles.filter(file => {
  108. if (!testFileExtension(file)) return false;
  109. const isTest = !testIgnore(file) && testMatch(file);
  110. if (!isTest) return false;
  111. return true;
  112. });
  113. return testFiles;
  114. }
  115. async function cachedCollectFiles(testDir, respectGitIgnore, fsCache) {
  116. const key = testDir + ':' + respectGitIgnore;
  117. let result = fsCache.get(key);
  118. if (!result) {
  119. result = await collectFiles(testDir, respectGitIgnore);
  120. fsCache.set(key, result);
  121. }
  122. return result;
  123. }
  124. async function collectFiles(testDir, respectGitIgnore) {
  125. if (!_fs.default.existsSync(testDir)) return [];
  126. if (!_fs.default.statSync(testDir).isDirectory()) return [];
  127. const checkIgnores = (entryPath, rules, isDirectory, parentStatus) => {
  128. let status = parentStatus;
  129. for (const rule of rules) {
  130. const ruleIncludes = rule.negate;
  131. if (status === 'included' === ruleIncludes) continue;
  132. const relative = _path.default.relative(rule.dir, entryPath);
  133. if (rule.match('/' + relative) || rule.match(relative)) {
  134. // Matches "/dir/file" or "dir/file"
  135. status = ruleIncludes ? 'included' : 'ignored';
  136. } else if (isDirectory && (rule.match('/' + relative + '/') || rule.match(relative + '/'))) {
  137. // Matches "/dir/subdir/" or "dir/subdir/" for directories.
  138. status = ruleIncludes ? 'included' : 'ignored';
  139. } else if (isDirectory && ruleIncludes && (rule.match('/' + relative, true) || rule.match(relative, true))) {
  140. // Matches "/dir/donotskip/" when "/dir" is excluded, but "!/dir/donotskip/file" is included.
  141. status = 'ignored-but-recurse';
  142. }
  143. }
  144. return status;
  145. };
  146. const files = [];
  147. const visit = async (dir, rules, status) => {
  148. const entries = await readDirAsync(dir, {
  149. withFileTypes: true
  150. });
  151. entries.sort((a, b) => a.name.localeCompare(b.name));
  152. if (respectGitIgnore) {
  153. const gitignore = entries.find(e => e.isFile() && e.name === '.gitignore');
  154. if (gitignore) {
  155. const content = await readFileAsync(_path.default.join(dir, gitignore.name), 'utf8');
  156. const newRules = content.split(/\r?\n/).map(s => {
  157. s = s.trim();
  158. if (!s) return;
  159. // Use flipNegate, because we handle negation ourselves.
  160. const rule = new _utilsBundle.minimatch.Minimatch(s, {
  161. matchBase: true,
  162. dot: true,
  163. flipNegate: true
  164. });
  165. if (rule.comment) return;
  166. rule.dir = dir;
  167. return rule;
  168. }).filter(rule => !!rule);
  169. rules = [...rules, ...newRules];
  170. }
  171. }
  172. for (const entry of entries) {
  173. if (entry.name === '.' || entry.name === '..') continue;
  174. if (entry.isFile() && entry.name === '.gitignore') continue;
  175. if (entry.isDirectory() && entry.name === 'node_modules') continue;
  176. const entryPath = _path.default.join(dir, entry.name);
  177. const entryStatus = checkIgnores(entryPath, rules, entry.isDirectory(), status);
  178. if (entry.isDirectory() && entryStatus !== 'ignored') await visit(entryPath, rules, entryStatus);else if (entry.isFile() && entryStatus === 'included') files.push(entryPath);
  179. }
  180. };
  181. await visit(testDir, [], 'included');
  182. return files;
  183. }