forked from angular/angular-cli
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathbuilder.ts
171 lines (146 loc) · 5.65 KB
/
builder.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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
/**
* @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
*/
import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect';
import assert from 'node:assert';
import { randomUUID } from 'node:crypto';
import path from 'node:path';
import { createVirtualModulePlugin } from '../../tools/esbuild/virtual-module-plugin';
import { loadEsmModule } from '../../utils/load-esm';
import { buildApplicationInternal } from '../application';
import type {
ApplicationBuilderExtensions,
ApplicationBuilderInternalOptions,
} from '../application/options';
import { ResultKind } from '../application/results';
import { OutputHashing } from '../application/schema';
import { writeTestFiles } from '../karma/application_builder';
import { findTests, getTestEntrypoints } from '../karma/find-tests';
import { normalizeOptions } from './options';
import type { Schema as UnitTestOptions } from './schema';
export type { UnitTestOptions };
/**
* @experimental Direct usage of this function is considered experimental.
*/
export async function* execute(
options: UnitTestOptions,
context: BuilderContext,
extensions: ApplicationBuilderExtensions = {},
): AsyncIterable<BuilderOutput> {
// Determine project name from builder context target
const projectName = context.target?.project;
if (!projectName) {
context.logger.error(
`The "${context.builder.builderName}" builder requires a target to be specified.`,
);
return;
}
context.logger.warn(
`NOTE: The "${context.builder.builderName}" builder is currently EXPERIMENTAL and not ready for production use.`,
);
const normalizedOptions = await normalizeOptions(context, projectName, options);
const { projectSourceRoot, workspaceRoot, runnerName } = normalizedOptions;
if (runnerName !== 'vitest') {
context.logger.error('Unknown test runner: ' + runnerName);
return;
}
// Find test files
const testFiles = await findTests(
normalizedOptions.include,
normalizedOptions.exclude,
workspaceRoot,
projectSourceRoot,
);
if (testFiles.length === 0) {
context.logger.error('No tests found.');
return { success: false };
}
const entryPoints = getTestEntrypoints(testFiles, { projectSourceRoot, workspaceRoot });
entryPoints.set('init-testbed', 'angular:test-bed-init');
const { startVitest } = await loadEsmModule<typeof import('vitest/node')>('vitest/node');
// Setup test file build options based on application build target options
const buildTargetOptions = (await context.validateOptions(
await context.getTargetOptions(normalizedOptions.buildTarget),
await context.getBuilderNameForTarget(normalizedOptions.buildTarget),
)) as unknown as ApplicationBuilderInternalOptions;
if (buildTargetOptions.polyfills?.includes('zone.js')) {
buildTargetOptions.polyfills.push('zone.js/testing');
}
const outputPath = path.join(context.workspaceRoot, 'dist/test-out', randomUUID());
const buildOptions: ApplicationBuilderInternalOptions = {
...buildTargetOptions,
watch: normalizedOptions.watch,
outputPath,
index: false,
browser: undefined,
server: undefined,
localize: false,
budgets: [],
serviceWorker: false,
appShell: false,
ssr: false,
prerender: false,
sourceMap: { scripts: true, vendor: false, styles: false },
outputHashing: OutputHashing.None,
optimization: false,
tsConfig: normalizedOptions.tsConfig,
entryPoints,
externalDependencies: ['vitest', ...(buildTargetOptions.externalDependencies ?? [])],
};
extensions ??= {};
extensions.codePlugins ??= [];
const virtualTestBedInit = createVirtualModulePlugin({
namespace: 'angular:test-bed-init',
loadContent: async () => {
const contents: string[] = [
// Initialize the Angular testing environment
`import { getTestBed } from '@angular/core/testing';`,
`import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';`,
`getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting(), {`,
` errorOnUnknownElements: true,`,
` errorOnUnknownProperties: true,`,
'});',
];
return {
contents: contents.join('\n'),
loader: 'js',
resolveDir: projectSourceRoot,
};
},
});
extensions.codePlugins.unshift(virtualTestBedInit);
let instance: import('vitest/node').Vitest | undefined;
for await (const result of buildApplicationInternal(buildOptions, context, extensions)) {
if (result.kind === ResultKind.Failure) {
continue;
} else if (result.kind !== ResultKind.Full) {
assert.fail('A full build result is required from the application builder.');
}
assert(result.files, 'Builder did not provide result files.');
await writeTestFiles(result.files, outputPath);
const setupFiles = ['init-testbed.js'];
if (buildTargetOptions?.polyfills?.length) {
setupFiles.push('polyfills.js');
}
instance ??= await startVitest('test', undefined /* cliFilters */, undefined /* options */, {
test: {
root: outputPath,
setupFiles,
environment: 'jsdom',
watch: normalizedOptions.watch,
coverage: {
enabled: normalizedOptions.codeCoverage,
exclude: normalizedOptions.codeCoverageExclude,
excludeAfterRemap: true,
},
},
});
// Check if all the tests pass to calculate the result
const testModules = instance.state.getTestModules();
yield { success: testModules.every((testModule) => testModule.ok()) };
}
}