@angular/build
Version:
Official build system for Angular
382 lines • 19.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.serveWithVite = serveWithVite;
const node_assert_1 = __importDefault(require("node:assert"));
const node_path_1 = require("node:path");
const plugins_1 = require("../../../tools/vite/plugins");
const utils_1 = require("../../../tools/vite/utils");
const utils_2 = require("../../../utils");
const environment_options_1 = require("../../../utils/environment-options");
const results_1 = require("../../application/results");
const schema_1 = require("../../application/schema");
const internal_1 = require("../internal");
const hmr_1 = require("./hmr");
const server_1 = require("./server");
const utils_3 = require("./utils");
/**
* Build options that are also present on the dev server but are only passed
* to the build.
*/
const CONVENIENCE_BUILD_OPTIONS = ['watch', 'poll', 'verbose', 'define'];
// eslint-disable-next-line max-lines-per-function
async function* serveWithVite(serverOptions, builderName, builderAction, context, transformers, extensions) {
// Get the browser configuration from the target name.
const rawBrowserOptions = await context.getTargetOptions(serverOptions.buildTarget);
// Deploy url is not used in the dev-server.
delete rawBrowserOptions.deployUrl;
// Copy convenience options to build
for (const optionName of CONVENIENCE_BUILD_OPTIONS) {
const optionValue = serverOptions[optionName];
if (optionValue !== undefined) {
if (optionName === 'define' && rawBrowserOptions[optionName]) {
// Define has merging behavior within the application
for (const [key, value] of Object.entries(optionValue)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
rawBrowserOptions[optionName][key] = value;
}
}
else {
rawBrowserOptions[optionName] = optionValue;
}
}
}
// TODO: Adjust architect to not force a JsonObject derived return type
const browserOptions = (await context.validateOptions(rawBrowserOptions, builderName));
if (browserOptions.prerender || (browserOptions.outputMode && browserOptions.server)) {
// Disable prerendering if enabled and force SSR.
// This is so instead of prerendering all the routes for every change, the page is "prerendered" when it is requested.
browserOptions.prerender = undefined;
browserOptions.ssr ||= true;
}
// Disable auto CSP.
browserOptions.security = {
autoCsp: false,
};
// Disable JSON build stats.
// These are not accessible with the dev server and can cause HMR fallbacks.
if (browserOptions.statsJson === true) {
context.logger.warn('Build JSON statistics output (`statsJson` option) has been disabled.' +
' The development server does not support this option.');
}
browserOptions.statsJson = false;
// Set all packages as external to support Vite's prebundle caching
browserOptions.externalPackages = serverOptions.prebundle;
// Disable generating a full manifest with routes.
// This is done during runtime when using the dev-server.
browserOptions.partialSSRBuild = true;
// The development server currently only supports a single locale when localizing.
// This matches the behavior of the Webpack-based development server but could be expanded in the future.
if (browserOptions.localize === true ||
(Array.isArray(browserOptions.localize) && browserOptions.localize.length > 1)) {
context.logger.warn('Localization (`localize` option) has been disabled. The development server only supports localizing a single locale per build.');
browserOptions.localize = false;
}
else if (browserOptions.localize) {
// When localization is enabled with a single locale, force a flat path to maintain behavior with the existing Webpack-based dev server.
browserOptions.forceI18nFlatOutput = true;
}
const { vendor: thirdPartySourcemaps, scripts: scriptsSourcemaps } = (0, utils_2.normalizeSourceMaps)(browserOptions.sourceMap ?? false);
if (scriptsSourcemaps && browserOptions.server) {
// https://nodejs.org/api/process.html#processsetsourcemapsenabledval
process.setSourceMapsEnabled(true);
}
if (serverOptions.hmr &&
(browserOptions.outputHashing === schema_1.OutputHashing.All ||
browserOptions.outputHashing === schema_1.OutputHashing.Bundles)) {
serverOptions.hmr = false;
context.logger.warn(`Hot Module Replacement (HMR) is disabled because the 'outputHashing' option is set to '${browserOptions.outputHashing}'. ` +
'HMR is incompatible with this setting.');
}
const componentsHmrCanBeUsed = browserOptions.aot && serverOptions.liveReload && serverOptions.hmr;
// Enable to support link-based component style hot reloading (`NG_HMR_CSTYLES=1` can be used to enable)
browserOptions.externalRuntimeStyles = componentsHmrCanBeUsed && environment_options_1.useComponentStyleHmr;
// Enable to support component template hot replacement (`NG_HMR_TEMPLATE=0` can be used to disable selectively)
// This will also replace file-based/inline styles as code if external runtime styles are not enabled.
browserOptions.templateUpdates = componentsHmrCanBeUsed && environment_options_1.useComponentTemplateHmr;
browserOptions.incrementalResults = true;
// Setup the prebundling transformer that will be shared across Vite prebundling requests
const prebundleTransformer = new internal_1.JavaScriptTransformer(
// Always enable JIT linking to support applications built with and without AOT.
// In a development environment the additional scope information does not
// have a negative effect unlike production where final output size is relevant.
{ sourcemap: true, jit: true, thirdPartySourcemaps }, 1);
// The index HTML path will be updated from the build results if provided by the builder
let htmlIndexPath = 'index.html';
const { createServer, normalizePath } = await Promise.resolve().then(() => __importStar(require('vite')));
let server;
let serverUrl;
let hadError = false;
const generatedFiles = new Map();
const assetFiles = new Map();
const externalMetadata = {
implicitBrowser: [],
implicitServer: [],
explicitBrowser: [],
explicitServer: [],
};
const componentStyles = new Map();
const templateUpdates = new Map();
// Add cleanup logic via a builder teardown.
let deferred;
context.addTeardown(async () => {
await server?.close();
await prebundleTransformer.close();
deferred?.();
});
// TODO: Switch this to an architect schedule call when infrastructure settings are supported
for await (const result of builderAction(browserOptions, context, extensions?.buildPlugins)) {
if (result.kind === results_1.ResultKind.Failure) {
if (result.errors.length && server) {
hadError = true;
server.ws.send({
type: 'error',
err: {
message: result.errors[0].text,
stack: '',
loc: result.errors[0].location ?? undefined,
},
});
}
yield { baseUrl: '', success: false };
continue;
}
// Clear existing error overlay on successful result
if (hadError && server) {
hadError = false;
// Send an empty update to clear the error overlay
server.ws.send({
'type': 'update',
updates: [],
});
}
let needClientUpdate = true;
switch (result.kind) {
case results_1.ResultKind.Full:
if (result.detail?.['htmlIndexPath']) {
htmlIndexPath = result.detail['htmlIndexPath'];
}
if (serverOptions.servePath === undefined && result.detail?.['htmlBaseHref']) {
const baseHref = result.detail['htmlBaseHref'];
// Remove trailing slash
serverOptions.servePath =
baseHref !== './' && baseHref.at(-1) === '/' ? baseHref.slice(0, -1) : baseHref;
}
assetFiles.clear();
componentStyles.clear();
generatedFiles.clear();
for (const [outputPath, file] of Object.entries(result.files)) {
(0, utils_3.updateResultRecord)(outputPath, file, normalizePath, htmlIndexPath, generatedFiles, assetFiles, componentStyles,
// The initial build will not yet have a server setup
!server);
}
// Clear stale template updates on code rebuilds
templateUpdates.clear();
break;
case results_1.ResultKind.Incremental:
(0, node_assert_1.default)(server, 'Builder must provide an initial full build before incremental results.');
// Background updates should only update server files/options
needClientUpdate = !result.background;
for (const removed of result.removed) {
const filePath = '/' + normalizePath(removed.path);
generatedFiles.delete(filePath);
assetFiles.delete(filePath);
}
for (const modified of result.modified) {
(0, utils_3.updateResultRecord)(modified, result.files[modified], normalizePath, htmlIndexPath, generatedFiles, assetFiles, componentStyles);
}
for (const added of result.added) {
(0, utils_3.updateResultRecord)(added, result.files[added], normalizePath, htmlIndexPath, generatedFiles, assetFiles, componentStyles);
}
break;
case results_1.ResultKind.ComponentUpdate:
(0, node_assert_1.default)(serverOptions.hmr, 'Component updates are only supported with HMR enabled.');
(0, node_assert_1.default)(server, 'Builder must provide an initial full build before component update results.');
for (const componentUpdate of result.updates) {
if (componentUpdate.type === 'template') {
templateUpdates.set(componentUpdate.id, componentUpdate.content);
server.ws.send('angular:component-update', {
id: componentUpdate.id,
timestamp: Date.now(),
});
}
}
context.logger.info('Component update sent to client(s).');
continue;
default:
context.logger.warn(`Unknown result kind [${result.kind}] provided by build.`);
continue;
}
// To avoid disconnecting the array objects from the option, these arrays need to be mutated instead of replaced.
(0, utils_1.updateExternalMetadata)(result, externalMetadata, browserOptions.externalDependencies);
if (server) {
// Update fs allow list to include any new assets from the build option.
server.config.server.fs.allow = [
...new Set([
...server.config.server.fs.allow,
...[...assetFiles.values()].map(({ source }) => source),
]),
];
const updatedFiles = await (0, hmr_1.invalidateUpdatedFiles)(normalizePath, generatedFiles, assetFiles, server);
if (needClientUpdate) {
(0, hmr_1.handleUpdate)(server, serverOptions, context.logger, componentStyles, updatedFiles);
}
}
else {
const projectName = context.target?.project;
if (!projectName) {
throw new Error('The builder requires a target.');
}
context.logger.info('NOTE: Raw file sizes do not reflect development server per-request transformations.');
if (browserOptions.ssr && serverOptions.inspect) {
const { host, port } = serverOptions.inspect;
const { default: inspector } = await Promise.resolve().then(() => __importStar(require('node:inspector')));
inspector.open(port, host, true);
context.addTeardown(() => inspector.close());
}
const { root = '' } = await context.getProjectMetadata(projectName);
const projectRoot = (0, node_path_1.join)(context.workspaceRoot, root);
const browsers = (0, internal_1.getSupportedBrowsers)(projectRoot, context.logger);
const target = (0, internal_1.transformSupportedBrowsersToTargets)(browsers);
// Needed for browser-esbuild as polyfills can be a string.
const polyfills = Array.isArray((browserOptions.polyfills ??= []))
? browserOptions.polyfills
: [browserOptions.polyfills];
let ssrMode = plugins_1.ServerSsrMode.NoSsr;
if (browserOptions.outputMode &&
typeof browserOptions.ssr === 'object' &&
browserOptions.ssr.entry) {
ssrMode = plugins_1.ServerSsrMode.ExternalSsrMiddleware;
}
else if (browserOptions.ssr) {
ssrMode = plugins_1.ServerSsrMode.InternalSsrMiddleware;
}
if (browserOptions.progress !== false && ssrMode !== plugins_1.ServerSsrMode.NoSsr) {
// This is a workaround for https://github.com/angular/angular-cli/issues/28336, which is caused by the interaction between `zone.js` and `listr2`.
process.once('SIGINT', () => {
process.kill(process.pid);
});
}
// Setup server and start listening
const serverConfiguration = await (0, server_1.setupServer)(serverOptions, generatedFiles, assetFiles, browserOptions.preserveSymlinks, externalMetadata, ssrMode, prebundleTransformer, target, (0, internal_1.isZonelessApp)(polyfills), componentStyles, templateUpdates, browserOptions.loader, {
...browserOptions.define,
'ngJitMode': browserOptions.aot ? 'false' : 'true',
'ngHmrMode': browserOptions.templateUpdates ? 'true' : 'false',
}, extensions?.middleware, transformers?.indexHtml, thirdPartySourcemaps);
server = await createServer(serverConfiguration);
await server.listen();
// Setup builder context logging for browser clients
server.hot.on('angular:log', (data) => {
if (typeof data?.text !== 'string') {
context.logger.warn('Development server client sent invalid internal log event.');
}
switch (data.kind) {
case 'error':
context.logger.error(`[CLIENT ERROR]: ${data.text}`);
break;
case 'warning':
context.logger.warn(`[CLIENT WARNING]: ${data.text}`);
break;
default:
context.logger.info(`[CLIENT INFO]: ${data.text}`);
break;
}
});
// Setup component HMR invalidation
// Invalidation occurs when the runtime cannot update a component
server.hot.on('angular:invalidate', (data) => {
if (typeof data?.id !== 'string') {
context.logger.warn('Development server client sent invalid internal invalidate event.');
}
// Clear invalid template update
templateUpdates.delete(data.id);
// Some cases are expected unsupported update scenarios but some may be errors.
// If an error occurred, log the error in addition to the invalidation.
if (data.error) {
context.logger.error(`Component update failed${data.message ? `: ${data.message}` : '.'}` +
'\nPlease consider reporting the error at https://github.com/angular/angular-cli/issues');
}
else {
context.logger.warn(`Component update unsupported${data.message ? `: ${data.message}` : '.'}`);
}
server?.ws.send({
type: 'full-reload',
path: '*',
});
context.logger.info('Page reload sent to client(s).');
});
const urls = server.resolvedUrls;
if (urls && (urls.local.length || urls.network.length)) {
serverUrl = new URL(urls.local[0] ?? urls.network[0]);
}
// log connection information
server.printUrls();
server.bindCLIShortcuts({
print: true,
customShortcuts: [
{
key: 'r',
description: 'force reload browser',
action(server) {
componentStyles.forEach((record) => record.used?.clear());
server.ws.send({
type: 'full-reload',
path: '*',
});
},
},
],
});
}
// TODO: adjust output typings to reflect both development servers
yield {
success: true,
port: serverUrl?.port,
baseUrl: serverUrl?.href,
};
}
await new Promise((resolve) => (deferred = resolve));
}
//# sourceMappingURL=index.js.map