@angular/build
Version:
Official build system for Angular
272 lines • 11.5 kB
JavaScript
;
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.findTests = findTests;
exports.getTestEntrypoints = getTestEntrypoints;
exports.generateNameFromPath = generateNameFromPath;
const node_crypto_1 = require("node:crypto");
const node_fs_1 = require("node:fs");
const node_os_1 = __importDefault(require("node:os"));
const node_path_1 = require("node:path");
const tinyglobby_1 = require("tinyglobby");
const path_1 = require("../../utils/path");
/**
* An array of file infix notations that identify a file as a test file.
* For example, `.spec` in `app.component.spec.ts`.
*/
const TEST_FILE_INFIXES = ['.spec', '.test'];
/** Maximum length for a generated test entrypoint name. */
const MAX_FILENAME_LENGTH = 128;
/**
* Finds all test files in the project. This function implements a special handling
* for static paths (non-globs) to improve developer experience. For example, if a
* user provides a path to a component, this function will find the corresponding
* test file. If a user provides a path to a directory, it will find all test
* files within that directory.
*
* @param include Glob patterns of files to include.
* @param exclude Glob patterns of files to exclude.
* @param workspaceRoot The absolute path to the workspace root.
* @param projectSourceRoot The absolute path to the project's source root.
* @returns A unique set of absolute paths to all test files.
*/
async function findTests(include, exclude, workspaceRoot, projectSourceRoot) {
const resolvedTestFiles = new Set();
const dynamicPatterns = [];
const projectRootPrefix = (0, path_1.toPosixPath)((0, node_path_1.relative)(workspaceRoot, projectSourceRoot) + '/');
const normalizedExcludes = exclude.map((p) => normalizePattern(p, projectRootPrefix));
// 1. Separate static and dynamic patterns
for (const pattern of include) {
const normalized = normalizePattern(pattern, projectRootPrefix);
if ((0, tinyglobby_1.isDynamicPattern)(pattern)) {
dynamicPatterns.push(normalized);
}
else {
const { resolved, unresolved } = await resolveStaticPattern(normalized, projectSourceRoot);
resolved.forEach((file) => resolvedTestFiles.add(file));
unresolved.forEach((p) => dynamicPatterns.push(p));
}
}
// 2. Execute a single glob for all dynamic patterns
if (dynamicPatterns.length > 0) {
const globMatches = await (0, tinyglobby_1.glob)(dynamicPatterns, {
cwd: projectSourceRoot,
absolute: true,
expandDirectories: false,
ignore: ['**/node_modules/**', ...normalizedExcludes],
});
for (const match of globMatches) {
resolvedTestFiles.add((0, path_1.toPosixPath)(match));
}
}
// 3. Combine and de-duplicate results
return [...resolvedTestFiles];
}
/**
* Generates unique, dash-delimited bundle names for a set of test files.
* This is used to create distinct output files for each test.
*
* @param testFiles An array of absolute paths to test files.
* @param options Configuration options for generating entry points.
* @returns A map where keys are the generated unique bundle names and values are the original file paths.
*/
function getTestEntrypoints(testFiles, { projectSourceRoot, workspaceRoot, removeTestExtension }) {
const seen = new Set();
const roots = [projectSourceRoot, workspaceRoot];
return new Map(Array.from(testFiles, (testFile) => {
const fileName = generateNameFromPath(testFile, roots, !!removeTestExtension);
const baseName = `spec-${fileName}`;
let uniqueName = baseName;
let suffix = 2;
while (seen.has(uniqueName)) {
uniqueName = `${baseName}-${suffix}`.replace(/([^\w](?:spec|test))-([\d]+)$/, '-$2$1');
++suffix;
}
seen.add(uniqueName);
return [uniqueName, testFile];
}));
}
/**
* Generates a unique, dash-delimited name from a file path. This is used to
* create a consistent and readable bundle name for a given test file.
*
* @param testFile The absolute path to the test file.
* @param roots An array of root paths to remove from the beginning of the test file path.
* @param removeTestExtension Whether to remove the test file infix and extension from the result.
* @returns A dash-cased name derived from the relative path of the test file.
*/
function generateNameFromPath(testFile, roots, removeTestExtension) {
const relativePath = removeRoots(testFile, roots.map(path_1.toPosixPath));
let startIndex = 0;
// Skip leading dots and slashes
while (startIndex < relativePath.length && /^[./\\]$/.test(relativePath[startIndex])) {
startIndex++;
}
let endIndex = relativePath.length;
if (removeTestExtension) {
const infixes = TEST_FILE_INFIXES.map((p) => p.substring(1)).join('|');
const match = relativePath.match(new RegExp(`\\.(${infixes})\\.[^.]+$`));
if (match?.index) {
endIndex = match.index;
}
}
else {
const extIndex = relativePath.lastIndexOf('.');
if (extIndex > startIndex) {
endIndex = extIndex;
}
}
// Build the final string in a single pass
let result = '';
for (let i = startIndex; i < endIndex; i++) {
const char = relativePath[i];
result += char === '/' || char === '\\' ? '-' : char;
}
return truncateName(result, relativePath);
}
/**
* Truncates a generated name if it exceeds the maximum allowed filename length.
* If truncation occurs, the name will be shortened by replacing a middle segment
* with an 8-character SHA256 hash of the original full path to maintain uniqueness.
*
* @param name The generated name to potentially truncate.
* @param originalPath The original full path from which the name was derived. Used for hashing.
* @returns The original name if within limits, or a truncated name with a hash.
*/
function truncateName(name, originalPath) {
if (name.length <= MAX_FILENAME_LENGTH) {
return name;
}
const hash = (0, node_crypto_1.createHash)('sha256').update(originalPath).digest('hex').substring(0, 8);
const availableLength = MAX_FILENAME_LENGTH - hash.length - 2; // 2 for '-' separators
const prefixLength = Math.floor(availableLength / 2);
const suffixLength = availableLength - prefixLength;
return `${name.substring(0, prefixLength)}-${hash}-${name.substring(name.length - suffixLength)}`;
}
/**
* Whether the current operating system's filesystem is case-insensitive.
*/
const isCaseInsensitiveFilesystem = node_os_1.default.platform() === 'win32' || node_os_1.default.platform() === 'darwin';
/**
* Removes a prefix from the beginning of a string, with conditional case-insensitivity
* based on the operating system's filesystem characteristics.
*
* @param text The string to remove the prefix from.
* @param prefix The prefix to remove.
* @returns The string with the prefix removed, or the original string if the prefix was not found.
*/
function removePrefix(text, prefix) {
if (isCaseInsensitiveFilesystem) {
if (text.toLowerCase().startsWith(prefix.toLowerCase())) {
return text.substring(prefix.length);
}
}
else {
if (text.startsWith(prefix)) {
return text.substring(prefix.length);
}
}
return text;
}
/**
* Removes potential root paths from a file path, returning a relative path.
* If no root path matches, it returns the file's basename.
*
* @param path The file path to process.
* @param roots An array of root paths to attempt to remove.
* @returns A relative path.
*/
function removeRoots(path, roots) {
for (const root of roots) {
const result = removePrefix(path, root);
// If the prefix was removed, the result will be a different string.
if (result !== path) {
return result;
}
}
return (0, node_path_1.basename)(path);
}
/**
* Normalizes a glob pattern by converting it to a POSIX path, removing leading
* slashes, and making it relative to the project source root.
*
* @param pattern The glob pattern to normalize.
* @param projectRootPrefix The POSIX-formatted prefix of the project's source root relative to the workspace root.
* @returns A normalized glob pattern.
*/
function normalizePattern(pattern, projectRootPrefix) {
const posixPattern = (0, path_1.toPosixPath)(pattern);
// Do not modify absolute paths. The globber will handle them correctly.
if ((0, node_path_1.isAbsolute)(posixPattern)) {
return posixPattern;
}
// For relative paths, ensure they are correctly relative to the project source root.
// This involves removing the project root prefix if the user provided a workspace-relative path.
const normalizedRelative = removePrefix(posixPattern, projectRootPrefix);
return normalizedRelative;
}
/**
* Resolves a static (non-glob) path.
*
* If the path is a directory, it returns a glob pattern to find all test files
* within that directory.
*
* If the path is a file, it attempts to find a corresponding test file by
* checking for files with the same name and a test infix (e.g., `.spec.ts`).
*
* If no corresponding test file is found, the original path is returned as an
* unresolved pattern.
*
* @param pattern The static path pattern.
* @param projectSourceRoot The absolute path to the project's source root.
* @returns A promise that resolves to an object containing resolved spec files and unresolved patterns.
*/
async function resolveStaticPattern(pattern, projectSourceRoot) {
const fullPath = (0, node_path_1.isAbsolute)(pattern) ? pattern : (0, node_path_1.join)(projectSourceRoot, pattern);
if (await isDirectory(fullPath)) {
const infixes = TEST_FILE_INFIXES.map((p) => p.substring(1)).join('|');
return { resolved: [], unresolved: [`${pattern}/**/*.@(${infixes}).@(ts|tsx)`] };
}
const fileExt = (0, node_path_1.extname)(fullPath);
const baseName = (0, node_path_1.basename)(fullPath, fileExt);
for (const infix of TEST_FILE_INFIXES) {
const potentialSpec = (0, node_path_1.join)((0, node_path_1.dirname)(fullPath), `${baseName}${infix}${fileExt}`);
if (await exists(potentialSpec)) {
return { resolved: [(0, path_1.toPosixPath)(potentialSpec)], unresolved: [] };
}
}
if (await exists(fullPath)) {
return { resolved: [(0, path_1.toPosixPath)(fullPath)], unresolved: [] };
}
return { resolved: [], unresolved: [(0, path_1.toPosixPath)(pattern)] };
}
/** Checks if a path exists and is a directory. */
async function isDirectory(path) {
try {
const stats = await node_fs_1.promises.stat(path);
return stats.isDirectory();
}
catch {
return false;
}
}
/** Checks if a path exists on the file system. */
async function exists(path) {
try {
await node_fs_1.promises.access(path, node_fs_1.constants.F_OK);
return true;
}
catch {
return false;
}
}
//# sourceMappingURL=test-discovery.js.map