Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit d1dd3a8

Browse files
committed
refactor(@angular/build): improve error handling for unit-test builder setup
Error handling has been enhanced to provide more actionable feedback for common misconfigurations, such as an invalid `buildTarget` or a malformed test runner package.
1 parent b720554 commit d1dd3a8

File tree

1 file changed

+125
-68
lines changed
  • packages/angular/build/src/builders/unit-test

1 file changed

+125
-68
lines changed

‎packages/angular/build/src/builders/unit-test/builder.ts‎

Lines changed: 125 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect';
9+
import {
10+
type BuilderContext,
11+
type BuilderOutput,
12+
targetStringFromTarget,
13+
} from '@angular-devkit/architect';
1014
import assert from 'node:assert';
1115
import { createVirtualModulePlugin } from '../../tools/esbuild/virtual-module-plugin';
1216
import { assertIsError } from '../../utils/error';
@@ -22,6 +26,101 @@ import type { Schema as UnitTestBuilderOptions } from './schema';
2226

2327
export type { UnitTestBuilderOptions };
2428

29+
async function loadTestRunner(runnerName: string): Promise<TestRunner> {
30+
// Harden against directory traversal
31+
if (!/^[a-zA-Z0-9-]+$/.test(runnerName)) {
32+
throw new Error(
33+
`Invalid runner name "${runnerName}". Runner names can only contain alphanumeric characters and hyphens.`,
34+
);
35+
}
36+
37+
let runnerModule;
38+
try {
39+
runnerModule = await import(`./runners/${runnerName}/index`);
40+
} catch (e) {
41+
assertIsError(e);
42+
if (e.code === 'ERR_MODULE_NOT_FOUND') {
43+
throw new Error(`Unknown test runner "${runnerName}".`);
44+
}
45+
throw new Error(
46+
`Failed to load the '${runnerName}' test runner. The package may be corrupted or improperly installed.\n` +
47+
`Error: ${e.message}`,
48+
);
49+
}
50+
51+
const runner = runnerModule.default;
52+
if (
53+
!runner ||
54+
typeof runner.getBuildOptions !== 'function' ||
55+
typeof runner.createExecutor !== 'function'
56+
) {
57+
throw new Error(
58+
`The loaded test runner '${runnerName}' does not appear to be a valid TestRunner implementation.`,
59+
);
60+
}
61+
62+
return runner;
63+
}
64+
65+
function prepareBuildExtensions(
66+
virtualFiles: Record<string, string> | undefined,
67+
projectSourceRoot: string,
68+
extensions?: ApplicationBuilderExtensions,
69+
): ApplicationBuilderExtensions | undefined {
70+
if (!virtualFiles) {
71+
return extensions;
72+
}
73+
74+
extensions ??= {};
75+
extensions.codePlugins ??= [];
76+
for (const [namespace, contents] of Object.entries(virtualFiles)) {
77+
extensions.codePlugins.push(
78+
createVirtualModulePlugin({
79+
namespace,
80+
loadContent: () => {
81+
return {
82+
contents,
83+
loader: 'js',
84+
resolveDir: projectSourceRoot,
85+
};
86+
},
87+
}),
88+
);
89+
}
90+
91+
return extensions;
92+
}
93+
94+
async function* runBuildAndTest(
95+
executor: import('./runners/api').TestExecutor,
96+
applicationBuildOptions: ApplicationBuilderInternalOptions,
97+
context: BuilderContext,
98+
extensions: ApplicationBuilderExtensions | undefined,
99+
): AsyncIterable<BuilderOutput> {
100+
for await (const buildResult of buildApplicationInternal(
101+
applicationBuildOptions,
102+
context,
103+
extensions,
104+
)) {
105+
if (buildResult.kind === ResultKind.Failure) {
106+
yield { success: false };
107+
continue;
108+
} else if (
109+
buildResult.kind !== ResultKind.Full &&
110+
buildResult.kind !== ResultKind.Incremental
111+
) {
112+
assert.fail(
113+
'A full and/or incremental build result is required from the application builder.',
114+
);
115+
}
116+
117+
assert(buildResult.files, 'Builder did not provide result files.');
118+
119+
// Pass the build artifacts to the executor
120+
yield* executor.execute(buildResult);
121+
}
122+
}
123+
25124
/**
26125
* @experimental Direct usage of this function is considered experimental.
27126
*/
@@ -43,24 +142,8 @@ export async function* execute(
43142
);
44143

45144
const normalizedOptions = await normalizeOptions(context, projectName, options);
46-
const { runnerName, projectSourceRoot } = normalizedOptions;
47-
48-
// Dynamically load the requested runner
49-
let runner: TestRunner;
50-
try {
51-
const { default: runnerModule } = await import(`./runners/${runnerName}/index`);
52-
runner = runnerModule;
53-
} catch (e) {
54-
assertIsError(e);
55-
if (e.code !== 'ERR_MODULE_NOT_FOUND') {
56-
throw e;
57-
}
58-
context.logger.error(`Unknown test runner "${runnerName}".`);
145+
const runner = await loadTestRunner(normalizedOptions.runnerName);
59146

60-
return;
61-
}
62-
63-
// Create the stateful executor once
64147
await using executor = await runner.createExecutor(context, normalizedOptions);
65148

66149
if (runner.isStandalone) {
@@ -73,68 +156,42 @@ export async function* execute(
73156
}
74157

75158
// Get base build options from the buildTarget
76-
const buildTargetOptions = (await context.validateOptions(
77-
await context.getTargetOptions(normalizedOptions.buildTarget),
78-
await context.getBuilderNameForTarget(normalizedOptions.buildTarget),
79-
)) as unknown as ApplicationBuilderInternalOptions;
159+
let buildTargetOptions: ApplicationBuilderInternalOptions;
160+
try {
161+
buildTargetOptions = (await context.validateOptions(
162+
await context.getTargetOptions(normalizedOptions.buildTarget),
163+
await context.getBuilderNameForTarget(normalizedOptions.buildTarget),
164+
)) as unknown as ApplicationBuilderInternalOptions;
165+
} catch (e) {
166+
assertIsError(e);
167+
context.logger.error(
168+
`Could not load build target options for "${targetStringFromTarget(normalizedOptions.buildTarget)}".\n` +
169+
`Please check your 'angular.json' configuration.\n` +
170+
`Error: ${e.message}`,
171+
);
172+
173+
return;
174+
}
80175

81176
// Get runner-specific build options from the hook
82177
const { buildOptions: runnerBuildOptions, virtualFiles } = await runner.getBuildOptions(
83178
normalizedOptions,
84179
buildTargetOptions,
85180
);
86181

87-
if (virtualFiles) {
88-
extensions ??= {};
89-
extensions.codePlugins ??= [];
90-
for (const [namespace, contents] of Object.entries(virtualFiles)) {
91-
extensions.codePlugins.push(
92-
createVirtualModulePlugin({
93-
namespace,
94-
loadContent: () => {
95-
return {
96-
contents,
97-
loader: 'js',
98-
resolveDir: projectSourceRoot,
99-
};
100-
},
101-
}),
102-
);
103-
}
104-
}
105-
106-
const { watch, tsConfig } = normalizedOptions;
182+
const finalExtensions = prepareBuildExtensions(
183+
virtualFiles,
184+
normalizedOptions.projectSourceRoot,
185+
extensions,
186+
);
107187

108188
// Prepare and run the application build
109189
const applicationBuildOptions = {
110-
// Base options
111190
...buildTargetOptions,
112-
watch,
113-
tsConfig,
114-
// Runner specific
115191
...runnerBuildOptions,
192+
watch: normalizedOptions.watch,
193+
tsConfig: normalizedOptions.tsConfig,
116194
} satisfies ApplicationBuilderInternalOptions;
117195

118-
for await (const buildResult of buildApplicationInternal(
119-
applicationBuildOptions,
120-
context,
121-
extensions,
122-
)) {
123-
if (buildResult.kind === ResultKind.Failure) {
124-
yield { success: false };
125-
continue;
126-
} else if (
127-
buildResult.kind !== ResultKind.Full &&
128-
buildResult.kind !== ResultKind.Incremental
129-
) {
130-
assert.fail(
131-
'A full and/or incremental build result is required from the application builder.',
132-
);
133-
}
134-
135-
assert(buildResult.files, 'Builder did not provide result files.');
136-
137-
// Pass the build artifacts to the executor
138-
yield* executor.execute(buildResult);
139-
}
196+
yield* runBuildAndTest(executor, applicationBuildOptions, context, finalExtensions);
140197
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /