Skip to content
Open
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
13 changes: 2 additions & 11 deletions packages/angular/build/src/builders/library/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect';
import type { logging } from '@angular-devkit/core';
import fs from 'node:fs/promises';
import {
resetSassWorkerPoolCaches,
shutdownSassWorkerPool,
Expand All @@ -27,8 +26,9 @@ import type { BuildWatcher } from '../../utils/watcher';
import { normalizeLibraryOptions } from './options';
import { updateWatchedEntryPoints } from './pipeline/entry-points';
import type { createComponentStylesheetBundlerForLibrary } from './pipeline/stylesheet-bundler';
import { loadPackageJson } from './pipeline/utils';
import type { Schema as LibraryBuilderOptions } from './schema';
import type { NormalizedLibraryOptions, PackageJsonData, SingleBuildState } from './types';
import type { NormalizedLibraryOptions, SingleBuildState } from './types';

/**
* Executes the library builder to compile, bundle, and package an Angular library into the Angular Package Format (APF).
Expand Down Expand Up @@ -330,15 +330,6 @@ async function* runWatchLoop(
}
}

/**
* Loads and parses a JSON file from disk.
*/
async function loadPackageJson(packageJsonPath: string): Promise<PackageJsonData> {
const content = await fs.readFile(packageJsonPath, 'utf-8');

return JSON.parse(content) as PackageJsonData;
}

/**
* Logs the build completion time and status.
*/
Expand Down
5 changes: 2 additions & 3 deletions packages/angular/build/src/builders/library/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
*/

import type { BuilderContext } from '@angular-devkit/architect';
import fs from 'node:fs/promises';
import path from 'node:path';
import type { StylesheetPluginsass } from '../../tools/esbuild/stylesheets/stylesheet-plugin-factory';
import { normalizeAssetPatterns } from '../../utils';
Expand All @@ -22,6 +21,7 @@ import {
} from '../../utils/postcss-configuration';
import { getProjectRootPaths } from '../../utils/project-metadata';
import { normalizeEntryPoints } from './pipeline/entry-points';
import { loadPackageJson } from './pipeline/utils';
import type { Schema as LibraryBuilderOptions } from './schema';
import type { NormalizedLibraryOptions, PackageJsonData } from './types';

Expand Down Expand Up @@ -69,8 +69,7 @@ export async function normalizeLibraryOptions(

let packageJson: PackageJsonData;
try {
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf8');
packageJson = JSON.parse(packageJsonContent) as PackageJsonData;
packageJson = await loadPackageJson(packageJsonPath);
} catch (error) {
assertIsError(error);
throw new Error(`Failed to read 'package.json' at '${packageJsonPath}': ${error.message}`, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,16 @@ export async function buildAction(actionContext: BuildActionContext): Promise<vo
}
}

if (shouldGenerateManifests) {
filesToEmit.push(...generatePackageManifests(options, isWatchMode));
let hasTslibImport = false;
for (const entryPoint of options.entryPoints.values()) {
if (buildState.previousBundleResults.get(entryPoint.name)?.hasTslibImport) {
hasTslibImport = true;
break;
}
}

if (shouldGenerateManifests || buildState.hasEmittedTslib !== hasTslibImport) {
filesToEmit.push(...(await generatePackageManifests(options, isWatchMode, hasTslibImport)));
}

filesToEmit.push(
Expand Down Expand Up @@ -195,6 +203,7 @@ export async function buildAction(actionContext: BuildActionContext): Promise<vo
});

buildState.hasEmittedManifests = true;
buildState.hasEmittedTslib = hasTslibImport;
buildState.hasEmittedAssets = true;
}

Expand Down
23 changes: 16 additions & 7 deletions packages/angular/build/src/builders/library/pipeline/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ interface MultiBundleOutput {

/** Map of bundle entry names to the set of virtual module IDs included in the bundle. */
moduleIdsByBundle: Map<string, Set<string>>;

/** Set of bundle entry names that import 'tslib'. */
tslibBundles: Set<string>;
}

/**
Expand Down Expand Up @@ -107,15 +110,16 @@ export async function bundleEntryPoints(

for (const { entryPoint, previousBundleResult } of items) {
const { bundleName, name } = entryPoint;
const esmModuleIds = esmOutput.moduleIdsByBundle.get(bundleName);
bundleResults.set(name, {
esmModuleIds:
esmOutput.moduleIdsByBundle.get(bundleName) ??
previousBundleResult?.esmModuleIds ??
new Set(),
esmModuleIds: esmModuleIds ?? previousBundleResult?.esmModuleIds ?? new Set(),
dtsModuleIds:
dtsOutput.moduleIdsByBundle.get(bundleName) ??
previousBundleResult?.dtsModuleIds ??
new Set(),
hasTslibImport: esmModuleIds
? esmOutput.tslibBundles.has(bundleName)
: (previousBundleResult?.hasTslibImport ?? false),
});
}

Expand Down Expand Up @@ -231,6 +235,7 @@ function createMemoryFileLoaderPlugin(
function processRolldownOutput(output: RolldownOutput['output'], dir: string): MultiBundleOutput {
const filesToEmit: MemoryOutputFile[] = [];
const moduleIdsByBundle = new Map<string, Set<string>>();
const tslibBundles = new Set<string>();
const chunksByFileName = new Map<string, OutputChunk>();
const entryChunks: OutputChunk[] = [];

Expand Down Expand Up @@ -276,6 +281,10 @@ function processRolldownOutput(output: RolldownOutput['output'], dir: string): M
}

for (const depFile of [...chunk.imports, ...chunk.dynamicImports]) {
if (depFile === 'tslib') {
tslibBundles.add(entryChunk.name);
}

const depChunk = chunksByFileName.get(depFile);
if (depChunk && !visited.has(depChunk)) {
queue.push(depChunk);
Expand All @@ -284,7 +293,7 @@ function processRolldownOutput(output: RolldownOutput['output'], dir: string): M
}
}

return { filesToEmit, moduleIdsByBundle };
return { filesToEmit, moduleIdsByBundle, tslibBundles };
}

/**
Expand Down Expand Up @@ -350,7 +359,7 @@ async function bundleAllEsm(
options: NormalizedLibraryOptions,
): Promise<MultiBundleOutput> {
if (entryPoints.length === 0) {
return { filesToEmit: [], moduleIdsByBundle: new Map() };
return { filesToEmit: [], moduleIdsByBundle: new Map(), tslibBundles: new Set() };
}

return executeMultiBundle(
Expand All @@ -376,7 +385,7 @@ async function bundleAllDts(
options: NormalizedLibraryOptions,
): Promise<MultiBundleOutput> {
if (entryPoints.length === 0) {
return { filesToEmit: [], moduleIdsByBundle: new Map() };
return { filesToEmit: [], moduleIdsByBundle: new Map(), tslibBundles: new Set() };
}

const dtsSourcemap = options.declarationMap;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,30 @@
* found in the LICENSE file at https://angular.dev/license
*/

import { createRequire } from 'node:module';
import path from 'node:path';
import type { NormalizedLibraryOptions, PackageJsonData } from '../types';
import {
FESM_OUTPUT_DIR,
type MemoryOutputFile,
TYPES_OUTPUT_DIR,
createMemoryOutputFile,
loadPackageJson,
} from './utils';

/**
* Generates the APF package.json and secondary entry point package.json manifests.
*
* @param options The normalized library options.
* @param isWatchMode Whether the builder is running in watch mode.
* @param hasTslibImport Whether any output chunk imports 'tslib'.
* @returns An array of memory output files containing generated package manifests and .npmignore.
*/
export function generatePackageManifests(
export async function generatePackageManifests(
options: NormalizedLibraryOptions,
isWatchMode: boolean,
): MemoryOutputFile[] {
hasTslibImport = false,
): Promise<MemoryOutputFile[]> {
const { packageJson: rawPackageJson, keepLifecycleScripts, compilationMode } = options;

const {
Expand Down Expand Up @@ -73,6 +77,16 @@ export function generatePackageManifests(
version: isWatchMode ? `0.0.0-watch+${Date.now()}` : version,
};

if (hasTslibImport && !rawPackageJson.dependencies?.['tslib']) {
const tslibVersion = await getAngularTslibRange(options.workspaceRoot);
if (tslibVersion) {
distPackageJson.dependencies = {
...rawPackageJson.dependencies,
tslib: tslibVersion,
};
}
}

// Retain scripts if keepLifecycleScripts is set
if (keepLifecycleScripts && scripts) {
distPackageJson.scripts = scripts;
Expand Down Expand Up @@ -166,3 +180,46 @@ function createExportConditions(
default: fesmPath,
};
}

/**
* Cached tslib range used by the Angular compiler.
*/
let cachedTslibRange: string | undefined;

/**
* Get the tslib range used by the Angular compiler.
* @param workspaceRoot path to the workspace root
* @returns tslib range
*/
async function getAngularTslibRange(workspaceRoot: string): Promise<string> {
if (cachedTslibRange) {
return cachedTslibRange;
}

const workspaceRequire = createRequire(path.join(workspaceRoot, 'package.json'));

// Try resolving via @angular/compiler dependencies
try {
const angularCompilerPkg = await loadPackageJson(
workspaceRequire.resolve('@angular/compiler/package.json'),
);
cachedTslibRange = angularCompilerPkg.dependencies?.['tslib'];
} catch {}

// Fallback: Try resolving tslib directly
if (!cachedTslibRange) {
try {
const tslibPkg = await loadPackageJson(workspaceRequire.resolve('tslib/package.json'));
if (tslibPkg.version) {
cachedTslibRange = `^${tslibPkg.version}`;
}
} catch {}
}

// Fail fast if still unresolved
if (!cachedTslibRange) {
throw new Error('Unable to resolve tslib range.');
}

return cachedTslibRange;
}
Loading
Loading