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
51 changes: 35 additions & 16 deletions libs/native-federation/src/builders/build/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
createAngularBuildAdapter,
setMemResultHandler,
} from '../../utils/angular-esbuild-adapter';
import { resolveInstrumentationFilter } from '../../utils/coverage-instrumentation';

import { JsonObject } from '@angular-devkit/core';
import { existsSync, mkdirSync, rmSync } from 'fs';
Expand Down Expand Up @@ -78,17 +79,24 @@ process.stderr.write = function (
return originalWrite(chunk, encodingOrCallback as BufferEncoding, callback);
};

function _buildApplication(options, context, pluginsOrExtensions) {
let extensions;
if (pluginsOrExtensions && Array.isArray(pluginsOrExtensions)) {
extensions = {
codePlugins: pluginsOrExtensions,
};
} else {
extensions = pluginsOrExtensions;
}
return buildApplicationInternal(options, context, extensions);
}
const createInternalAngularBuilder =
(opts?: { instrumentForCoverage?: (request: string) => boolean }) =>
(options, context, pluginsOrExtensions) => {
let extensions;
if (pluginsOrExtensions && Array.isArray(pluginsOrExtensions)) {
extensions = {
codePlugins: pluginsOrExtensions,
};
} else {
extensions = pluginsOrExtensions;
}

if (opts?.instrumentForCoverage) {
options.instrumentForCoverage = opts.instrumentForCoverage;
}

return buildApplicationInternal(options, context, extensions);
};

export async function* runBuilder(
nfOptions: NfBuilderSchema,
Expand Down Expand Up @@ -373,13 +381,18 @@ export async function* runBuilder(

options.deleteOutputPath = false;

const instrumentForCoverage = await resolveInstrumentationFilter(context, {
instrumentForCoverage: nfOptions.instrumentForCoverage,
codeCoverageExclude: nfOptions.codeCoverageExclude,
});

const appBuilderName = '@angular/build:application';

const builderRun = runServer
? serveWithVite(
serverOptions,
appBuilderName,
_buildApplication,
createInternalAngularBuilder({ instrumentForCoverage }),
context,
nfOptions.skipHtmlTransform
? {}
Expand All @@ -389,10 +402,16 @@ export async function* runBuilder(
middleware,
},
)
: buildApplication(options, context, {
codePlugins: plugins as any,
indexHtmlTransformer: transformIndexHtml(nfOptions),
});
: buildApplication(
instrumentForCoverage
? ({ ...options, instrumentForCoverage } as unknown as typeof options)
: options,
context,
{
codePlugins: plugins as any,
indexHtmlTransformer: transformIndexHtml(nfOptions),
},
);

const rebuildQueue = new RebuildQueue();

Expand Down
2 changes: 2 additions & 0 deletions libs/native-federation/src/builders/build/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export interface NfBuilderSchema extends JsonObject {
baseHref?: string;
outputPath?: string;
ssr: boolean;
instrumentForCoverage?: boolean;
codeCoverageExclude?: string[];
devServer?: boolean;
cacheExternalArtifacts?: boolean;
} // eslint-disable-line
11 changes: 11 additions & 0 deletions libs/native-federation/src/builders/build/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@
"description": "uses federation for ssr in ApplicationBuilder too",
"default": false
},
"instrumentForCoverage": {
"type": "boolean",
"description": "Enables Istanbul instrumentation of the served/built bundles to collect code coverage data for E2E tests (e.g. Cypress). Uses the same instrumentation filter as 'ng test --code-coverage'.",
"default": false
},
"codeCoverageExclude": {
"type": "array",
"items": { "type": "string" },
"description": "Globs (relative to the workspace root) of files to exclude from coverage instrumentation. Only applies when instrumentForCoverage is enabled.",
"default": []
},
"devServer": {
"type": "boolean",
"description": "can be used to disable the dev server when dev=true"
Expand Down
83 changes: 83 additions & 0 deletions libs/native-federation/src/utils/coverage-instrumentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { globSync } from 'node:fs';
import * as path from 'node:path';

import { type BuilderContext } from '@angular-devkit/architect';

// Mirrors @angular/build's src/builders/karma/coverage.ts. Copied rather than
// imported: those helpers aren't re-exported from @angular/build/private and sit
// on an unstable internal path.

export function createInstrumentationFilter(
includedBasePath: string,
excludedPaths: Set<string>,
): (request: string) => boolean {
return (request: string): boolean =>
!excludedPaths.has(request) &&
!/\.(e2e|spec)\.tsx?$|[\\/]node_modules[\\/]|[\\/]\.angular[\\/]/.test(
request,
) &&
request.startsWith(includedBasePath);
}

export function getInstrumentationExcludedPaths(
root: string,
excludedPaths: string[],
): Set<string> {
const excluded = new Set<string>();
for (const excludeGlob of excludedPaths) {
const excludePath =
excludeGlob[0] === '/' ? excludeGlob.slice(1) : excludeGlob;
for (const p of globSync(excludePath, { cwd: root })) {
excluded.add(path.join(root, p));
}
}
return excluded;
}

export async function resolveInstrumentationFilter(
context: BuilderContext,
options: { instrumentForCoverage?: boolean; codeCoverageExclude?: string[] },
): Promise<((request: string) => boolean) | undefined> {
if (!options.instrumentForCoverage) {
return undefined;
}

const workspaceRoot = context.workspaceRoot;

return createInstrumentationFilter(
await getProjectSourceRoot(context),
getInstrumentationExcludedPaths(
workspaceRoot,
options.codeCoverageExclude ?? [],
),
);
}

// Mirrors @angular/build's getProjectSourceRoot: without a target, fall back to
// the workspace root; sourceRoot defaults to <root>/src.
async function getProjectSourceRoot(context: BuilderContext): Promise<string> {
const projectName = context.target?.project;
if (!projectName) {
return context.workspaceRoot;
}

const projectMetadata = await context.getProjectMetadata(projectName);
const projectRoot = path.join(
context.workspaceRoot,
(projectMetadata['root'] as string) ?? '',
);
const rawSourceRoot = projectMetadata['sourceRoot'] as string | undefined;
return normalizeDirectoryPath(
rawSourceRoot === undefined
? path.join(projectRoot, 'src')
: path.join(context.workspaceRoot, rawSourceRoot),
);
}

function normalizeDirectoryPath(directoryPath: string): string {
const last = directoryPath.at(-1);
if (last === '/' || last === '\\') {
return directoryPath.slice(0, -1);
}
return directoryPath;
}
Loading