ng-packagr
Version:
Compile and package Angular libraries in Angular Package Format (APF)
315 lines • 13.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BundlerContext = exports.BuildOutputFileType = void 0;
const esbuild_1 = require("esbuild");
const node_path_1 = require("node:path");
const path_1 = require("../utils/path");
const load_result_cache_1 = require("./load-result-cache");
var BuildOutputFileType;
(function (BuildOutputFileType) {
BuildOutputFileType[BuildOutputFileType["Browser"] = 0] = "Browser";
BuildOutputFileType[BuildOutputFileType["Media"] = 1] = "Media";
BuildOutputFileType[BuildOutputFileType["ServerApplication"] = 2] = "ServerApplication";
BuildOutputFileType[BuildOutputFileType["ServerRoot"] = 3] = "ServerRoot";
BuildOutputFileType[BuildOutputFileType["Root"] = 4] = "Root";
})(BuildOutputFileType || (exports.BuildOutputFileType = BuildOutputFileType = {}));
/**
* Determines if an unknown value is an esbuild BuildFailure error object thrown by esbuild.
* @param value A potential esbuild BuildFailure error object.
* @returns `true` if the object is determined to be a BuildFailure object; otherwise, `false`.
*/
function isEsBuildFailure(value) {
return !!value && typeof value === 'object' && 'errors' in value && 'warnings' in value;
}
class BundlerContext {
workspaceRoot;
incremental;
useContext;
#esbuildContext;
#esbuildOptions;
#esbuildResult;
#activeBundlePromise;
#disposed = false;
#optionsFactory;
#shouldCacheResult;
#loadCache;
watchFiles = new Set();
constructor(workspaceRoot, incremental, options, useContext = incremental, initialFilter, sharedLoadCache) {
this.workspaceRoot = workspaceRoot;
this.incremental = incremental;
this.useContext = useContext;
if (initialFilter && typeof initialFilter !== 'function') {
this.#loadCache = initialFilter;
}
else {
this.#loadCache = sharedLoadCache;
}
// To cache the results an option factory is needed to capture the full set of dependencies
this.#shouldCacheResult = incremental && typeof options === 'function';
this.#optionsFactory = (...args) => {
const baseOptions = typeof options === 'function' ? options(...args) : options;
return {
...baseOptions,
metafile: true,
write: false,
};
};
}
/**
* Executes the esbuild build function and normalizes the build result in the event of a
* build failure that results in no output being generated.
* All builds use the `write` option with a value of `false` to allow for the output files
* build result array to be populated.
*
* @returns If output files are generated, the full esbuild BuildResult; if not, the
* warnings and errors for the attempted build.
*/
async bundle(force = false) {
// Return existing result if present
if (this.#esbuildResult) {
return this.#esbuildResult;
}
if (!force && this.#activeBundlePromise !== undefined) {
return this.#activeBundlePromise;
}
const bundlePromise = this.#performBundle().finally(() => {
if (this.#activeBundlePromise === bundlePromise) {
this.#activeBundlePromise = undefined;
}
});
this.#activeBundlePromise = bundlePromise;
const result = await bundlePromise;
if (this.#shouldCacheResult) {
this.#esbuildResult = result;
}
return result;
}
async #performBundle() {
// Create esbuild options if not present
if (this.#esbuildOptions === undefined) {
if (this.incremental && !this.#loadCache) {
this.#loadCache = new load_result_cache_1.MemoryLoadResultCache();
}
this.#esbuildOptions = this.#optionsFactory(this.#loadCache);
}
if (this.incremental) {
this.watchFiles.clear();
}
let result;
try {
if (this.#esbuildContext) {
// Rebuild using the existing incremental build context
result = await this.#esbuildContext.rebuild();
}
else if (this.useContext) {
// Create an incremental build context and perform the first build.
// Context creation does not perform a build.
const esbuildContext = await (0, esbuild_1.context)(this.#esbuildOptions);
if (this.#disposed) {
await esbuildContext.dispose();
throw new Error('BundlerContext was disposed during build.');
}
this.#esbuildContext = esbuildContext;
result = await this.#esbuildContext.rebuild();
}
else {
// For non-incremental builds, perform a single build
if (this.#disposed) {
throw new Error('BundlerContext was disposed during build.');
}
result = await (0, esbuild_1.build)(this.#esbuildOptions);
if (this.#disposed) {
throw new Error('BundlerContext was disposed during build.');
}
}
}
catch (failure) {
// Build failures will throw an exception which contains errors/warnings
if (isEsBuildFailure(failure)) {
this.#addErrorsToWatch(failure);
this.#addLoadCacheFilesToWatch();
return failure;
}
else {
throw failure;
}
}
// Update files that should be watched.
// While this should technically not be linked to incremental mode, incremental is only
// currently enabled with watch mode where watch files are needed.
if (this.incremental) {
// Add input files except virtual angular files which do not exist on disk
for (const input of Object.keys(result.metafile.inputs)) {
const isInternal = isInternalAngularFile(input) || isInternalBundlerFile(input);
// Input file paths are always relative to the workspace root unless already absolute
const normalizedAbsoluteInput = (0, node_path_1.isAbsolute)(input)
? (0, path_1.ensureUnixPath)(input)
: (0, path_1.ensureUnixPath)((0, node_path_1.join)(this.workspaceRoot, input));
if (!isInternal) {
this.watchFiles.add(normalizedAbsoluteInput);
}
if (this.#loadCache) {
const cachedLoad = await (this.#loadCache.get(input) ??
this.#loadCache.get(input.replace(';', ':')) ??
this.#loadCache.get('file:' + normalizedAbsoluteInput));
if (cachedLoad?.watchFiles) {
for (const file of cachedLoad.watchFiles) {
if (!isInternalAngularFile(file)) {
this.watchFiles.add((0, node_path_1.isAbsolute)(file) ? (0, path_1.ensureUnixPath)(file) : (0, path_1.ensureUnixPath)((0, node_path_1.join)(this.workspaceRoot, file)));
}
}
}
}
}
}
// Return if the build encountered any errors
if (result.errors.length) {
this.#addErrorsToWatch(result);
this.#addLoadCacheFilesToWatch();
return {
errors: result.errors,
warnings: result.warnings,
};
}
// Return the successful build results
return {
errors: undefined,
warnings: result.warnings,
metafile: result.metafile,
outputFiles: result.outputFiles,
};
}
#addErrorsToWatch(result) {
for (const error of result.errors) {
const file = error.location?.file;
if (file && !isInternalAngularFile(file)) {
this.watchFiles.add((0, node_path_1.isAbsolute)(file) ? (0, path_1.ensureUnixPath)(file) : (0, path_1.ensureUnixPath)((0, node_path_1.join)(this.workspaceRoot, file)));
}
for (const note of error.notes ?? []) {
const noteFile = note.location?.file;
if (noteFile && !isInternalAngularFile(noteFile)) {
this.watchFiles.add((0, node_path_1.isAbsolute)(noteFile) ? (0, path_1.ensureUnixPath)(noteFile) : (0, path_1.ensureUnixPath)((0, node_path_1.join)(this.workspaceRoot, noteFile)));
}
}
}
}
#addLoadCacheFilesToWatch() {
if (this.incremental && this.#loadCache) {
for (const file of this.#loadCache.watchFiles) {
if (!isInternalAngularFile(file)) {
this.watchFiles.add((0, node_path_1.isAbsolute)(file) ? (0, path_1.ensureUnixPath)(file) : (0, path_1.ensureUnixPath)((0, node_path_1.join)(this.workspaceRoot, file)));
}
}
}
}
/**
* Invalidate a stored bundler result based on the previous watch files
* and a list of changed files.
* The context must be created with incremental mode enabled for results
* to be stored.
* @returns True, if the result was invalidated; False, otherwise.
*/
invalidate(files) {
if (!this.incremental) {
return false;
}
let candidateFiles;
if (files instanceof Set) {
let isCandidateReady = true;
for (const file of files) {
if (file !== (0, path_1.ensureUnixPath)(file) ||
(!(0, node_path_1.isAbsolute)(file) && !files.has((0, path_1.ensureUnixPath)((0, node_path_1.join)(this.workspaceRoot, file))))) {
isCandidateReady = false;
break;
}
}
if (isCandidateReady) {
candidateFiles = files;
}
else {
const normalizedFiles = new Set();
for (const file of files) {
const normalized = (0, path_1.ensureUnixPath)(file);
normalizedFiles.add(normalized);
if (!(0, node_path_1.isAbsolute)(normalized)) {
normalizedFiles.add((0, path_1.ensureUnixPath)((0, node_path_1.join)(this.workspaceRoot, normalized)));
}
}
candidateFiles = normalizedFiles;
}
}
else {
const normalizedFiles = new Set();
for (const file of files) {
const normalized = (0, path_1.ensureUnixPath)(file);
normalizedFiles.add(normalized);
if (!(0, node_path_1.isAbsolute)(normalized)) {
normalizedFiles.add((0, path_1.ensureUnixPath)((0, node_path_1.join)(this.workspaceRoot, normalized)));
}
}
candidateFiles = normalizedFiles;
}
let invalid = false;
for (const file of candidateFiles) {
if (this.#loadCache?.invalidate(file)) {
invalid = true;
}
}
if (!invalid) {
if (this.watchFiles.size < candidateFiles.size) {
for (const file of this.watchFiles) {
if (candidateFiles.has(file)) {
invalid = true;
break;
}
}
}
else {
for (const file of candidateFiles) {
if (this.watchFiles.has(file)) {
invalid = true;
break;
}
}
}
}
if (invalid) {
this.#esbuildResult = undefined;
}
return invalid;
}
/**
* Disposes incremental build resources present in the context.
*
* @returns A promise that resolves when disposal is complete.
*/
async dispose() {
this.#disposed = true;
try {
this.#esbuildOptions = undefined;
this.#esbuildResult = undefined;
this.#activeBundlePromise = undefined;
this.#loadCache = undefined;
await this.#esbuildContext?.dispose();
}
finally {
this.#esbuildContext = undefined;
}
}
}
exports.BundlerContext = BundlerContext;
function isInternalAngularFile(file) {
return file.startsWith('angular:');
}
function isInternalBundlerFile(file) {
// Bundler virtual files such as "<define:???>" or "<runtime>"
if (file[0] === '<' && file.at(-1) === '>') {
return true;
}
// Any (disabled): path is a virtual esbuild entry that doesn't exist on disk
if (file.includes('(disabled):')) {
return true;
}
return false;
}
//# sourceMappingURL=bundler-context.js.map