Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions goldens/public-api/angular_devkit/build_angular/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export type BrowserBuilderOptions = {
i18nDuplicateTranslation?: I18NTranslation;
i18nMissingTranslation?: I18NTranslation;
index: IndexUnion;
inlineStyleLanguage?: InlineStyleLanguage;
inlineStyleLanguage?: InlineStyleLanguage_2;
localize?: Localize;
main: string;
namedChunks?: boolean;
Expand All @@ -58,16 +58,16 @@ export type BrowserBuilderOptions = {
outputHashing?: OutputHashing;
outputPath: string;
poll?: number;
polyfills?: Polyfills;
polyfills?: Polyfills_2;
preserveSymlinks?: boolean;
progress?: boolean;
resourcesOutputPath?: string;
scripts?: ScriptElement[];
scripts?: ScriptElement_2[];
serviceWorker?: boolean;
sourceMap?: SourceMapUnion;
statsJson?: boolean;
stylePreprocessorOptions?: StylePreprocessorOptions;
styles?: StyleElement[];
styles?: StyleElement_2[];
subresourceIntegrity?: boolean;
tsConfig: string;
vendorChunk?: boolean;
Expand Down Expand Up @@ -216,18 +216,18 @@ export type KarmaBuilderOptions = {
exclude?: string[];
fileReplacements?: FileReplacement_2[];
include?: string[];
inlineStyleLanguage?: InlineStyleLanguage_2;
inlineStyleLanguage?: InlineStyleLanguage;
karmaConfig?: string;
main?: string;
poll?: number;
polyfills?: Polyfills_2;
polyfills?: Polyfills;
preserveSymlinks?: boolean;
progress?: boolean;
reporters?: string[];
scripts?: ScriptElement_2[];
scripts?: ScriptElement[];
sourceMap?: SourceMapUnion_2;
stylePreprocessorOptions?: StylePreprocessorOptions_2;
styles?: StyleElement_2[];
styles?: StyleElement[];
tsConfig: string;
watch?: boolean;
webWorkerTsConfig?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,16 @@
*/

import { purgeStaleBuildCache } from '@angular/build/private';
import { BuilderContext, BuilderOutput } from '@angular-devkit/architect';
import type { Config, ConfigOptions } from 'karma';
import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect';
import type { ConfigOptions, Server } from 'karma';
import * as path from 'node:path';
import { Observable, defaultIfEmpty, from, switchMap } from 'rxjs';
import { Configuration } from 'webpack';
import webpack, { Configuration } from 'webpack';
import { getCommonConfig, getStylesConfig } from '../../tools/webpack/configs';
import { ExecutionTransformer } from '../../transforms';
import type { ExecutionTransformer } from '../../transforms';
import { generateBrowserWebpackConfigFromContext } from '../../utils/webpack-browser-config';
import { Schema as BrowserBuilderOptions, OutputHashing } from '../browser/schema';
import { type Schema as BrowserBuilderOptions, OutputHashing } from '../browser/schema';
import { FindTestsPlugin } from './find-tests-plugin';
import { Schema as KarmaBuilderOptions } from './schema';
import type { Schema as KarmaBuilderOptions } from './schema';

export type KarmaConfigOptions = ConfigOptions & {
buildWebpack?: unknown;
Expand All @@ -33,9 +32,22 @@ export function execute(
// The karma options transform cannot be async without a refactor of the builder implementation
karmaOptions?: (options: KarmaConfigOptions) => KarmaConfigOptions;
} = {},
): Observable<BuilderOutput> {
return from(initializeBrowser(options, context, transforms.webpackConfiguration)).pipe(
switchMap(async ([karma, webpackConfig]) => {
): AsyncIterable<BuilderOutput> {
let karmaServer: Server;
let isCancelled = false;

return new ReadableStream({
async start(controller) {
const [karma, webpackConfig] = await initializeBrowser(
options,
context,
transforms.webpackConfiguration,
);

if (isCancelled) {
return;
}

const projectName = context.target?.project;
if (!projectName) {
throw new Error(`The 'karma' builder requires a target to be specified.`);
Expand Down Expand Up @@ -65,9 +77,31 @@ export function execute(
}),
);

const KARMA_APPLICATION_PATH = '_karma_webpack_';
webpackConfig.output ??= {};
webpackConfig.output.path = `/${KARMA_APPLICATION_PATH}/`;
webpackConfig.output.publicPath = `/${KARMA_APPLICATION_PATH}/`;

if (karmaOptions.singleRun) {
webpackConfig.plugins.unshift({
apply: (compiler: webpack.Compiler) => {
compiler.hooks.afterEnvironment.tap('karma', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
compiler.watchFileSystem = { watch: () => {} } as any;
});
},
});
}

// Remove the watch option to avoid the [DEP_WEBPACK_WATCH_WITHOUT_CALLBACK] warning.
// The compiler is initialized in watch mode by webpack-dev-middleware.
delete webpackConfig.watch;

const compiler = webpack(webpackConfig);

karmaOptions.buildWebpack = {
options,
webpackConfig,
compiler,
logger: context.logger,
};

Expand All @@ -77,38 +111,43 @@ export function execute(
{ promiseConfig: true, throwErrors: true },
);

return [karma, parsedKarmaConfig] as [typeof karma, KarmaConfigOptions];
}),
switchMap(
([karma, karmaConfig]) =>
new Observable<BuilderOutput>((subscriber) => {
// Pass onto Karma to emit BuildEvents.
karmaConfig.buildWebpack ??= {};
if (typeof karmaConfig.buildWebpack === 'object') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(karmaConfig.buildWebpack as any).failureCb ??= () =>
subscriber.next({ success: false });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(karmaConfig.buildWebpack as any).successCb ??= () =>
subscriber.next({ success: true });
}
if (isCancelled) {
return;
}

// Complete the observable once the Karma server returns.
const karmaServer = new karma.Server(karmaConfig as Config, (exitCode) => {
subscriber.next({ success: exitCode === 0 });
subscriber.complete();
});
const enqueue = (value: BuilderOutput) => {
try {
controller.enqueue(value);
} catch {
// Controller is already closed
}
};

const karmaStart = karmaServer.start();
const close = () => {
try {
controller.close();
} catch {
// Controller is already closed
}
};

// Cleanup, signal Karma to exit.
return () => {
void karmaStart.then(() => karmaServer.stop());
};
}),
),
defaultIfEmpty({ success: false }),
);
// Close the stream once the Karma server returns.
karmaServer = new karma.Server(parsedKarmaConfig, (exitCode) => {
enqueue({ success: exitCode === 0 });
close();
});

karmaServer.on('run_complete', (_, results) => {
enqueue({ success: results.exitCode === 0 });
});

await karmaServer.start();
},
async cancel() {
isCancelled = true;
await karmaServer?.stop();
},
});
}

async function initializeBrowser(
Expand Down
87 changes: 31 additions & 56 deletions packages/angular_devkit/build_angular/src/builders/karma/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import { strings } from '@angular-devkit/core';
import type { ConfigOptions } from 'karma';
import { createRequire } from 'node:module';
import * as path from 'node:path';
import { Observable, from, mergeMap } from 'rxjs';
import { Configuration } from 'webpack';
import { ExecutionTransformer } from '../../transforms';
import { normalizeFileReplacements } from '../../utils';
Expand All @@ -31,45 +30,46 @@ export type KarmaConfigOptions = ConfigOptions & {
/**
* @experimental Direct usage of this function is considered experimental.
*/
export function execute(
export async function* execute(
options: KarmaBuilderOptions,
context: BuilderContext,
transforms: {
webpackConfiguration?: ExecutionTransformer<Configuration>;
// The karma options transform cannot be async without a refactor of the builder implementation
karmaOptions?: (options: KarmaConfigOptions) => KarmaConfigOptions;
} = {},
): Observable<BuilderOutput> {
): AsyncIterable<BuilderOutput> {
// Check Angular version.
assertCompatibleAngularVersion(context.workspaceRoot);

return from(getExecuteWithBuilder(options, context)).pipe(
mergeMap(([useEsbuild, executeWithBuilder]) => {
if (useEsbuild) {
if (transforms.webpackConfiguration) {
context.logger.warn(
`This build is using the application builder but transforms.webpackConfiguration was provided. The transform will be ignored.`,
);
}

if (options.fileReplacements) {
options.fileReplacements = normalizeFileReplacements(options.fileReplacements, './');
}

if (typeof options.polyfills === 'string') {
options.polyfills = [options.polyfills];
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
return executeWithBuilder(options as any, context, transforms);
} else {
const karmaOptions = getBaseKarmaOptions(options, context);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
return executeWithBuilder(options as any, context, karmaOptions, transforms);
}
}),
);
if (await checkForEsbuild(options, context)) {
if (transforms.webpackConfiguration) {
context.logger.warn(
`This build is using the application builder but transforms.webpackConfiguration was provided. The transform will be ignored.`,
);
}

if (options.fileReplacements) {
options.fileReplacements = normalizeFileReplacements(options.fileReplacements, './');
}

if (typeof options.polyfills === 'string') {
options.polyfills = [options.polyfills];
}

const { executeKarmaBuilder } = await import('@angular/build');

yield* executeKarmaBuilder(
options as unknown as import('@angular/build').KarmaBuilderOptions,
context,
transforms,
);
} else {
const karmaOptions = getBaseKarmaOptions(options, context);
const { execute } = await import('./browser_builder');

yield* execute(options, context, karmaOptions, transforms);
}
}

function getBaseKarmaOptions(
Expand Down Expand Up @@ -169,32 +169,7 @@ function getBuiltInKarmaConfig(
}

export type { KarmaBuilderOptions };
export default createBuilder<Record<string, string> & KarmaBuilderOptions>(execute);

async function getExecuteWithBuilder(
options: KarmaBuilderOptions,
context: BuilderContext,
): Promise<
[
boolean,
(
| (typeof import('@angular/build'))['executeKarmaBuilder']
| (typeof import('./browser_builder'))['execute']
),
]
> {
const useEsbuild = await checkForEsbuild(options, context);
let execute;
if (useEsbuild) {
const { executeKarmaBuilder } = await import('@angular/build');
execute = executeKarmaBuilder;
} else {
const browserBuilderModule = await import('./browser_builder');
execute = browserBuilderModule.execute;
}

return [useEsbuild, execute];
}
export default createBuilder<KarmaBuilderOptions>(execute);

async function checkForEsbuild(
options: KarmaBuilderOptions,
Expand Down
29 changes: 24 additions & 5 deletions packages/angular_devkit/build_angular/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@
* found in the LICENSE file at https://angular.dev/license
*/

import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect';
import { type Observable, from } from 'rxjs';
import {
type KarmaBuilderOptions,
type KarmaConfigOptions,
execute as executeKarmaBuilderInternal,
} from './builders/karma';
import type { ExecutionTransformer } from './transforms';

export * from './transforms';

export { CrossOrigin, OutputHashing, Type } from './builders/browser/schema';
Expand Down Expand Up @@ -40,11 +49,21 @@ export {
type ExtractI18nBuilderOptions,
} from './builders/extract-i18n';

export {
execute as executeKarmaBuilder,
type KarmaBuilderOptions,
type KarmaConfigOptions,
} from './builders/karma';
/**
* @experimental Direct usage of this function is considered experimental.
*/
export function executeKarmaBuilder(
options: KarmaBuilderOptions,
context: BuilderContext,
transforms?: {
webpackConfiguration?: ExecutionTransformer<import('webpack').Configuration>;
karmaOptions?: (options: KarmaConfigOptions) => KarmaConfigOptions;
},
): Observable<BuilderOutput> {
return from(executeKarmaBuilderInternal(options, context, transforms));
}

export { type KarmaBuilderOptions, type KarmaConfigOptions };

export {
execute as executeProtractorBuilder,
Expand Down
Loading