@angular/build
Version:
Official build system for Angular
324 lines • 15.8 kB
JavaScript
;
/**
* @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
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.execute = execute;
const node_crypto_1 = require("node:crypto");
const node_fs_1 = require("node:fs");
const fs = __importStar(require("node:fs/promises"));
const node_path_1 = __importDefault(require("node:path"));
const web_1 = require("node:stream/web");
const virtual_module_plugin_1 = require("../../tools/esbuild/virtual-module-plugin");
const test_files_1 = require("../../utils/test-files");
const index_1 = require("../application/index");
const results_1 = require("../application/results");
const schema_1 = require("../application/schema");
const assets_middleware_1 = require("./assets-middleware");
const coverage_1 = require("./coverage");
const karma_config_1 = require("./karma-config");
const options_1 = require("./options");
const polyfills_plugin_1 = require("./polyfills-plugin");
const progress_reporter_1 = require("./progress-reporter");
const utils_1 = require("./utils");
class ApplicationBuildError extends Error {
constructor(message) {
super(message);
this.name = 'ApplicationBuildError';
}
}
function execute(options, context, transforms) {
const normalizedOptions = (0, options_1.normalizeOptions)(context, options);
const karmaOptions = (0, karma_config_1.getBaseKarmaOptions)(normalizedOptions, context);
let karmaServer;
return new web_1.ReadableStream({
async start(controller) {
let init;
try {
init = await initializeApplication(normalizedOptions, context, karmaOptions, transforms);
}
catch (err) {
if (err instanceof ApplicationBuildError) {
controller.enqueue({ success: false, message: err.message });
controller.close();
return;
}
throw err;
}
const [karma, karmaConfig, buildOptions, buildIterator] = init;
// If `--watch` is explicitly enabled or if we are keeping the Karma
// process running, we should hook Karma into the build.
if (buildIterator) {
(0, progress_reporter_1.injectKarmaReporter)(buildOptions, buildIterator, karmaConfig, controller);
}
// Close the stream once the Karma server returns.
karmaServer = new karma.Server(karmaConfig, (exitCode) => {
controller.enqueue({ success: exitCode === 0 });
controller.close();
});
await karmaServer.start();
},
async cancel() {
await karmaServer?.stop();
},
});
}
async function initializeApplication(options, context, karmaOptions, transforms) {
const karma = await Promise.resolve().then(() => __importStar(require('karma')));
const projectSourceRoot = await (0, utils_1.getProjectSourceRoot)(context);
// Setup temporary output path and ensure it is empty
const outputPath = node_path_1.default.join(context.workspaceRoot, 'dist/test-out', (0, node_crypto_1.randomUUID)());
await fs.rm(outputPath, { recursive: true, force: true });
// Setup exit cleanup for temporary directory
const handleProcessExit = () => (0, node_fs_1.rmSync)(outputPath, { recursive: true, force: true });
process.once('exit', handleProcessExit);
process.once('SIGINT', handleProcessExit);
process.once('uncaughtException', handleProcessExit);
const { buildOptions, mainName } = await setupBuildOptions(options, context, projectSourceRoot, outputPath);
const [buildOutput, buildIterator] = await runEsbuild(buildOptions, context, projectSourceRoot);
const karmaConfig = await configureKarma(karma, context, karmaOptions, options, buildOptions, buildOutput, mainName, transforms);
return [karma, karmaConfig, buildOptions, buildIterator];
}
async function setupBuildOptions(options, context, projectSourceRoot, outputPath) {
const entryPoints = await (0, utils_1.collectEntrypoints)(options, context, projectSourceRoot);
const mainName = 'test_main';
if (options.main) {
entryPoints.set(mainName, options.main);
}
else {
entryPoints.set(mainName, 'angular:test-bed-init');
}
const instrumentForCoverage = options.codeCoverage
? (0, coverage_1.createInstrumentationFilter)(projectSourceRoot, (0, coverage_1.getInstrumentationExcludedPaths)(context.workspaceRoot, options.codeCoverageExclude ?? []))
: undefined;
const [polyfills, jasmineCleanup] = (0, utils_1.normalizePolyfills)(options.polyfills);
for (let idx = 0; idx < jasmineCleanup.length; ++idx) {
entryPoints.set(`jasmine-cleanup-${idx}`, jasmineCleanup[idx]);
}
const buildOptions = {
assets: options.assets,
entryPoints,
tsConfig: options.tsConfig,
outputPath,
preserveSymlinks: options.preserveSymlinks,
aot: options.aot,
index: false,
outputHashing: schema_1.OutputHashing.None,
optimization: false,
sourceMap: options.sourceMap,
instrumentForCoverage,
styles: options.styles,
scripts: options.scripts,
polyfills,
webWorkerTsConfig: options.webWorkerTsConfig,
watch: options.watch,
stylePreprocessorOptions: options.stylePreprocessorOptions,
inlineStyleLanguage: options.inlineStyleLanguage,
fileReplacements: options.fileReplacements,
define: options.define,
loader: options.loader,
externalDependencies: options.externalDependencies,
};
return { buildOptions, mainName };
}
async function runEsbuild(buildOptions, context, projectSourceRoot) {
const usesZoneJS = buildOptions.polyfills?.includes('zone.js');
const virtualTestBedInit = (0, virtual_module_plugin_1.createVirtualModulePlugin)({
namespace: 'angular:test-bed-init',
loadContent: async () => {
const contents = [
// Initialize the Angular testing environment
`import { NgModule${usesZoneJS ? ', provideZoneChangeDetection' : ''} } from '@angular/core';`,
`import { getTestBed } from '@angular/core/testing';`,
`import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';`,
`@NgModule({ providers: [${usesZoneJS ? 'provideZoneChangeDetection(), ' : ''}], })`,
`export class TestModule {}`,
`getTestBed().initTestEnvironment([BrowserTestingModule, TestModule], platformBrowserTesting(), {`,
` errorOnUnknownElements: true,`,
` errorOnUnknownProperties: true,`,
`});`,
];
return {
contents: contents.join('\n'),
loader: 'js',
resolveDir: projectSourceRoot,
};
},
});
// Build tests with `application` builder, using test files as entry points.
const [buildOutput, buildIterator] = await (0, utils_1.first)((0, index_1.buildApplicationInternal)(buildOptions, context, { codePlugins: [virtualTestBedInit] }), { cancel: !buildOptions.watch });
if (buildOutput.kind === results_1.ResultKind.Failure) {
throw new ApplicationBuildError('Build failed');
}
else if (buildOutput.kind !== results_1.ResultKind.Full) {
throw new ApplicationBuildError('A full build result is required from the application builder.');
}
// Write test files
await (0, test_files_1.writeTestFiles)(buildOutput.files, buildOptions.outputPath);
return [buildOutput, buildIterator];
}
async function configureKarma(karma, context, karmaOptions, options, buildOptions, buildOutput, mainName, transforms) {
const outputPath = buildOptions.outputPath;
// We need to add this to the beginning *after* the testing framework has
// prepended its files. The output path is required for each since they are
// added later in the test process via a plugin.
const polyfillsFile = {
pattern: `${outputPath}/polyfills.js`,
included: true,
served: true,
type: 'module',
watched: false,
};
const jasmineCleanupFiles = {
pattern: `${outputPath}/jasmine-cleanup-*.js`,
included: true,
served: true,
type: 'module',
watched: false,
};
karmaOptions.basePath = outputPath;
const scriptsFiles = [];
if (options.scripts?.length) {
const outputScripts = new Set();
for (const scriptEntry of options.scripts) {
const outputName = typeof scriptEntry === 'string'
? 'scripts.js'
: `${scriptEntry.bundleName ?? 'scripts'}.js`;
if (outputScripts.has(outputName)) {
continue;
}
outputScripts.add(outputName);
scriptsFiles.push({
pattern: `${outputPath}/${outputName}`,
watched: false,
included: typeof scriptEntry === 'string' ? true : scriptEntry.inject !== false,
type: 'js',
});
}
}
karmaOptions.files ??= [];
karmaOptions.files.push(
// Serve global setup script.
{ pattern: `${mainName}.js`, type: 'module', watched: false },
// Serve all source maps.
{ pattern: `*.map`, included: false, watched: false },
// These are the test entrypoints.
{ pattern: `spec-*.js`, type: 'module', watched: false });
if ((0, utils_1.hasChunkOrWorkerFiles)(buildOutput.files)) {
karmaOptions.files.push(
// Allow loading of chunk-* files but don't include them all on load.
{
pattern: `{chunk,worker}-*.js`,
type: 'module',
included: false,
watched: false,
});
}
if (options.styles?.length) {
// Serve CSS outputs on page load, these are the global styles.
karmaOptions.files.push({ pattern: `*.css`, type: 'css', watched: false });
}
const parsedKarmaConfig = await karma.config.parseConfig(options.karmaConfig, transforms?.karmaOptions ? await transforms.karmaOptions(karmaOptions) : karmaOptions, { promiseConfig: true, throwErrors: true });
// Check for jsdom which does not support executing ESM scripts.
// If present, remove jsdom and issue a warning.
const updatedBrowsers = parsedKarmaConfig.browsers?.filter((browser) => browser !== 'jsdom');
if (parsedKarmaConfig.browsers?.length !== updatedBrowsers?.length) {
parsedKarmaConfig.browsers = updatedBrowsers;
context.logger.warn(`'jsdom' does not support ESM code execution and cannot be used for karma testing.` +
` The 'jsdom' entry has been removed from the 'browsers' option.`);
}
// Remove the webpack plugin/framework:
// Alternative would be to make the Karma plugin "smart" but that's a tall order
// with managing unneeded imports etc..
parsedKarmaConfig.plugins ??= [];
const pluginLengthBefore = parsedKarmaConfig.plugins.length;
parsedKarmaConfig.plugins = parsedKarmaConfig.plugins.filter((plugin) => {
if (typeof plugin === 'string') {
return plugin !== 'framework:@angular-devkit/build-angular';
}
return !plugin['framework:@angular-devkit/build-angular'];
});
parsedKarmaConfig.frameworks ??= [];
parsedKarmaConfig.frameworks = parsedKarmaConfig.frameworks.filter((framework) => framework !== '@angular-devkit/build-angular');
const pluginLengthAfter = parsedKarmaConfig.plugins.length;
if (pluginLengthBefore !== pluginLengthAfter) {
context.logger.warn(`Ignoring framework "@angular-devkit/build-angular" from karma config file because it's not compatible with the application builder.`);
}
parsedKarmaConfig.plugins.push(assets_middleware_1.AngularAssetsMiddleware.createPlugin(buildOutput));
parsedKarmaConfig.middleware ??= [];
parsedKarmaConfig.middleware.push(assets_middleware_1.AngularAssetsMiddleware.NAME);
parsedKarmaConfig.plugins.push(polyfills_plugin_1.AngularPolyfillsPlugin.createPlugin(polyfillsFile, jasmineCleanupFiles, scriptsFiles));
parsedKarmaConfig.reporters ??= [];
parsedKarmaConfig.reporters.push(polyfills_plugin_1.AngularPolyfillsPlugin.NAME);
// Adjust karma junit reporter outDir location to maintain previous (devkit) behavior
// The base path for the reporter was previously the workspace root.
// To keep the files in the same location, the reporter's output directory is adjusted
// to be relative to the workspace root when using junit.
if (parsedKarmaConfig.reporters?.some((reporter) => reporter === 'junit')) {
if ('junitReporter' in parsedKarmaConfig) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const junitReporterOptions = parsedKarmaConfig['junitReporter'];
if (junitReporterOptions.outputDir == undefined) {
junitReporterOptions.outputDir = context.workspaceRoot;
}
else if (typeof junitReporterOptions.outputDir === 'string' &&
!node_path_1.default.isAbsolute(junitReporterOptions.outputDir)) {
junitReporterOptions.outputDir = node_path_1.default.join(context.workspaceRoot, junitReporterOptions.outputDir);
}
}
else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
parsedKarmaConfig['junitReporter'] = {
outputDir: context.workspaceRoot,
};
}
}
// When using code-coverage, auto-add karma-coverage.
// This was done as part of the karma plugin for webpack.
if (options.codeCoverage &&
!parsedKarmaConfig.reporters?.some((r) => r === 'coverage' || r === 'coverage-istanbul')) {
parsedKarmaConfig.reporters = (parsedKarmaConfig.reporters ?? []).concat(['coverage']);
}
return parsedKarmaConfig;
}
//# sourceMappingURL=application_builder.js.map