Skip to content

Commit 95a7218

Browse files
committed
fixup! feat(@angular/build): add library builder
1 parent 68c0da7 commit 95a7218

16 files changed

Lines changed: 460 additions & 146 deletions

‎packages/angular/build/src/builders/library/builder.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,6 @@ export async function* executeLibraryBuilder(
148148

149149
for (const { entryPoint } of graph.nodes.values()) {
150150
allWatchedFiles.add(entryPoint.entryFilePath);
151-
allWatchedFiles.add(entryPoint.tsConfigPath);
152151
}
153152

154153
if (isWatchMode) {

‎packages/angular/build/src/builders/library/options.ts‎

Lines changed: 64 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,6 @@ export interface NormalizedEntryPoint {
4141
/** Absolute path to entry file. */
4242
entryFilePath: string;
4343

44-
/** Absolute path to tsConfig file for this entry point. */
45-
tsConfigPath: string;
46-
4744
/** Is this the primary entry point ('.')? */
4845
isPrimary: boolean;
4946
}
@@ -57,7 +54,7 @@ export interface PackageJsonData {
5754
typings?: string;
5855
types?: string;
5956
sideEffects?: boolean | string[];
60-
exports?: Record<string, unknown>;
57+
exports?: string | Record<string, unknown>;
6158
scripts?: Record<string, string>;
6259
workspaces?: unknown;
6360
dependencies?: Record<string, string>;
@@ -119,7 +116,6 @@ export async function normalizeLibraryOptions(
119116

120117
const {
121118
tsConfig,
122-
entryPoints: rawEntryPoints,
123119
assets: rawAssets,
124120
stylePreprocessorOptions,
125121
inlineStyleLanguage = 'css',
@@ -155,10 +151,9 @@ export async function normalizeLibraryOptions(
155151
}
156152

157153
const entryPoints = normalizeEntryPoints(
158-
rawEntryPoints,
159-
workspaceRoot,
160-
resolvedTsConfigPath,
161-
projectName,
154+
packageJson.exports,
155+
projectRoot,
156+
packageJsonPath,
162157
packageName,
163158
);
164159

@@ -241,18 +236,16 @@ export async function normalizeLibraryOptions(
241236
/**
242237
* Normalizes a single entry point specification.
243238
*
244-
* @param key The entry point key from configuration (e.g. '.' or './testing').
245-
* @param value The entry point file path string or object with entryPoint and tsConfig.
246-
* @param workspaceRoot The workspace root directory.
247-
* @param defaultTsConfigPath The default tsConfig path for the project.
239+
* @param key The entry point key from package.json exports (e.g. '.' or './testing').
240+
* @param targetPath The relative file path string from exports.
241+
* @param projectRoot The library project root directory.
248242
* @param packageName The root package name (e.g. `@my/lib`).
249243
* @returns The normalized entry point descriptor.
250244
*/
251245
function normalizeEntryPoint(
252246
key: string,
253-
value: LibraryBuilderOptions['entryPoints'][string],
254-
workspaceRoot: string,
255-
defaultTsConfigPath: string,
247+
targetPath: string,
248+
projectRoot: string,
256249
packageName: string,
257250
): NormalizedEntryPoint {
258251
const posixKey = toPosixPath(key).replace(/\/+$/, '');
@@ -265,69 +258,95 @@ function normalizeEntryPoint(
265258

266259
if (name !== '.' && (path.posix.isAbsolute(name) || name.includes('..'))) {
267260
throw new Error(
268-
`Invalid entry point key '${key}'. Entry point keys must be relative subpaths without '..' (e.g. './testing' or 'testing').`,
261+
`Invalid entry point key '${key}'. Entry point keys must be relative subpaths without '..' (e.g. './testing').`,
269262
);
270263
}
271264

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

276-
const entryFilePath = path.resolve(
277-
workspaceRoot,
278-
typeof value === 'string' ? value : value.entryPoint,
279-
);
269+
const entryFilePath = path.resolve(projectRoot, targetPath);
280270

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

287-
const tsConfigPath =
288-
typeof value !== 'string' && value.tsConfig
289-
? path.resolve(workspaceRoot, value.tsConfig)
290-
: defaultTsConfigPath;
291-
292277
return {
293278
subpath,
294279
name,
295280
displayName,
296281
bundleName,
297282
entryFilePath,
298-
tsConfigPath,
299283
isPrimary,
300284
};
301285
}
302286

303287
/**
304-
* Normalizes all entry points for the library project.
288+
* Normalizes all entry points from the library's `package.json` `exports` field.
305289
*
306-
* @param rawEntryPoints The raw entryPoints dictionary from schema options.
307-
* @param workspaceRoot The workspace root directory.
308-
* @param defaultTsConfigPath The default tsConfig path for the project.
309-
* @param projectName The project name used in error reporting.
290+
* @param rawExports The `exports` field from `package.json`.
291+
* @param projectRoot The library project root directory.
292+
* @param packageJsonPath Path to `package.json` for error reporting.
310293
* @param packageName The root package name (e.g. `@my/lib`).
311294
* @returns A Map of normalized entry points keyed by name.
312295
*/
313296
function normalizeEntryPoints(
314-
rawEntryPoints: LibraryBuilderOptions['entryPoints'],
315-
workspaceRoot: string,
316-
defaultTsConfigPath: string,
317-
projectName: string,
297+
rawExports: PackageJsonData['exports'],
298+
projectRoot: string,
299+
packageJsonPath: string,
318300
packageName: string,
319301
): Map<string, NormalizedEntryPoint> {
302+
if (!rawExports || (typeof rawExports !== 'string' && typeof rawExports !== 'object')) {
303+
throw new Error(
304+
`The 'package.json' at '${packageJsonPath}' must contain an 'exports' field defining the primary entry point ('.').`,
305+
);
306+
}
307+
308+
const exportsRecord = typeof rawExports === 'string' ? { '.': rawExports } : rawExports;
309+
320310
const entryPoints = new Map<string, NormalizedEntryPoint>();
321311
let hasPrimary = false;
322312

323-
for (const [key, value] of Object.entries(rawEntryPoints)) {
324-
const entryPoint = normalizeEntryPoint(
325-
key,
326-
value,
327-
workspaceRoot,
328-
defaultTsConfigPath,
329-
packageName,
330-
);
313+
for (const [key, value] of Object.entries(exportsRecord)) {
314+
let target: string | undefined;
315+
if (typeof value === 'string') {
316+
target = value;
317+
} else if (
318+
typeof value === 'object' &&
319+
value !== null &&
320+
!Array.isArray(value) &&
321+
typeof (value as Record<string, unknown>)['default'] === 'string'
322+
) {
323+
target = (value as Record<string, unknown>)['default'] as string;
324+
}
325+
326+
const posixKey = toPosixPath(key).replace(/\/+$/, '');
327+
const isPrimary = posixKey === '.' || posixKey === '';
328+
329+
if (!target) {
330+
if (isPrimary) {
331+
throw new Error(
332+
`The primary entry point '.' in '${packageJsonPath}' must specify a string path ` +
333+
`or a 'default' condition pointing to a TypeScript file.`,
334+
);
335+
}
336+
// Non-JS/TS conditional export (e.g., sass/style-only subpath); preserve in package.json without compiling.
337+
continue;
338+
}
339+
340+
if (!isPrimary) {
341+
const isTsSource = /\.(?:ts|mts)$/.test(target) && !/\.d\.(?:ts|mts)$/.test(target);
342+
const isInvalidCodeEntry = /\.(?:d\.[cm]?ts|cts|tsx|jsx)$/.test(target);
343+
if (!isTsSource && !isInvalidCodeEntry) {
344+
// Static asset, stylesheet, or package.json export; preserve in package.json without compiling.
345+
continue;
346+
}
347+
}
348+
349+
const entryPoint = normalizeEntryPoint(key, target, projectRoot, packageName);
331350
if (entryPoints.has(entryPoint.name)) {
332351
throw new Error(
333352
`Duplicate entry point detected: '${key}' resolves to the same name ('${entryPoint.name}') as an existing entry point.`,
@@ -341,7 +360,7 @@ function normalizeEntryPoints(
341360

342361
if (!hasPrimary) {
343362
throw new Error(
344-
`The 'entryPoints' option in project '${projectName}' must contain a primary entry point with key '.'.`,
363+
`The 'exports' field in '${packageJsonPath}' must contain a primary entry point with key '.'.`,
345364
);
346365
}
347366

‎packages/angular/build/src/builders/library/pipeline/compilation.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export interface StylesheetBundlerAdapter {
8282

8383
export type CompileEntryPointOptions = Pick<
8484
NormalizedLibraryOptions,
85+
| 'tsConfigPath'
8586
| 'compilationMode'
8687
| 'declarationMap'
8788
| 'packageName'
@@ -113,8 +114,9 @@ export async function compileEntryPoint(
113114
upstreamDtsFiles?: Map<string, string>,
114115
sourceFileCache?: Map<string, ts.SourceFile>,
115116
): Promise<CompilationResult> {
116-
const { entryFilePath, tsConfigPath, bundleName } = entryPoint;
117+
const { entryFilePath, bundleName } = entryPoint;
117118
const {
119+
tsConfigPath,
118120
compilationMode,
119121
declarationMap,
120122
cacheOptions,

‎packages/angular/build/src/builders/library/pipeline/compiler-worker.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export async function compileEntryPointInWorker(
105105
modifiedFiles?: string[],
106106
): Promise<CompilationOutput> {
107107
const workerOptions: CompileWorkerOptions = {
108+
tsConfigPath: options.tsConfigPath,
108109
compilationMode: options.compilationMode,
109110
declarationMap: options.declarationMap,
110111
packageName: options.packageName,

‎packages/angular/build/src/builders/library/pipeline/entry-point-graph.ts‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,20 +156,18 @@ export class EntryPointGraph {
156156
private cachedNodeMeta?: Array<{
157157
node: EntryPointNode;
158158
entryFile: string;
159-
tsConfig: string;
160159
dirWithSep: string;
161160
}>;
162161

163162
private getNodeMeta() {
164163
this.cachedNodeMeta ??= Array.from(this.nodes.values())
165164
.map((node) => {
166-
const { entryFilePath, tsConfigPath } = node.entryPoint;
165+
const { entryFilePath } = node.entryPoint;
167166
const nodeDir = toPosixPath(path.dirname(entryFilePath));
168167

169168
return {
170169
node,
171170
entryFile: toPosixPath(entryFilePath),
172-
tsConfig: toPosixPath(tsConfigPath),
173171
dirWithSep: nodeDir.endsWith('/') ? nodeDir : `${nodeDir}/`,
174172
};
175173
})
@@ -191,8 +189,8 @@ export class EntryPointGraph {
191189
for (const file of changedFiles) {
192190
let matched = false;
193191

194-
for (const { node, entryFile, tsConfig } of nodeMeta) {
195-
if (file === entryFile || file === tsConfig || node.referencedFiles.has(file)) {
192+
for (const { node, entryFile } of nodeMeta) {
193+
if (file === entryFile || node.referencedFiles.has(file)) {
196194
node.isDirty = true;
197195
hasChanges = true;
198196
matched = true;
@@ -329,7 +327,7 @@ export async function buildEntryPointGraph(
329327
for (const dep of dependencies) {
330328
if (!graph.nodes.has(dep)) {
331329
throw new Error(
332-
`Entry point '${dep}' imported by '${entryPoint.name}' does not exist in 'entryPoints'.`,
330+
`Entry point '${dep}' imported by '${entryPoint.name}' does not exist in 'package.json' exports.`,
333331
);
334332
}
335333

‎packages/angular/build/src/builders/library/pipeline/package-manifests_spec.ts‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ describe('generatePackageManifests', () => {
8686
displayName: packageName,
8787
bundleName: primaryBundleName,
8888
entryFilePath: join(tempDir, 'src/public-api.ts'),
89-
tsConfigPath: join(tempDir, 'tsconfig.lib.json'),
9089
isPrimary: true,
9190
});
9291

@@ -98,7 +97,6 @@ describe('generatePackageManifests', () => {
9897
displayName: `${packageName}/testing`,
9998
bundleName: secondaryBundleName,
10099
entryFilePath: join(tempDir, 'testing/src/public-api.ts'),
101-
tsConfigPath: join(tempDir, 'tsconfig.lib.json'),
102100
isPrimary: false,
103101
});
104102
}

‎packages/angular/build/src/builders/library/schema.json‎

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,6 @@
44
"description": "Library builder target options for Build Architect. Builds an Angular library package conforming to the Angular Package Format (APF).",
55
"type": "object",
66
"properties": {
7-
"entryPoints": {
8-
"type": "object",
9-
"description": "Map of package entry points. The '.' key represents the primary entry point; other keys define secondary subpath entry points.",
10-
"required": ["."],
11-
"additionalProperties": {
12-
"oneOf": [
13-
{
14-
"type": "string",
15-
"description": "Path to the entry file (e.g. 'projects/my-lib/src/public-api.ts')."
16-
},
17-
{
18-
"$ref": "#/definitions/entryPoint"
19-
}
20-
]
21-
}
22-
},
237
"tsConfig": {
248
"type": "string",
259
"description": "The full path for the TypeScript configuration file, relative to the current workspace root."
@@ -139,23 +123,8 @@
139123
}
140124
},
141125
"additionalProperties": false,
142-
"required": ["tsConfig", "entryPoints"],
126+
"required": ["tsConfig"],
143127
"definitions": {
144-
"entryPoint": {
145-
"type": "object",
146-
"properties": {
147-
"entryPoint": {
148-
"type": "string",
149-
"description": "Path to the entry file."
150-
},
151-
"tsConfig": {
152-
"type": "string",
153-
"description": "Optional TypeScript configuration file specific to this entry point."
154-
}
155-
},
156-
"required": ["entryPoint"],
157-
"additionalProperties": false
158-
},
159128
"assetPattern": {
160129
"oneOf": [
161130
{

‎packages/angular/build/src/builders/library/tests/behavior/apf_spec.ts‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,18 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) =>
2121
'projects/lib/secondary/src/public-api.ts': 'export const SECONDARY_VALUE = 42;\n',
2222
});
2323

24+
await harness.modifyFile('projects/lib/package.json', (content) => {
25+
const pkg = JSON.parse(content);
26+
pkg.exports = {
27+
'.': './src/public-api.ts',
28+
'./secondary': './secondary/src/public-api.ts',
29+
};
30+
31+
return JSON.stringify(pkg, null, 2);
32+
});
33+
2434
harness.useTarget('build', {
2535
...BASE_OPTIONS,
26-
entryPoints: {
27-
'.': 'projects/lib/src/public-api.ts',
28-
'secondary': 'projects/lib/secondary/src/public-api.ts',
29-
},
3036
assets: [
3137
'projects/lib/README.md',
3238
'projects/lib/LICENSE',

0 commit comments

Comments
 (0)