Skip to content

Commit

Permalink
feat: add secondary entrypoint schematic
Browse files Browse the repository at this point in the history
  • Loading branch information
Jefiozie committed Nov 4, 2024
1 parent d16a9aa commit 6d617b8
Show file tree
Hide file tree
Showing 7 changed files with 314 additions and 0 deletions.
6 changes: 6 additions & 0 deletions packages/schematics/angular/collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@
"schema": "./library/schema.json",
"description": "Generate a library project for Angular."
},
"library-secondary-entrypoint": {
"aliases": ["secondary"],
"factory": "./secondary-entrypoint",
"schema": "./secondary-entrypoint/schema.json",
"description": "Generate a secondary-entrypoint in a library project for Angular."
},
"web-worker": {
"factory": "./web-worker",
"schema": "./web-worker/schema.json",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# <%= secondaryEntryPoint %>

Secondary entry point of `<%= mainEntryPoint %>`. It can be used by importing from `<%= secondaryEntryPoint %>`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "<%= relativePathToWorkspaceRoot %>/node_modules/ng-packagr/ng-package.schema.json",
"lib": {
"entryFile": "src/<%= entryFile %>.ts"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/*
* Public API Surface of <%= dasherize(name) %>
*/

export const greeting = 'Hello Angular World!';
96 changes: 96 additions & 0 deletions packages/schematics/angular/secondary-entrypoint/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import {
Rule,
SchematicsException,
Tree,
apply,
applyTemplates,
chain,
mergeWith,
move,
strings,
url,
} from '@angular-devkit/schematics';
import { JSONFile } from '../utility/json-file';
import { latestVersions } from '../utility/latest-versions';
import { relativePathToWorkspaceRoot } from '../utility/paths';
import { buildDefaultPath, getWorkspace } from '../utility/workspace';
import { ProjectType } from '../utility/workspace-models';
import { Schema as LibraryOptions } from './schema';

function updateTsConfig(packageName: string, ...paths: string[]) {
return (host: Tree) => {
if (!host.exists('tsconfig.json')) {
return host;
}

const file = new JSONFile(host, 'tsconfig.json');
const jsonPath = ['compilerOptions', 'paths', packageName];
const value = file.get(jsonPath);
file.modify(jsonPath, Array.isArray(value) ? [...value, ...paths] : paths);
};
}

export default function (options: LibraryOptions): Rule {
return async (host: Tree) => {
return async (host: Tree) => {
const workspace = await getWorkspace(host);
const project = workspace.projects.get(options.project);

if (!project) {
throw new SchematicsException(`Project "${options.project}" does not exist.`);
}

if (project?.extensions.projectType !== ProjectType.Library) {
throw new SchematicsException(
`Secondary Entrypoint schematic requires a project type of "library".`,
);
}

const path = buildDefaultPath(project);
const libDir = `${path}/${options.name}`;
const pkgPath = `${project.root}/package.json`;

const pkg = host.readJson(pkgPath) as { name: string } | null;
if (pkg === null) {
throw new SchematicsException('Could not find package.json');
}

const mainEntryPoint = pkg.name;
const secondaryEntryPoint = `${mainEntryPoint}/${options.name}`;

let folderName = mainEntryPoint.startsWith('@') ? mainEntryPoint.slice(1) : mainEntryPoint;
if (/[A-Z]/.test(folderName)) {
folderName = strings.dasherize(folderName);
}

const distRoot = `dist/${folderName}/${options.name}`;

const templateSource = apply(url('./files'), [
applyTemplates({
...strings,
...options,
mainEntryPoint,
secondaryEntryPoint,
relativePathToWorkspaceRoot: relativePathToWorkspaceRoot(libDir),
packageName: options.name,
angularLatestVersion: latestVersions.Angular.replace(/~|\^/, ''),
tsLibLatestVersion: latestVersions['tslib'].replace(/~|\^/, ''),
}),
move(libDir),
]);

return chain([
mergeWith(templateSource),
updateTsConfig(secondaryEntryPoint, './' + distRoot),
]);
};
};
}
164 changes: 164 additions & 0 deletions packages/schematics/angular/secondary-entrypoint/index_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
import { Schema as LibraryOptions } from '../library/schema';
import { Schema as WorkspaceOptions } from '../workspace/schema';
import { Schema as GenerateLibrarySchema } from './schema';
import { parse as parseJson } from 'jsonc-parser';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function getJsonFileContent(tree: UnitTestTree, path: string): any {
return parseJson(tree.readContent(path).toString());
}

describe('Secondary Entrypoint Schematic', () => {
const schematicRunner = new SchematicTestRunner(
'@schematics/ng_packagr',
require.resolve('../collection.json'),
);
const defaultOptions: GenerateLibrarySchema = {
name: 'foo-secondary',
project: 'foo',
};

const workspaceOptions: WorkspaceOptions = {
name: 'workspace',
newProjectRoot: 'projects',

version: '6.0.0',
};
const libaryOptions: LibraryOptions = {
name: 'foo',
entryFile: 'my-index',
standalone: true,
skipPackageJson: false,
skipTsConfig: false,
skipInstall: false,
};

let workspaceTree: UnitTestTree;
beforeEach(async () => {
workspaceTree = await schematicRunner.runSchematic('workspace', workspaceOptions);
});

it('should create correct files', async () => {
workspaceTree = await schematicRunner.runSchematic('library', libaryOptions, workspaceTree);
const tree = await schematicRunner.runSchematic(
'secondary',
{ ...defaultOptions },
workspaceTree,
);
const files = tree.files;

expect(files).toEqual(
jasmine.arrayContaining([
'/projects/foo/src/lib/foo-secondary/README.md',
'/projects/foo/src/lib/foo-secondary/ng-package.json',
'/projects/foo/src/lib/foo-secondary/src/public-api.ts',
]),
);
});

it('should set correct main and secondary entrypoints in the README', async () => {
workspaceTree = await schematicRunner.runSchematic('library', libaryOptions, workspaceTree);
const tree = await schematicRunner.runSchematic(
'secondary',
{ ...defaultOptions },
workspaceTree,
);
const content = tree.readContent('/projects/foo/src/lib/foo-secondary/README.md');
expect(content).toMatch('# foo/foo-secondary');
});

it('should set a custom entryfile', async () => {
workspaceTree = await schematicRunner.runSchematic('library', libaryOptions, workspaceTree);
const tree = await schematicRunner.runSchematic(
'secondary',
{ ...defaultOptions, entryFile: 'my-index' },
workspaceTree,
);
const files = tree.files;
expect(files).toEqual(
jasmine.arrayContaining([
'/projects/foo/src/lib/foo-secondary/README.md',
'/projects/foo/src/lib/foo-secondary/ng-package.json',
'/projects/foo/src/lib/foo-secondary/src/my-index.ts',
]),
);
});

it('should handle scope packages', async () => {
workspaceTree = await schematicRunner.runSchematic(
'library',
{ ...libaryOptions, name: '@scope/package' },
workspaceTree,
);
const tree = await schematicRunner.runSchematic(
'secondary',
{ ...defaultOptions, name: 'testing', project: '@scope/package' },
workspaceTree,
);
const files = tree.files;
expect(files).toEqual(
jasmine.arrayContaining([
'/projects/scope/package/src/lib/testing/README.md',
'/projects/scope/package/src/lib/testing/ng-package.json',
'/projects/scope/package/src/lib/testing/src/public-api.ts',
]),
);
});

it(`should add paths mapping to the tsconfig`, async () => {
workspaceTree = await schematicRunner.runSchematic(
'library',
{ ...libaryOptions, name: '@scope/package' },
workspaceTree,
);
const tree = await schematicRunner.runSchematic(
'secondary',
{ ...defaultOptions, name: 'testing', project: '@scope/package' },
workspaceTree,
);

const tsConfigJson = getJsonFileContent(tree, 'tsconfig.json');
expect(tsConfigJson.compilerOptions.paths['@scope/package/testing']).toEqual([
'./dist/scope/package/testing',
]);
});

it(`should append to existing paths mappings`, async () => {
workspaceTree = await schematicRunner.runSchematic(
'library',
{ ...libaryOptions, name: '@scope/package' },
workspaceTree,
);
workspaceTree.overwrite(
'tsconfig.json',
JSON.stringify({
compilerOptions: {
paths: {
'unrelated': ['./something/else.ts'],
'@scope/package/testing': ['libs/*'],
},
},
}),
);
const tree = await schematicRunner.runSchematic(
'secondary',
{ ...defaultOptions, name: 'testing', project: '@scope/package' },
workspaceTree,
);

const tsConfigJson = getJsonFileContent(tree, 'tsconfig.json');
expect(tsConfigJson.compilerOptions.paths['@scope/package/testing']).toEqual([
'libs/*',
'./dist/scope/package/testing',
]);
});
});
34 changes: 34 additions & 0 deletions packages/schematics/angular/secondary-entrypoint/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "SchematicsLibrary",
"title": "Secondary Entrypoint Schema",
"type": "object",
"description": "Creates a secondary entrypoint in a library project in a project.",
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"description": "The name of the library.",
"pattern": "^(?:@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*/)?[a-zA-Z0-9-~][a-zA-Z0-9-._~]*$",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What name would you like to use for the secondary entrypoint?"
},
"project": {
"type": "string",
"description": "The name of the project.",
"$default": {
"$source": "projectName"
}
},
"entryFile": {
"type": "string",
"format": "path",
"description": "The path at which to create the library's secondary public API file, relative to the workspace root.",
"default": "public-api"
}
},
"required": ["name", "project"]
}

0 comments on commit 6d617b8

Please sign in to comment.