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
25 changes: 18 additions & 7 deletions packages/angular/build/src/builders/library/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,11 @@ import { purgeStaleBuildCache } from '../../utils/purge-cache';
import { getSupportedBrowsers } from '../../utils/supported-browsers';
import { assertCompatibleAngularVersion } from '../../utils/version';
import type { BuildWatcher } from '../../utils/watcher';
import {
type NormalizedLibraryOptions,
type PackageJsonData,
normalizeLibraryOptions,
} from './options';
import type { SingleBuildState } from './pipeline/build-action';
import { normalizeLibraryOptions } from './options';
import { updateWatchedEntryPoints } from './pipeline/entry-points';
import type { createComponentStylesheetBundlerForLibrary } from './pipeline/stylesheet-bundler';
import type { Schema as LibraryBuilderOptions } from './schema';
import type { NormalizedLibraryOptions, PackageJsonData, 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 @@ -275,6 +272,19 @@ async function* runWatchLoop(
try {
const packageJson = await loadPackageJson(packageJsonPath);
options.packageJson = packageJson;
if (!packageJson.name) {
throw new Error(`The package.json at '${packageJsonPath}' must contain a 'name'.`);
}
options.packageName = packageJson.name;

updateWatchedEntryPoints(
packageJson,
options,
buildState,
allWatchedFiles,
packageJsonPath,
);

hasPackageJsonChanges = true;
} catch (error) {
assertIsError(error);
Expand All @@ -290,7 +300,8 @@ async function* runWatchLoop(

const hasSourceChanges =
!buildState.singleProgramCache ||
Boolean(buildState.hasCompilationError) ||
buildState.hasCompilationError ||
buildState.hasEntryPointsChanges ||
hasModifiedWatchedFile(changedFiles, allWatchedFiles, posixPackageJsonPath);

if (
Expand Down
200 changes: 3 additions & 197 deletions packages/angular/build/src/builders/library/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,66 +14,16 @@ import { normalizeAssetPatterns } from '../../utils';
import { supportColor } from '../../utils/color';
import { assertIsError } from '../../utils/error';
import { normalizeCacheOptions } from '../../utils/normalize-cache';
import { isSubDirectory, toPosixPath } from '../../utils/path';
import { isSubDirectory } from '../../utils/path';
import {
type PostcssConfiguration,
generateSearchDirectories,
getTailwindConfig,
loadPostcssConfiguration,
} from '../../utils/postcss-configuration';
import { getProjectRootPaths } from '../../utils/project-metadata';
import { type NormalizedEntryPoint, getEntryPointBundleName } from './pipeline/entry-points';
import { normalizeEntryPoints } from './pipeline/entry-points';
import type { Schema as LibraryBuilderOptions } from './schema';

export type { NormalizedEntryPoint } from './pipeline/entry-points';

export interface PackageJsonData {
name: string;
version?: string;
type?: string;
main?: string;
module?: string;
typings?: string;
types?: string;
sideEffects?: boolean | string[];
exports?: string | Record<string, unknown>;
scripts?: Record<string, string>;
workspaces?: unknown;
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
peerDependenciesMeta?: Record<string, { optional?: boolean }>;
[key: string]: unknown;
}

export interface NormalizedLibraryOptions {
workspaceRoot: string;
projectRoot: string;
packageName: string;
packageJson: PackageJsonData;
outputPath: string;
deleteOutputPath: boolean;
packageJsonPath: string;
tsConfigPath: string;
entryPoints: Map<string, NormalizedEntryPoint>;
inlineStyleLanguage: 'css' | 'less' | 'sass' | 'scss';
styleIncludePaths: string[];
sass?: StylesheetPluginsass;
assets: ReturnType<typeof normalizeAssetPatterns>;
compilationMode: 'partial' | 'full';
declarationMap: boolean;
allowedNonPeerDependencies: RegExp[];
keepLifecycleScripts: boolean;
watch: boolean;
poll?: number;
preserveSymlinks: boolean;
progress: boolean;
clearScreen?: boolean;
cacheOptions: ReturnType<typeof normalizeCacheOptions>;
postcssConfiguration?: { config: PostcssConfiguration; configPath: string };
tailwindConfiguration?: { file: string; package: string };
colors: boolean;
}
import type { NormalizedLibraryOptions, PackageJsonData } from './types';

export async function normalizeLibraryOptions(
context: BuilderContext,
Expand Down Expand Up @@ -217,147 +167,3 @@ export async function normalizeLibraryOptions(
tailwindConfiguration,
};
}

/**
* Normalizes a single entry point specification.
*
* @param key The entry point key from package.json exports (e.g. '.' or './testing').
* @param posixKey Normalized POSIX key without trailing slashes.
* @param isPrimary Whether this is the primary entry point.
* @param targetPath The relative file path string from exports.
* @param projectRoot The library project root directory.
* @param packageName The root package name (e.g. `@my/lib`).
* @returns The normalized entry point descriptor.
*/
function normalizeEntryPoint(
key: string,
posixKey: string,
isPrimary: boolean,
targetPath: string,
projectRoot: string,
packageName: string,
): NormalizedEntryPoint {
const name = isPrimary
? '.'
: posixKey[0] === '.' && posixKey[1] === '/'
? posixKey.slice(2)
: posixKey;

if (name !== '.' && (path.posix.isAbsolute(name) || name.includes('..'))) {
throw new Error(
`Invalid entry point key '${key}'. Entry point keys must be relative subpaths without '..' (e.g. './testing').`,
);
}

const subpath = isPrimary ? '.' : `./${name}`;
const displayName = isPrimary ? packageName : `${packageName}/${name}`;
const bundleName = getEntryPointBundleName(packageName, name);

const entryFilePath = path.resolve(projectRoot, targetPath);

if (!/(?<!\.d)\.(?:ts|mts)$/.test(entryFilePath)) {
throw new Error(
`Entry point '${key}' file path must be a TypeScript file ('.ts' or '.mts'): '${entryFilePath}'.`,
);
}

return {
subpath,
name,
displayName,
bundleName,
entryFilePath,
isPrimary,
};
}

/**
* Normalizes all entry points from the library's `package.json` `exports` field.
*
* @param rawExports The `exports` field from `package.json`.
* @param projectRoot The library project root directory.
* @param packageJsonPath Path to `package.json` for error reporting.
* @param packageName The root package name (e.g. `@my/lib`).
* @returns A Map of normalized entry points keyed by name.
*/
function normalizeEntryPoints(
rawExports: PackageJsonData['exports'],
projectRoot: string,
packageJsonPath: string,
packageName: string,
): Map<string, NormalizedEntryPoint> {
if (!rawExports || (typeof rawExports !== 'string' && typeof rawExports !== 'object')) {
throw new Error(
`The 'package.json' at '${packageJsonPath}' must contain an 'exports' field defining the primary entry point ('.').`,
);
}

const exportsRecord = typeof rawExports === 'string' ? { '.': rawExports } : rawExports;

const entryPoints = new Map<string, NormalizedEntryPoint>();
let hasPrimary = false;

for (const [key, value] of Object.entries(exportsRecord)) {
let target: string | undefined;

if (typeof value === 'string') {
target = value;
} else if (
typeof value === 'object' &&
value !== null &&
!Array.isArray(value) &&
typeof (value as Record<string, unknown>)['default'] === 'string'
) {
target = (value as Record<string, unknown>)['default'] as string;
}

const posixKey = toPosixPath(key).replace(/\/+$/, '');
const isPrimary = posixKey === '.' || posixKey === '';

if (!target) {
if (isPrimary) {
throw new Error(
`The primary entry point '.' in '${packageJsonPath}' must specify a string path ` +
`or a 'default' condition pointing to a TypeScript file.`,
);
}

// Non-JS/TS conditional export (e.g., sass/style-only subpath); preserve in package.json without compiling.
continue;
}

if (!isPrimary && !/\.m?ts$/.test(target)) {
// Static asset, stylesheet, or package.json export; preserve in package.json without compiling.
continue;
}

const entryPoint = normalizeEntryPoint(
key,
posixKey,
isPrimary,
target,
projectRoot,
packageName,
);

if (entryPoints.has(entryPoint.name)) {
throw new Error(
`Duplicate entry point detected: '${key}' resolves to the same name ('${entryPoint.name}') as an existing entry point.`,
);
}

entryPoints.set(entryPoint.name, entryPoint);

if (entryPoint.isPrimary) {
hasPrimary = true;
}
}

if (!hasPrimary) {
throw new Error(
`The 'exports' field in '${packageJsonPath}' must contain a primary entry point with key '.'.`,
);
}

return entryPoints;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import path from 'node:path';
import picomatch from 'picomatch';
import { toPosixPath } from '../../../utils/path';
import { DEFAULT_ASSET_IGNORE, resolveAssets } from '../../../utils/resolve-assets';
import type { NormalizedLibraryOptions } from '../options';
import type { NormalizedLibraryOptions } from '../types';
import { type DiskOutputFile, createDiskOutputFile } from './utils';

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,14 @@ import { constants, copyFile, mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { emitFilesToDisk } from '../../../tools/esbuild/utils';
import { toPosixPath } from '../../../utils/path';
import type { NormalizedLibraryOptions } from '../options';
import type { NormalizedLibraryOptions, SingleBuildState } from '../types';
import { collectAssetsToEmit } from './assets';
import { type BundleEntryPointInput, type BundleResult, bundleEntryPoints } from './bundler';
import { type SingleProgramCache, compileLibrary } from './compilation';
import { type BundleEntryPointInput, bundleEntryPoints } from './bundler';
import { compileLibrary } from './compilation';
import { generatePackageManifests } from './package-manifests';
import type { createComponentStylesheetBundlerForLibrary } from './stylesheet-bundler';
import type { OutputFile } from './utils';

/**
* State preserved across incremental builds in watch mode.
*/
export interface SingleBuildState {
singleProgramCache?: SingleProgramCache;
previousBundleResults: Map<string, BundleResult>;
pendingChangedEsmFiles: Set<string>;
pendingChangedDtsFiles: Set<string>;
hasCompilationError?: boolean;
hasEmittedManifests?: boolean;
hasEmittedAssets?: boolean;
directoryExists: Set<string>;
}

/**
* Creates a fresh {@link SingleBuildState} instance.
*/
Expand Down Expand Up @@ -86,7 +72,8 @@ export async function buildAction(actionContext: BuildActionContext): Promise<vo
const shouldCompileEntryPoints =
!modifiedFiles ||
!buildState.singleProgramCache ||
Boolean(buildState.hasCompilationError) ||
buildState.hasCompilationError ||
buildState.hasEntryPointsChanges ||
pendingChangedEsmFiles.size > 0 ||
pendingChangedDtsFiles.size > 0 ||
hasModifiedWatchedFile(modifiedFiles, allWatchedFiles, posixPackageJsonPath);
Expand All @@ -100,6 +87,7 @@ export async function buildAction(actionContext: BuildActionContext): Promise<vo

if (shouldCompileEntryPoints) {
buildState.hasCompilationError = true;
buildState.hasEntryPointsChanges = false;

const {
esmFiles,
Expand Down
13 changes: 1 addition & 12 deletions packages/angular/build/src/builders/library/pipeline/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,14 @@ import {
} from 'rolldown';
import { dts } from 'rolldown-plugin-dts';
import { toPosixPath } from '../../../utils/path';
import type { NormalizedEntryPoint, NormalizedLibraryOptions } from '../options';
import type { BundleResult, NormalizedEntryPoint, NormalizedLibraryOptions } from '../types';
import {
FESM_OUTPUT_DIR,
type MemoryOutputFile,
TYPES_OUTPUT_DIR,
createMemoryOutputFile,
} from './utils';

/**
* Cached module ID sets for a bundled entry point.
*/
export interface BundleResult {
/** Exact set of virtual ESM module IDs bundled into this entry point. */
esmModuleIds: ReadonlySet<string>;

/** Exact set of virtual DTS module IDs bundled into this entry point. */
dtsModuleIds: ReadonlySet<string>;
}

/**
* Output of the entry point bundling process.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,11 @@ import {
import type { ComponentStylesheetBundler } from '../../../tools/esbuild/angular/component-stylesheets';
import { useTypeChecking } from '../../../utils/environment-options';
import { toPosixPath } from '../../../utils/path';
import type { NormalizedEntryPoint, NormalizedLibraryOptions } from '../options';
import type { NormalizedEntryPoint, NormalizedLibraryOptions, SingleProgramCache } from '../types';
import { isDeclarationFile, isDeclarationSourceMapFile } from './utils';

const EMITTED_EXTENSIONS = ['.js', '.mjs', '.cjs', '.d.ts', '.d.mts', '.d.cts'];

/**
* Cached state for the single unified library compilation.
*/
export interface SingleProgramCache {
/** The active Angular compilation instance. */
readonly compilationInstance: AngularCompilation;

/** In-memory map of emitted JavaScript files keyed by relative output path. */
readonly esmFiles: Map<string, string>;

/** In-memory map of emitted TypeScript declaration files keyed by relative output path. */
readonly dtsFiles: Map<string, string>;

/** Set of file paths that failed during stylesheet bundling or compilation. */
readonly failedFiles?: ReadonlySet<string>;
}

/**
* Output of the unified library compilation step.
*/
Expand Down
Loading
Loading