Skip to content

Commit 50434a9

Browse files
committed
feat: add secondary entrypoint schematic
1 parent 214ac89 commit 50434a9

File tree

7 files changed

+314
-0
lines changed

7 files changed

+314
-0
lines changed

packages/schematics/angular/collection.json

+6
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@
117117
"schema": "./library/schema.json",
118118
"description": "Generate a library project for Angular."
119119
},
120+
"library-secondary-entrypoint": {
121+
"aliases": ["secondary"],
122+
"factory": "./secondary-entrypoint",
123+
"schema": "./secondary-entrypoint/schema.json",
124+
"description": "Generate a secondary-entrypoint in a library project for Angular."
125+
},
120126
"web-worker": {
121127
"factory": "./web-worker",
122128
"schema": "./web-worker/schema.json",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# <%= secondaryEntryPoint %>
2+
3+
Secondary entry point of `<%= mainEntryPoint %>`. It can be used by importing from `<%= secondaryEntryPoint %>`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"$schema": "<%= relativePathToWorkspaceRoot %>/node_modules/ng-packagr/ng-package.schema.json",
3+
"lib": {
4+
"entryFile": "src/<%= entryFile %>.ts"
5+
}
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/*
2+
* Public API Surface of <%= dasherize(name) %>
3+
*/
4+
5+
export const greeting = 'Hello Angular World!';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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 {
10+
Rule,
11+
SchematicsException,
12+
Tree,
13+
apply,
14+
applyTemplates,
15+
chain,
16+
mergeWith,
17+
move,
18+
strings,
19+
url,
20+
} from '@angular-devkit/schematics';
21+
import { JSONFile } from '../utility/json-file';
22+
import { latestVersions } from '../utility/latest-versions';
23+
import { relativePathToWorkspaceRoot } from '../utility/paths';
24+
import { buildDefaultPath, getWorkspace } from '../utility/workspace';
25+
import { ProjectType } from '../utility/workspace-models';
26+
import { Schema as LibraryOptions } from './schema';
27+
28+
function updateTsConfig(packageName: string, ...paths: string[]) {
29+
return (host: Tree) => {
30+
if (!host.exists('tsconfig.json')) {
31+
return host;
32+
}
33+
34+
const file = new JSONFile(host, 'tsconfig.json');
35+
const jsonPath = ['compilerOptions', 'paths', packageName];
36+
const value = file.get(jsonPath);
37+
file.modify(jsonPath, Array.isArray(value) ? [...value, ...paths] : paths);
38+
};
39+
}
40+
41+
export default function (options: LibraryOptions): Rule {
42+
return async (host: Tree) => {
43+
return async (host: Tree) => {
44+
const workspace = await getWorkspace(host);
45+
const project = workspace.projects.get(options.project);
46+
47+
if (!project) {
48+
throw new SchematicsException(`Project "${options.project}" does not exist.`);
49+
}
50+
51+
if (project?.extensions.projectType !== ProjectType.Library) {
52+
throw new SchematicsException(
53+
`Secondary Entrypoint schematic requires a project type of "library".`,
54+
);
55+
}
56+
57+
const path = buildDefaultPath(project);
58+
const libDir = `${path}/${options.name}`;
59+
const pkgPath = `${project.root}/package.json`;
60+
61+
const pkg = host.readJson(pkgPath) as { name: string } | null;
62+
if (pkg === null) {
63+
throw new SchematicsException('Could not find package.json');
64+
}
65+
66+
const mainEntryPoint = pkg.name;
67+
const secondaryEntryPoint = `${mainEntryPoint}/${options.name}`;
68+
69+
let folderName = mainEntryPoint.startsWith('@') ? mainEntryPoint.slice(1) : mainEntryPoint;
70+
if (/[A-Z]/.test(folderName)) {
71+
folderName = strings.dasherize(folderName);
72+
}
73+
74+
const distRoot = `dist/${folderName}/${options.name}`;
75+
76+
const templateSource = apply(url('./files'), [
77+
applyTemplates({
78+
...strings,
79+
...options,
80+
mainEntryPoint,
81+
secondaryEntryPoint,
82+
relativePathToWorkspaceRoot: relativePathToWorkspaceRoot(libDir),
83+
packageName: options.name,
84+
angularLatestVersion: latestVersions.Angular.replace(/~|\^/, ''),
85+
tsLibLatestVersion: latestVersions['tslib'].replace(/~|\^/, ''),
86+
}),
87+
move(libDir),
88+
]);
89+
90+
return chain([
91+
mergeWith(templateSource),
92+
updateTsConfig(secondaryEntryPoint, './' + distRoot),
93+
]);
94+
};
95+
};
96+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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 { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
10+
import { Schema as LibraryOptions } from '../library/schema';
11+
import { Schema as WorkspaceOptions } from '../workspace/schema';
12+
import { Schema as GenerateLibrarySchema } from './schema';
13+
import { parse as parseJson } from 'jsonc-parser';
14+
15+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
16+
function getJsonFileContent(tree: UnitTestTree, path: string): any {
17+
return parseJson(tree.readContent(path).toString());
18+
}
19+
20+
describe('Secondary Entrypoint Schematic', () => {
21+
const schematicRunner = new SchematicTestRunner(
22+
'@schematics/ng_packagr',
23+
require.resolve('../collection.json'),
24+
);
25+
const defaultOptions: GenerateLibrarySchema = {
26+
name: 'foo-secondary',
27+
project: 'foo',
28+
};
29+
30+
const workspaceOptions: WorkspaceOptions = {
31+
name: 'workspace',
32+
newProjectRoot: 'projects',
33+
34+
version: '6.0.0',
35+
};
36+
const libaryOptions: LibraryOptions = {
37+
name: 'foo',
38+
entryFile: 'my-index',
39+
standalone: true,
40+
skipPackageJson: false,
41+
skipTsConfig: false,
42+
skipInstall: false,
43+
};
44+
45+
let workspaceTree: UnitTestTree;
46+
beforeEach(async () => {
47+
workspaceTree = await schematicRunner.runSchematic('workspace', workspaceOptions);
48+
});
49+
50+
it('should create correct files', async () => {
51+
workspaceTree = await schematicRunner.runSchematic('library', libaryOptions, workspaceTree);
52+
const tree = await schematicRunner.runSchematic(
53+
'secondary',
54+
{ ...defaultOptions },
55+
workspaceTree,
56+
);
57+
const files = tree.files;
58+
59+
expect(files).toEqual(
60+
jasmine.arrayContaining([
61+
'/projects/foo/src/lib/foo-secondary/README.md',
62+
'/projects/foo/src/lib/foo-secondary/ng-package.json',
63+
'/projects/foo/src/lib/foo-secondary/src/public-api.ts',
64+
]),
65+
);
66+
});
67+
68+
it('should set correct main and secondary entrypoints in the README', async () => {
69+
workspaceTree = await schematicRunner.runSchematic('library', libaryOptions, workspaceTree);
70+
const tree = await schematicRunner.runSchematic(
71+
'secondary',
72+
{ ...defaultOptions },
73+
workspaceTree,
74+
);
75+
const content = tree.readContent('/projects/foo/src/lib/foo-secondary/README.md');
76+
expect(content).toMatch('# foo/foo-secondary');
77+
});
78+
79+
it('should set a custom entryfile', async () => {
80+
workspaceTree = await schematicRunner.runSchematic('library', libaryOptions, workspaceTree);
81+
const tree = await schematicRunner.runSchematic(
82+
'secondary',
83+
{ ...defaultOptions, entryFile: 'my-index' },
84+
workspaceTree,
85+
);
86+
const files = tree.files;
87+
expect(files).toEqual(
88+
jasmine.arrayContaining([
89+
'/projects/foo/src/lib/foo-secondary/README.md',
90+
'/projects/foo/src/lib/foo-secondary/ng-package.json',
91+
'/projects/foo/src/lib/foo-secondary/src/my-index.ts',
92+
]),
93+
);
94+
});
95+
96+
it('should handle scope packages', async () => {
97+
workspaceTree = await schematicRunner.runSchematic(
98+
'library',
99+
{ ...libaryOptions, name: '@scope/package' },
100+
workspaceTree,
101+
);
102+
const tree = await schematicRunner.runSchematic(
103+
'secondary',
104+
{ ...defaultOptions, name: 'testing', project: '@scope/package' },
105+
workspaceTree,
106+
);
107+
const files = tree.files;
108+
expect(files).toEqual(
109+
jasmine.arrayContaining([
110+
'/projects/scope/package/src/lib/testing/README.md',
111+
'/projects/scope/package/src/lib/testing/ng-package.json',
112+
'/projects/scope/package/src/lib/testing/src/public-api.ts',
113+
]),
114+
);
115+
});
116+
117+
it(`should add paths mapping to the tsconfig`, async () => {
118+
workspaceTree = await schematicRunner.runSchematic(
119+
'library',
120+
{ ...libaryOptions, name: '@scope/package' },
121+
workspaceTree,
122+
);
123+
const tree = await schematicRunner.runSchematic(
124+
'secondary',
125+
{ ...defaultOptions, name: 'testing', project: '@scope/package' },
126+
workspaceTree,
127+
);
128+
129+
const tsConfigJson = getJsonFileContent(tree, 'tsconfig.json');
130+
expect(tsConfigJson.compilerOptions.paths['@scope/package/testing']).toEqual([
131+
'./dist/scope/package/testing',
132+
]);
133+
});
134+
135+
it(`should append to existing paths mappings`, async () => {
136+
workspaceTree = await schematicRunner.runSchematic(
137+
'library',
138+
{ ...libaryOptions, name: '@scope/package' },
139+
workspaceTree,
140+
);
141+
workspaceTree.overwrite(
142+
'tsconfig.json',
143+
JSON.stringify({
144+
compilerOptions: {
145+
paths: {
146+
'unrelated': ['./something/else.ts'],
147+
'@scope/package/testing': ['libs/*'],
148+
},
149+
},
150+
}),
151+
);
152+
const tree = await schematicRunner.runSchematic(
153+
'secondary',
154+
{ ...defaultOptions, name: 'testing', project: '@scope/package' },
155+
workspaceTree,
156+
);
157+
158+
const tsConfigJson = getJsonFileContent(tree, 'tsconfig.json');
159+
expect(tsConfigJson.compilerOptions.paths['@scope/package/testing']).toEqual([
160+
'libs/*',
161+
'./dist/scope/package/testing',
162+
]);
163+
});
164+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema",
3+
"$id": "SchematicsLibrary",
4+
"title": "Secondary Entrypoint Schema",
5+
"type": "object",
6+
"description": "Creates a secondary entrypoint in a library project in a project.",
7+
"additionalProperties": false,
8+
"properties": {
9+
"name": {
10+
"type": "string",
11+
"description": "The name of the library.",
12+
"pattern": "^(?:@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*/)?[a-zA-Z0-9-~][a-zA-Z0-9-._~]*$",
13+
"$default": {
14+
"$source": "argv",
15+
"index": 0
16+
},
17+
"x-prompt": "What name would you like to use for the secondary entrypoint?"
18+
},
19+
"project": {
20+
"type": "string",
21+
"description": "The name of the project.",
22+
"$default": {
23+
"$source": "projectName"
24+
}
25+
},
26+
"entryFile": {
27+
"type": "string",
28+
"format": "path",
29+
"description": "The path at which to create the library's secondary public API file, relative to the workspace root.",
30+
"default": "public-api"
31+
}
32+
},
33+
"required": ["name", "project"]
34+
}

0 commit comments

Comments
 (0)