Skip to content

Commit c033dbc

Browse files
committed
feat(@schematics/angular): use library builder and add entry-point schematic
Update the library schematic to configure @angular/build:library with a single tsconfig.lib.json instead of @angular/build:ng-packagr, and add an entry-point schematic to generate secondary entry points under src/<name>/.
1 parent 435ce82 commit c033dbc

14 files changed

Lines changed: 775 additions & 144 deletions

File tree

‎packages/angular/cli/lib/config/workspace-schema.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@
147147
"@schematics/angular:directive": {
148148
"$ref": "../../../../schematics/angular/directive/schema.json"
149149
},
150+
"@schematics/angular:entry-point": {
151+
"$ref": "../../../../schematics/angular/entry-point/schema.json"
152+
},
150153
"@schematics/angular:enum": {
151154
"$ref": "../../../../schematics/angular/enum/schema.json"
152155
},

‎packages/schematics/angular/collection.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@
111111
"schema": "./library/schema.json",
112112
"description": "Generate a library project for Angular."
113113
},
114+
"entry-point": {
115+
"factory": "./entry-point",
116+
"schema": "./entry-point/schema.json",
117+
"description": "Generate a secondary entry point for an Angular library."
118+
},
114119
"web-worker": {
115120
"factory": "./web-worker",
116121
"schema": "./web-worker/schema.json",

packages/schematics/angular/library/files/src/__entryFile__.ts.template renamed to packages/schematics/angular/entry-point/files/__entryFile__.ts.template

File renamed without changes.
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { type JsonObject, type JsonValue, isJsonObject } from '@angular-devkit/core';
10+
import {
11+
type Rule,
12+
SchematicsException,
13+
type Tree,
14+
apply,
15+
applyTemplates,
16+
chain,
17+
mergeWith,
18+
move,
19+
noop,
20+
schematic,
21+
strings,
22+
url,
23+
} from '@angular-devkit/schematics';
24+
import { basename, join, relative } from 'node:path/posix';
25+
import { JSONFile } from '../utility/json-file';
26+
import { createProjectSchematic } from '../utility/project';
27+
import { ProjectType } from '../utility/workspace-models';
28+
import type { Schema as EntryPointOptions } from './schema';
29+
30+
function updateTsConfig(packageName: string, ...paths: string[]): Rule {
31+
return (host: Tree) => {
32+
if (!host.exists('tsconfig.json')) {
33+
return host;
34+
}
35+
36+
const file = new JSONFile(host, 'tsconfig.json');
37+
const jsonPath = ['compilerOptions', 'paths', packageName];
38+
const value = file.get(jsonPath);
39+
file.modify(jsonPath, Array.isArray(value) ? Array.from(new Set([...value, ...paths])) : paths);
40+
};
41+
}
42+
43+
export default createProjectSchematic<EntryPointOptions>((options, { project, tree }) => {
44+
if (project.extensions['projectType'] !== ProjectType.Library) {
45+
throw new SchematicsException(`Project "${options.project}" is not a library project.`);
46+
}
47+
48+
if (options.name === 'lib' || options.name.startsWith('lib/')) {
49+
throw new SchematicsException(
50+
`Entry point name "${options.name}" cannot be "lib" or start with "lib/" ` +
51+
'as it conflicts with the primary entry point source directory.',
52+
);
53+
}
54+
55+
const libDir = project.root;
56+
const packageJsonPath = join(libDir, 'package.json');
57+
if (!tree.exists(packageJsonPath)) {
58+
throw new SchematicsException(`Could not find package.json at "${packageJsonPath}".`);
59+
}
60+
61+
const packageJson = new JSONFile(tree, packageJsonPath);
62+
const rawPackageName = packageJson.get(['name']);
63+
const packageName =
64+
typeof rawPackageName === 'string' && rawPackageName
65+
? rawPackageName
66+
: strings.dasherize(options.project);
67+
68+
const isPrimary = options.name === '.';
69+
const entryFile = (options.entryFile ?? 'public-api').replace(/\.[cm]?ts$/i, '');
70+
const sourceRoot = project.sourceRoot ?? join(libDir, 'src');
71+
const entryPointDir = join(sourceRoot, options.name);
72+
const exportKey = isPrimary ? '.' : `./${options.name}`;
73+
const exportTarget = `./${relative(libDir, join(entryPointDir, `${entryFile}.ts`))}`;
74+
75+
const existingExports = (packageJson.get(['exports']) as JsonValue | undefined) ?? null;
76+
let exportsObject: JsonObject = {};
77+
if (typeof existingExports === 'string' || Array.isArray(existingExports)) {
78+
exportsObject = { '.': existingExports };
79+
} else if (isJsonObject(existingExports)) {
80+
const keys = Object.keys(existingExports);
81+
if (keys.some((k) => k.startsWith('.'))) {
82+
exportsObject = { ...existingExports };
83+
} else if (keys.length > 0) {
84+
exportsObject = { '.': existingExports };
85+
}
86+
}
87+
88+
if (Object.prototype.hasOwnProperty.call(exportsObject, exportKey)) {
89+
throw new SchematicsException(
90+
`Entry point "${exportKey}" already exists in "${packageJsonPath}".`,
91+
);
92+
}
93+
94+
if (
95+
isJsonObject(existingExports) &&
96+
Object.keys(existingExports).some((k) => k.startsWith('.'))
97+
) {
98+
packageJson.modify(['exports', exportKey], exportTarget);
99+
} else {
100+
packageJson.modify(['exports'], {
101+
...exportsObject,
102+
[exportKey]: exportTarget,
103+
});
104+
}
105+
106+
const prefix = options.prefix ?? project.prefix ?? 'lib';
107+
const sourceDir = join(entryPointDir, 'lib');
108+
const artifactName = isPrimary
109+
? /^@.*\/.*/.test(options.project)
110+
? options.project.split('/')[1]
111+
: options.project
112+
: basename(options.name);
113+
114+
const buildOptions = project.targets.get('build')?.options;
115+
let distRoot: string;
116+
if (typeof buildOptions?.['outputPath'] === 'string') {
117+
distRoot = buildOptions['outputPath'];
118+
} else {
119+
let folderName = options.project.startsWith('@') ? options.project.slice(1) : options.project;
120+
if (/[A-Z]/.test(folderName)) {
121+
folderName = strings.dasherize(folderName);
122+
}
123+
distRoot = `dist/${folderName}`;
124+
}
125+
126+
const tsConfigPathKey = isPrimary ? packageName : `${packageName}/${options.name}`;
127+
const tsConfigPathTarget = `./${join(distRoot, options.name)}`;
128+
129+
const templateSource = apply(url('./files'), [
130+
applyTemplates({
131+
...strings,
132+
...options,
133+
name: artifactName,
134+
entryFile,
135+
}),
136+
move(entryPointDir),
137+
]);
138+
139+
return chain([
140+
mergeWith(templateSource),
141+
options.skipTsConfig ? noop() : updateTsConfig(tsConfigPathKey, tsConfigPathTarget),
142+
options.standalone
143+
? noop()
144+
: schematic('module', {
145+
name: artifactName,
146+
commonModule: false,
147+
flat: true,
148+
path: sourceDir,
149+
project: options.project,
150+
// Explicitly set the `typeSeparator` this also ensures that the generated files are valid even if the `module` schematic
151+
// inherits its `typeSeparator` from the workspace.
152+
typeSeparator: '-',
153+
}),
154+
schematic('component', {
155+
name: artifactName,
156+
selector: `${prefix}-${strings.dasherize(artifactName)}`,
157+
inlineStyle: true,
158+
inlineTemplate: true,
159+
flat: true,
160+
path: sourceDir,
161+
export: true,
162+
standalone: options.standalone,
163+
skipTests: options.skipTests,
164+
project: options.project,
165+
// Explicitly set an empty `type` since it doesn't necessarily make sense in a library.
166+
// This also ensures that the generated files are valid even if the `component` schematic
167+
// inherits its `type` from the workspace.
168+
type: '',
169+
}),
170+
]);
171+
});

0 commit comments

Comments
 (0)