/
find-tests-plugin.ts
157 lines (130 loc) · 4.63 KB
/
find-tests-plugin.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
/**
* @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.io/license
*/
import assert from 'assert';
import { PathLike, constants, promises as fs } from 'fs';
import glob, { hasMagic } from 'glob';
import { basename, dirname, extname, join, relative } from 'path';
import { promisify } from 'util';
import type { Compilation, Compiler } from 'webpack';
import { addError } from '../../utils/webpack-diagnostics';
const globPromise = promisify(glob);
/**
* The name of the plugin provided to Webpack when tapping Webpack compiler hooks.
*/
const PLUGIN_NAME = 'angular-find-tests-plugin';
export interface FindTestsPluginOptions {
include?: string[];
workspaceRoot: string;
projectSourceRoot: string;
}
export class FindTestsPlugin {
private compilation: Compilation | undefined;
constructor(private options: FindTestsPluginOptions) {}
apply(compiler: Compiler): void {
const { include = ['**/*.spec.ts'], projectSourceRoot, workspaceRoot } = this.options;
const webpackOptions = compiler.options;
const entry =
typeof webpackOptions.entry === 'function' ? webpackOptions.entry() : webpackOptions.entry;
let originalImport: string[] | undefined;
// Add tests files are part of the entry-point.
webpackOptions.entry = async () => {
const specFiles = await findTests(include, workspaceRoot, projectSourceRoot);
if (!specFiles.length) {
assert(this.compilation, 'Compilation cannot be undefined.');
addError(
this.compilation,
`Specified patterns: "${include.join(', ')}" did not match any spec files.`,
);
}
const entrypoints = await entry;
const entrypoint = entrypoints['main'];
if (!entrypoint.import) {
throw new Error(`Cannot find 'main' entrypoint.`);
}
originalImport ??= entrypoint.import;
entrypoint.import = [...originalImport, ...specFiles];
return entrypoints;
};
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
this.compilation = compilation;
compilation.contextDependencies.add(projectSourceRoot);
});
}
}
// go through all patterns and find unique list of files
async function findTests(
patterns: string[],
workspaceRoot: string,
projectSourceRoot: string,
): Promise<string[]> {
const matchingTestsPromises = patterns.map((pattern) =>
findMatchingTests(pattern, workspaceRoot, projectSourceRoot),
);
const files = await Promise.all(matchingTestsPromises);
// Unique file names
return [...new Set(files.flat())];
}
const normalizePath = (path: string): string => path.replace(/\\/g, '/');
async function findMatchingTests(
pattern: string,
workspaceRoot: string,
projectSourceRoot: string,
): Promise<string[]> {
// normalize pattern, glob lib only accepts forward slashes
let normalizedPattern = normalizePath(pattern);
if (normalizedPattern.charAt(0) === '/') {
normalizedPattern = normalizedPattern.substring(1);
}
const relativeProjectRoot = normalizePath(relative(workspaceRoot, projectSourceRoot) + '/');
// remove relativeProjectRoot to support relative paths from root
// such paths are easy to get when running scripts via IDEs
if (normalizedPattern.startsWith(relativeProjectRoot)) {
normalizedPattern = normalizedPattern.substring(relativeProjectRoot.length);
}
// special logic when pattern does not look like a glob
if (!hasMagic(normalizedPattern)) {
if (await isDirectory(join(projectSourceRoot, normalizedPattern))) {
normalizedPattern = `${normalizedPattern}/**/*.spec.@(ts|tsx)`;
} else {
// see if matching spec file exists
const fileExt = extname(normalizedPattern);
// Replace extension to `.spec.ext`. Example: `src/app/app.component.ts`-> `src/app/app.component.spec.ts`
const potentialSpec = join(
projectSourceRoot,
dirname(normalizedPattern),
`${basename(normalizedPattern, fileExt)}.spec${fileExt}`,
);
if (await exists(potentialSpec)) {
return [potentialSpec];
}
}
}
return globPromise(normalizedPattern, {
cwd: projectSourceRoot,
root: projectSourceRoot,
nomount: true,
absolute: true,
ignore: ['**/node_modules/**'],
});
}
async function isDirectory(path: PathLike): Promise<boolean> {
try {
const stats = await fs.stat(path);
return stats.isDirectory();
} catch {
return false;
}
}
async function exists(path: PathLike): Promise<boolean> {
try {
await fs.access(path, constants.F_OK);
return true;
} catch {
return false;
}
}