Skip to content
Merged
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
1 change: 1 addition & 0 deletions goldens/public-api/angular/build/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ export type UnitTestBuilderOptions = {
codeCoverageExclude?: string[];
codeCoverageReporters?: SchemaCodeCoverageReporter[];
debug?: boolean;
dumpVirtualFiles?: boolean;
exclude?: string[];
filter?: string;
include?: string[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as fs from 'node:fs/promises';
import path from 'node:path';
import { ReadableStream } from 'node:stream/web';
import { createVirtualModulePlugin } from '../../tools/esbuild/virtual-module-plugin';
import { writeTestFiles } from '../../utils/test-files';
import { buildApplicationInternal } from '../application/index';
import { ApplicationBuilderInternalOptions } from '../application/options';
import { Result, ResultKind } from '../application/results';
Expand All @@ -30,7 +31,6 @@ import {
getProjectSourceRoot,
hasChunkOrWorkerFiles,
normalizePolyfills,
writeTestFiles,
} from './utils';
import type { KarmaBuilderTransformsOptions } from './index';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
import type { BuilderOutput } from '@angular-devkit/architect';
import type { Config, ConfigOptions } from 'karma';
import type { ReadableStreamController } from 'node:stream/web';
import { writeTestFiles } from '../../utils/test-files';
import type { ApplicationBuilderInternalOptions } from '../application/options';
import type { Result } from '../application/results';
import { ResultKind } from '../application/results';
import type { LatestBuildFiles } from './assets-middleware';
import { writeTestFiles } from './utils';

const LATEST_BUILD_FILES_TOKEN = 'angularLatestBuildFiles';

Expand Down
34 changes: 0 additions & 34 deletions packages/angular/build/src/builders/karma/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,9 @@
*/

import type { BuilderContext } from '@angular-devkit/architect';
import * as fs from 'node:fs/promises';
import { createRequire } from 'node:module';
import path from 'node:path';
import { BuildOutputFileType } from '../../tools/esbuild/bundler-context';
import { emitFilesToDisk } from '../../tools/esbuild/utils';
import { getProjectRootPaths } from '../../utils/project-metadata';
import { ResultFile } from '../application/results';
import { findTests, getTestEntrypoints } from './find-tests';
import type { NormalizedKarmaBuilderOptions } from './options';

Expand Down Expand Up @@ -72,36 +68,6 @@ export function hasChunkOrWorkerFiles(files: Record<string, unknown>): boolean {
});
}

export async function writeTestFiles(
files: Record<string, ResultFile>,
testDir: string,
): Promise<void> {
const directoryExists = new Set<string>();
// Writes the test related output files to disk and ensures the containing directories are present
await emitFilesToDisk(Object.entries(files), async ([filePath, file]) => {
if (file.type !== BuildOutputFileType.Browser && file.type !== BuildOutputFileType.Media) {
return;
}

const fullFilePath = path.join(testDir, filePath);

// Ensure output subdirectories exist
const fileBasePath = path.dirname(fullFilePath);
if (fileBasePath && !directoryExists.has(fileBasePath)) {
await fs.mkdir(fileBasePath, { recursive: true });
directoryExists.add(fileBasePath);
}

if (file.origin === 'memory') {
// Write file contents
await fs.writeFile(fullFilePath, file.contents);
} else {
// Copy file contents
await fs.copyFile(file.inputPath, fullFilePath, fs.constants.COPYFILE_FICLONE);
}
});
}

/** Returns the first item yielded by the given generator and cancels the execution. */
export async function first<T>(
generator: AsyncIterable<T>,
Expand Down
30 changes: 29 additions & 1 deletion packages/angular/build/src/builders/unit-test/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ import {
targetStringFromTarget,
} from '@angular-devkit/architect';
import assert from 'node:assert';
import { rm } from 'node:fs/promises';
import path from 'node:path';
import { createVirtualModulePlugin } from '../../tools/esbuild/virtual-module-plugin';
import { assertIsError } from '../../utils/error';
import { writeTestFiles } from '../../utils/test-files';
import { buildApplicationInternal } from '../application';
import type {
ApplicationBuilderExtensions,
Expand Down Expand Up @@ -96,6 +99,7 @@ async function* runBuildAndTest(
executor: import('./runners/api').TestExecutor,
applicationBuildOptions: ApplicationBuilderInternalOptions,
context: BuilderContext,
dumpDirectory: string | undefined,
extensions: ApplicationBuilderExtensions | undefined,
): AsyncIterable<BuilderOutput> {
let consecutiveErrorCount = 0;
Expand All @@ -118,6 +122,20 @@ async function* runBuildAndTest(

assert(buildResult.files, 'Builder did not provide result files.');

if (dumpDirectory) {
if (buildResult.kind === ResultKind.Full) {
// Full build, so clean the directory
await rm(dumpDirectory, { recursive: true, force: true });
} else {
// Incremental build, so delete removed files
for (const file of buildResult.removed) {
await rm(path.join(dumpDirectory, file.path), { force: true });
}
}
await writeTestFiles(buildResult.files, dumpDirectory);
context.logger.info(`Build output files successfully dumped to '${dumpDirectory}'.`);
}

// Pass the build artifacts to the executor
try {
yield* executor.execute(buildResult);
Expand Down Expand Up @@ -263,7 +281,17 @@ export async function* execute(
progress: normalizedOptions.buildProgress ?? buildTargetOptions.progress,
} satisfies ApplicationBuilderInternalOptions;

yield* runBuildAndTest(executor, applicationBuildOptions, context, finalExtensions);
const dumpDirectory = normalizedOptions.dumpVirtualFiles
? path.join(normalizedOptions.cacheOptions.path, 'unit-test', 'output-files')
: undefined;

yield* runBuildAndTest(
executor,
applicationBuildOptions,
context,
dumpDirectory,
finalExtensions,
);
} catch (e) {
assertIsError(e);
context.logger.error(
Expand Down
1 change: 1 addition & 0 deletions packages/angular/build/src/builders/unit-test/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export async function normalizeOptions(
setupFiles: options.setupFiles
? options.setupFiles.map((setupFile) => path.join(workspaceRoot, setupFile))
: [],
dumpVirtualFiles: options.dumpVirtualFiles,
};
}

Expand Down
6 changes: 6 additions & 0 deletions packages/angular/build/src/builders/unit-test/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,12 @@
"progress": {
"type": "boolean",
"description": "Shows build progress information in the console. Defaults to the `progress` setting of the specified `buildTarget`."
},
"dumpVirtualFiles": {
"type": "boolean",
"description": "Dumps build output files to the `.angular/cache` directory for debugging purposes.",
"default": false,
"visible": false
}
},
"additionalProperties": false,
Expand Down
51 changes: 51 additions & 0 deletions packages/angular/build/src/utils/test-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* @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 * as fs from 'node:fs/promises';
import path from 'node:path';
import { ResultFile } from '../builders/application/results';
import { BuildOutputFileType } from '../tools/esbuild/bundler-context';
import { emitFilesToDisk } from '../tools/esbuild/utils';

/**
* Writes a collection of build result files to a specified directory.
* This function handles both in-memory and on-disk files, creating subdirectories
* as needed.
*
* @param files A map of file paths to `ResultFile` objects, representing the build output.
* @param testDir The absolute path to the directory where the files should be written.
*/
export async function writeTestFiles(
files: Record<string, ResultFile>,
testDir: string,
): Promise<void> {
const directoryExists = new Set<string>();
// Writes the test related output files to disk and ensures the containing directories are present
await emitFilesToDisk(Object.entries(files), async ([filePath, file]) => {
if (file.type !== BuildOutputFileType.Browser && file.type !== BuildOutputFileType.Media) {
return;
}

const fullFilePath = path.join(testDir, filePath);

// Ensure output subdirectories exist
const fileBasePath = path.dirname(fullFilePath);
if (fileBasePath && !directoryExists.has(fileBasePath)) {
await fs.mkdir(fileBasePath, { recursive: true });
directoryExists.add(fileBasePath);
}

if (file.origin === 'memory') {
// Write file contents
await fs.writeFile(fullFilePath, file.contents);
} else {
// Copy file contents
await fs.copyFile(file.inputPath, fullFilePath, fs.constants.COPYFILE_FICLONE);
}
});
}