mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-17 11:03:53 +08:00
fix(@angular-devkit/build-angular): remove double-watch in karma
The Karma file watching was racing with the file writes done by the application builder. Since we already tell Karma when to reun via `.refeshFiles()`, disabling Karma's own file watcher should make things more reliable. This allows removing a weird special-case in the test case and removes the noisy "File chaned" logs generated by Karma. Fixes https://github.com/angular/angular-cli/issues/28755
This commit is contained in:
parent
18b6aea397
commit
faabbbf910
@ -9,6 +9,7 @@
|
|||||||
import { BuildOutputFileType } from '@angular/build';
|
import { BuildOutputFileType } from '@angular/build';
|
||||||
import {
|
import {
|
||||||
ApplicationBuilderInternalOptions,
|
ApplicationBuilderInternalOptions,
|
||||||
|
Result,
|
||||||
ResultFile,
|
ResultFile,
|
||||||
ResultKind,
|
ResultKind,
|
||||||
buildApplicationInternal,
|
buildApplicationInternal,
|
||||||
@ -42,6 +43,7 @@ class ApplicationBuildError extends Error {
|
|||||||
function injectKarmaReporter(
|
function injectKarmaReporter(
|
||||||
context: BuilderContext,
|
context: BuilderContext,
|
||||||
buildOptions: BuildOptions,
|
buildOptions: BuildOptions,
|
||||||
|
buildIterator: AsyncIterator<Result>,
|
||||||
karmaConfig: Config & ConfigOptions,
|
karmaConfig: Config & ConfigOptions,
|
||||||
subscriber: Subscriber<BuilderOutput>,
|
subscriber: Subscriber<BuilderOutput>,
|
||||||
) {
|
) {
|
||||||
@ -64,13 +66,15 @@ function injectKarmaReporter(
|
|||||||
|
|
||||||
private startWatchingBuild() {
|
private startWatchingBuild() {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
for await (const buildOutput of buildApplicationInternal(
|
// This is effectively "for await of but skip what's already consumed".
|
||||||
{
|
let isDone = false; // to mark the loop condition as "not constant".
|
||||||
...buildOptions,
|
while (!isDone) {
|
||||||
watch: true,
|
const { done, value: buildOutput } = await buildIterator.next();
|
||||||
},
|
if (done) {
|
||||||
context,
|
isDone = true;
|
||||||
)) {
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (buildOutput.kind === ResultKind.Failure) {
|
if (buildOutput.kind === ResultKind.Failure) {
|
||||||
subscriber.next({ success: false, message: 'Build failed' });
|
subscriber.next({ success: false, message: 'Build failed' });
|
||||||
} else if (
|
} else if (
|
||||||
@ -121,12 +125,12 @@ export function execute(
|
|||||||
): Observable<BuilderOutput> {
|
): Observable<BuilderOutput> {
|
||||||
return from(initializeApplication(options, context, karmaOptions, transforms)).pipe(
|
return from(initializeApplication(options, context, karmaOptions, transforms)).pipe(
|
||||||
switchMap(
|
switchMap(
|
||||||
([karma, karmaConfig, buildOptions]) =>
|
([karma, karmaConfig, buildOptions, buildIterator]) =>
|
||||||
new Observable<BuilderOutput>((subscriber) => {
|
new Observable<BuilderOutput>((subscriber) => {
|
||||||
// If `--watch` is explicitly enabled or if we are keeping the Karma
|
// If `--watch` is explicitly enabled or if we are keeping the Karma
|
||||||
// process running, we should hook Karma into the build.
|
// process running, we should hook Karma into the build.
|
||||||
if (options.watch ?? !karmaConfig.singleRun) {
|
if (buildIterator) {
|
||||||
injectKarmaReporter(context, buildOptions, karmaConfig, subscriber);
|
injectKarmaReporter(context, buildOptions, buildIterator, karmaConfig, subscriber);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Complete the observable once the Karma server returns.
|
// Complete the observable once the Karma server returns.
|
||||||
@ -199,7 +203,9 @@ async function initializeApplication(
|
|||||||
webpackConfiguration?: ExecutionTransformer<Configuration>;
|
webpackConfiguration?: ExecutionTransformer<Configuration>;
|
||||||
karmaOptions?: (options: ConfigOptions) => ConfigOptions;
|
karmaOptions?: (options: ConfigOptions) => ConfigOptions;
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<[typeof import('karma'), Config & ConfigOptions, BuildOptions]> {
|
): Promise<
|
||||||
|
[typeof import('karma'), Config & ConfigOptions, BuildOptions, AsyncIterator<Result> | null]
|
||||||
|
> {
|
||||||
if (transforms.webpackConfiguration) {
|
if (transforms.webpackConfiguration) {
|
||||||
context.logger.warn(
|
context.logger.warn(
|
||||||
`This build is using the application builder but transforms.webpackConfiguration was provided. The transform will be ignored.`,
|
`This build is using the application builder but transforms.webpackConfiguration was provided. The transform will be ignored.`,
|
||||||
@ -247,10 +253,14 @@ async function initializeApplication(
|
|||||||
styles: options.styles,
|
styles: options.styles,
|
||||||
polyfills: normalizePolyfills(options.polyfills),
|
polyfills: normalizePolyfills(options.polyfills),
|
||||||
webWorkerTsConfig: options.webWorkerTsConfig,
|
webWorkerTsConfig: options.webWorkerTsConfig,
|
||||||
|
watch: options.watch ?? !karmaOptions.singleRun,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build tests with `application` builder, using test files as entry points.
|
// Build tests with `application` builder, using test files as entry points.
|
||||||
const buildOutput = await first(buildApplicationInternal(buildOptions, context));
|
const [buildOutput, buildIterator] = await first(
|
||||||
|
buildApplicationInternal(buildOptions, context),
|
||||||
|
{ cancel: !buildOptions.watch },
|
||||||
|
);
|
||||||
if (buildOutput.kind === ResultKind.Failure) {
|
if (buildOutput.kind === ResultKind.Failure) {
|
||||||
throw new ApplicationBuildError('Build failed');
|
throw new ApplicationBuildError('Build failed');
|
||||||
} else if (buildOutput.kind !== ResultKind.Full) {
|
} else if (buildOutput.kind !== ResultKind.Full) {
|
||||||
@ -265,28 +275,33 @@ async function initializeApplication(
|
|||||||
karmaOptions.files ??= [];
|
karmaOptions.files ??= [];
|
||||||
karmaOptions.files.push(
|
karmaOptions.files.push(
|
||||||
// Serve polyfills first.
|
// Serve polyfills first.
|
||||||
{ pattern: `${outputPath}/polyfills.js`, type: 'module' },
|
{ pattern: `${outputPath}/polyfills.js`, type: 'module', watched: false },
|
||||||
// Serve global setup script.
|
// Serve global setup script.
|
||||||
{ pattern: `${outputPath}/${mainName}.js`, type: 'module' },
|
{ pattern: `${outputPath}/${mainName}.js`, type: 'module', watched: false },
|
||||||
// Serve all source maps.
|
// Serve all source maps.
|
||||||
{ pattern: `${outputPath}/*.map`, included: false },
|
{ pattern: `${outputPath}/*.map`, included: false, watched: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hasChunkOrWorkerFiles(buildOutput.files)) {
|
if (hasChunkOrWorkerFiles(buildOutput.files)) {
|
||||||
karmaOptions.files.push(
|
karmaOptions.files.push(
|
||||||
// Allow loading of chunk-* files but don't include them all on load.
|
// Allow loading of chunk-* files but don't include them all on load.
|
||||||
{ pattern: `${outputPath}/{chunk,worker}-*.js`, type: 'module', included: false },
|
{
|
||||||
|
pattern: `${outputPath}/{chunk,worker}-*.js`,
|
||||||
|
type: 'module',
|
||||||
|
included: false,
|
||||||
|
watched: false,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
karmaOptions.files.push(
|
karmaOptions.files.push(
|
||||||
// Serve remaining JS on page load, these are the test entrypoints.
|
// Serve remaining JS on page load, these are the test entrypoints.
|
||||||
{ pattern: `${outputPath}/*.js`, type: 'module' },
|
{ pattern: `${outputPath}/*.js`, type: 'module', watched: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (options.styles?.length) {
|
if (options.styles?.length) {
|
||||||
// Serve CSS outputs on page load, these are the global styles.
|
// Serve CSS outputs on page load, these are the global styles.
|
||||||
karmaOptions.files.push({ pattern: `${outputPath}/*.css`, type: 'css' });
|
karmaOptions.files.push({ pattern: `${outputPath}/*.css`, type: 'css', watched: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedKarmaConfig: Config & ConfigOptions = await karma.config.parseConfig(
|
const parsedKarmaConfig: Config & ConfigOptions = await karma.config.parseConfig(
|
||||||
@ -327,7 +342,7 @@ async function initializeApplication(
|
|||||||
parsedKarmaConfig.reporters = (parsedKarmaConfig.reporters ?? []).concat(['coverage']);
|
parsedKarmaConfig.reporters = (parsedKarmaConfig.reporters ?? []).concat(['coverage']);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [karma, parsedKarmaConfig, buildOptions];
|
return [karma, parsedKarmaConfig, buildOptions, buildIterator];
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasChunkOrWorkerFiles(files: Record<string, unknown>): boolean {
|
function hasChunkOrWorkerFiles(files: Record<string, unknown>): boolean {
|
||||||
@ -364,9 +379,22 @@ export async function writeTestFiles(files: Record<string, ResultFile>, testDir:
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the first item yielded by the given generator and cancels the execution. */
|
/** Returns the first item yielded by the given generator and cancels the execution. */
|
||||||
async function first<T>(generator: AsyncIterable<T>): Promise<T> {
|
async function first<T>(
|
||||||
|
generator: AsyncIterable<T>,
|
||||||
|
{ cancel }: { cancel: boolean },
|
||||||
|
): Promise<[T, AsyncIterator<T> | null]> {
|
||||||
|
if (!cancel) {
|
||||||
|
const iterator: AsyncIterator<T> = generator[Symbol.asyncIterator]();
|
||||||
|
const firstValue = await iterator.next();
|
||||||
|
if (firstValue.done) {
|
||||||
|
throw new Error('Expected generator to emit at least once.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [firstValue.value, iterator];
|
||||||
|
}
|
||||||
|
|
||||||
for await (const value of generator) {
|
for await (const value of generator) {
|
||||||
return value;
|
return [value, null];
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('Expected generator to emit at least once.');
|
throw new Error('Expected generator to emit at least once.');
|
||||||
|
@ -6,12 +6,12 @@
|
|||||||
* found in the LICENSE file at https://angular.dev/license
|
* found in the LICENSE file at https://angular.dev/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { concatMap, count, debounceTime, take, timeout } from 'rxjs';
|
import { concatMap, count, debounceTime, distinctUntilChanged, take, timeout } from 'rxjs';
|
||||||
import { execute } from '../../index';
|
import { execute } from '../../index';
|
||||||
import { BASE_OPTIONS, KARMA_BUILDER_INFO, describeKarmaBuilder } from '../setup';
|
import { BASE_OPTIONS, KARMA_BUILDER_INFO, describeKarmaBuilder } from '../setup';
|
||||||
import { BuilderOutput } from '@angular-devkit/architect';
|
import { BuilderOutput } from '@angular-devkit/architect';
|
||||||
|
|
||||||
describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget, isApplicationBuilder) => {
|
describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget) => {
|
||||||
describe('Behavior: "Rebuilds"', () => {
|
describe('Behavior: "Rebuilds"', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await setupTarget(harness);
|
await setupTarget(harness);
|
||||||
@ -33,31 +33,31 @@ describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget, isAppli
|
|||||||
async (result) => {
|
async (result) => {
|
||||||
// Karma run should succeed.
|
// Karma run should succeed.
|
||||||
// Add a compilation error.
|
// Add a compilation error.
|
||||||
expect(result?.success).toBeTrue();
|
expect(result?.success).withContext('Initial test run should succeed').toBeTrue();
|
||||||
// Add an syntax error to a non-main file.
|
// Add an syntax error to a non-main file.
|
||||||
await harness.appendToFile('src/app/app.component.spec.ts', `error`);
|
await harness.appendToFile('src/app/app.component.spec.ts', `error`);
|
||||||
},
|
},
|
||||||
async (result) => {
|
async (result) => {
|
||||||
expect(result?.success).toBeFalse();
|
expect(result?.success)
|
||||||
|
.withContext('Test should fail after build error was introduced')
|
||||||
|
.toBeFalse();
|
||||||
await harness.writeFile('src/app/app.component.spec.ts', goodFile);
|
await harness.writeFile('src/app/app.component.spec.ts', goodFile);
|
||||||
},
|
},
|
||||||
async (result) => {
|
async (result) => {
|
||||||
expect(result?.success).toBeTrue();
|
expect(result?.success)
|
||||||
|
.withContext('Test should succeed again after build error was fixed')
|
||||||
|
.toBeTrue();
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
if (isApplicationBuilder) {
|
|
||||||
expectedSequence.unshift(async (result) => {
|
|
||||||
// This is the initial Karma run, it should succeed.
|
|
||||||
// For simplicity, we trigger a run the first time we build in watch mode.
|
|
||||||
expect(result?.success).toBeTrue();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildCount = await harness
|
const buildCount = await harness
|
||||||
.execute({ outputLogsOnFailure: false })
|
.execute({ outputLogsOnFailure: false })
|
||||||
.pipe(
|
.pipe(
|
||||||
timeout(60000),
|
timeout(60000),
|
||||||
debounceTime(500),
|
debounceTime(500),
|
||||||
|
// There may be a sequence of {success:true} events that should be
|
||||||
|
// de-duplicated.
|
||||||
|
distinctUntilChanged((prev, current) => prev.result?.success === current.result?.success),
|
||||||
concatMap(async ({ result }, index) => {
|
concatMap(async ({ result }, index) => {
|
||||||
await expectedSequence[index](result);
|
await expectedSequence[index](result);
|
||||||
}),
|
}),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user