diff --git a/packages/angular/cli/lib/config/workspace-schema.json b/packages/angular/cli/lib/config/workspace-schema.json index 00bd43311e03..a5da28d651f0 100644 --- a/packages/angular/cli/lib/config/workspace-schema.json +++ b/packages/angular/cli/lib/config/workspace-schema.json @@ -147,6 +147,9 @@ "@schematics/angular:directive": { "$ref": "../../../../schematics/angular/directive/schema.json" }, + "@schematics/angular:entry-point": { + "$ref": "../../../../schematics/angular/entry-point/schema.json" + }, "@schematics/angular:enum": { "$ref": "../../../../schematics/angular/enum/schema.json" }, diff --git a/packages/schematics/angular/README.md b/packages/schematics/angular/README.md index 8fa52ff7e608..86c5e2027871 100644 --- a/packages/schematics/angular/README.md +++ b/packages/schematics/angular/README.md @@ -13,6 +13,7 @@ for generating an Angular application. | component | Creates a new, generic component definition in the given project | | directive | Creates a new, generic directive definition in the given project | | enum | Generates a new, generic enum definition in the given project | +| entry-point | Creates a new secondary entry point in an Angular library project | | guard | Generates a new, generic route guard definition in the given project | | interceptor | Creates a new, generic interceptor definition in the given project | | interface | Creates a new, generic interface definition in the given project | diff --git a/packages/schematics/angular/collection.json b/packages/schematics/angular/collection.json index 29b361ccafbb..3302c707aa74 100755 --- a/packages/schematics/angular/collection.json +++ b/packages/schematics/angular/collection.json @@ -111,6 +111,11 @@ "schema": "./library/schema.json", "description": "Generate a library project for Angular." }, + "entry-point": { + "factory": "./entry-point", + "schema": "./entry-point/schema.json", + "description": "Generate a secondary entry point for an Angular library." + }, "web-worker": { "factory": "./web-worker", "schema": "./web-worker/schema.json", diff --git a/packages/schematics/angular/entry-point/files/__entryFile__.ts.template b/packages/schematics/angular/entry-point/files/__entryFile__.ts.template new file mode 100644 index 000000000000..62eb3bb8cadb --- /dev/null +++ b/packages/schematics/angular/entry-point/files/__entryFile__.ts.template @@ -0,0 +1,6 @@ +/* + * Public API Surface of <%= dasherize(name) %> + */ + +export * from './<%= isPrimary ? 'lib/' : '' %><%= dasherize(name) %>';<% if (!standalone) { %> +export * from './<%= isPrimary ? 'lib/' : '' %><%= dasherize(name) %>-module';<% } %> diff --git a/packages/schematics/angular/entry-point/index.ts b/packages/schematics/angular/entry-point/index.ts new file mode 100644 index 000000000000..c889f3465eb4 --- /dev/null +++ b/packages/schematics/angular/entry-point/index.ts @@ -0,0 +1,177 @@ +/** + * @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 { type JsonObject, type JsonValue, isJsonObject } from '@angular-devkit/core'; +import { + type Rule, + SchematicsException, + type Tree, + apply, + applyTemplates, + chain, + mergeWith, + move, + noop, + schematic, + strings, + url, +} from '@angular-devkit/schematics'; +import { basename, join, relative } from 'node:path/posix'; +import { JSONFile } from '../utility/json-file'; +import { createProjectSchematic } from '../utility/project'; +import { Builders, ProjectType } from '../utility/workspace-models'; +import type { Schema as EntryPointOptions } from './schema'; + +function updateTsConfig(packageName: string, ...paths: string[]): Rule { + 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) ? Array.from(new Set([...value, ...paths])) : paths); + }; +} + +export default createProjectSchematic((options, { project, tree }) => { + if (project.extensions['projectType'] !== ProjectType.Library) { + throw new SchematicsException(`Project "${options.project}" is not a library project.`); + } + + const buildTarget = project.targets.get('build'); + if (buildTarget?.builder !== Builders.BuildLibrary) { + throw new SchematicsException( + `Project "${options.project}" does not use the "${Builders.BuildLibrary}" builder.`, + ); + } + + if (options.name === 'package.json') { + throw new SchematicsException('Entry point name "package.json" is reserved.'); + } + + const libDir = project.root; + const packageJsonPath = join(libDir, 'package.json'); + if (!tree.exists(packageJsonPath)) { + throw new SchematicsException(`Could not find package.json at "${packageJsonPath}".`); + } + + const packageJson = new JSONFile(tree, packageJsonPath); + const rawPackageName = packageJson.get(['name']); + const packageName = + typeof rawPackageName === 'string' && rawPackageName + ? rawPackageName + : strings.dasherize(options.project); + + const isPrimary = options.name === '.'; + const entryFile = (options.entryFile ?? 'public-api').replace(/\.ts$/i, ''); + const entryPointDir = join(libDir, options.name, 'src'); + const exportKey = isPrimary ? '.' : `./${options.name}`; + const exportTarget = `./${relative(libDir, join(entryPointDir, `${entryFile}.ts`))}`; + + const existingExports = (packageJson.get(['exports']) as JsonValue | undefined) ?? null; + let exportsObject: JsonObject = {}; + if (typeof existingExports === 'string' || Array.isArray(existingExports)) { + exportsObject = { '.': existingExports }; + } else if (isJsonObject(existingExports)) { + const keys = Object.keys(existingExports); + if (keys.some((k) => k.startsWith('.'))) { + exportsObject = { ...existingExports }; + } else if (keys.length > 0) { + exportsObject = { '.': existingExports }; + } + } + + if (Object.prototype.hasOwnProperty.call(exportsObject, exportKey)) { + throw new SchematicsException( + `Entry point "${exportKey}" already exists in "${packageJsonPath}".`, + ); + } + + if ( + isJsonObject(existingExports) && + Object.keys(existingExports).some((k) => k.startsWith('.')) + ) { + packageJson.modify(['exports', exportKey], exportTarget, false); + } else { + packageJson.modify( + ['exports'], + { + ...exportsObject, + [exportKey]: exportTarget, + }, + false, + ); + } + + const prefix = (options.prefix ?? project.prefix) || 'lib'; + const sourceDir = isPrimary ? join(entryPointDir, 'lib') : entryPointDir; + const artifactName = isPrimary + ? /^@.*\/.*/.test(options.project) + ? options.project.split('/')[1] + : options.project + : basename(options.name); + + const buildOptions = buildTarget.options; + const distRoot = + typeof buildOptions?.['outputPath'] === 'string' + ? buildOptions['outputPath'] + : join('dist', options.project); + + const tsConfigPathKey = isPrimary ? packageName : `${packageName}/*`; + const tsConfigPathTarget = `./${join(distRoot, isPrimary ? '.' : '*')}`; + + const standalone = options.standalone ?? true; + + const templateSource = apply(url('../entry-point/files'), [ + applyTemplates({ + ...strings, + ...options, + isPrimary, + standalone, + name: artifactName, + entryFile, + }), + move(entryPointDir), + ]); + + return chain([ + mergeWith(templateSource), + options.skipTsConfig ? noop() : updateTsConfig(tsConfigPathKey, tsConfigPathTarget), + standalone + ? noop() + : schematic('module', { + name: artifactName, + commonModule: false, + flat: true, + path: sourceDir, + project: options.project, + // Explicitly set the `typeSeparator` this also ensures that the generated files are valid even if the `module` schematic + // inherits its `typeSeparator` from the workspace. + typeSeparator: '-', + }), + schematic('component', { + name: artifactName, + selector: `${prefix}-${strings.dasherize(artifactName)}`, + inlineStyle: true, + inlineTemplate: true, + flat: true, + path: sourceDir, + export: true, + standalone, + skipTests: options.skipTests, + project: options.project, + // Explicitly set an empty `type` and `exportDefault: false` since they don't necessarily make + // sense in a library and ensure that the generated files match the entry-point exports even + // if the `component` schematic inherits options from the workspace. + type: '', + exportDefault: false, + }), + ]); +}); diff --git a/packages/schematics/angular/entry-point/index_spec.ts b/packages/schematics/angular/entry-point/index_spec.ts new file mode 100644 index 000000000000..ff047c5ef3d7 --- /dev/null +++ b/packages/schematics/angular/entry-point/index_spec.ts @@ -0,0 +1,458 @@ +/** + * @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, type UnitTestTree } from '@angular-devkit/schematics/testing'; +import type { Schema as ApplicationOptions } from '../application/schema'; +import type { Schema as GenerateLibrarySchema } from '../library/schema'; +import type { Schema as WorkspaceOptions } from '../workspace/schema'; +import type { Schema as GenerateEntryPointSchema } from './schema'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function getJsonFileContent(tree: UnitTestTree, path: string): any { + return tree.readJson(path); +} + +describe('EntryPoint Schematic', () => { + const schematicRunner = new SchematicTestRunner( + '@schematics/angular', + require.resolve('../collection.json'), + ); + + const workspaceOptions: WorkspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '6.0.0', + }; + + const defaultLibraryOptions: GenerateLibrarySchema = { + name: 'foo', + skipPackageJson: false, + skipTsConfig: false, + skipInstall: false, + }; + + const defaultOptions: GenerateEntryPointSchema = { + name: 'secondary', + project: 'foo', + }; + + let workspaceTree: UnitTestTree; + beforeEach(async () => { + workspaceTree = await schematicRunner.runSchematic('workspace', workspaceOptions); + workspaceTree = await schematicRunner.runSchematic( + 'library', + defaultLibraryOptions, + workspaceTree, + ); + }); + + it('should create secondary entry-point files', async () => { + const tree = await schematicRunner.runSchematic('entry-point', defaultOptions, workspaceTree); + + expect(tree.files).toEqual( + jasmine.arrayContaining([ + '/projects/foo/secondary/src/public-api.ts', + '/projects/foo/secondary/src/secondary.ts', + '/projects/foo/secondary/src/secondary.spec.ts', + ]), + ); + + const publicApiContent = tree.readContent('/projects/foo/secondary/src/public-api.ts'); + expect(publicApiContent).toContain("export * from './secondary';"); + expect(publicApiContent).not.toContain('secondary-module'); + }); + + it('should support nested secondary entry-point names', async () => { + const tree = await schematicRunner.runSchematic( + 'entry-point', + { + ...defaultOptions, + name: 'feature/sub', + }, + workspaceTree, + ); + + expect(tree.files).toEqual( + jasmine.arrayContaining([ + '/projects/foo/feature/sub/src/public-api.ts', + '/projects/foo/feature/sub/src/sub.ts', + '/projects/foo/feature/sub/src/sub.spec.ts', + ]), + ); + + const pkgJson = getJsonFileContent(tree, '/projects/foo/package.json'); + expect(pkgJson.exports['./feature/sub']).toBe('./feature/sub/src/public-api.ts'); + + const tsConfigJson = getJsonFileContent(tree, '/tsconfig.json'); + expect(tsConfigJson.compilerOptions.paths['foo/*']).toEqual(['./dist/foo/*']); + }); + + it('should support custom entryFile option', async () => { + const tree = await schematicRunner.runSchematic( + 'entry-point', + { + ...defaultOptions, + entryFile: 'my-entry', + }, + workspaceTree, + ); + + expect(tree.files).toContain('/projects/foo/secondary/src/my-entry.ts'); + const pkgJson = getJsonFileContent(tree, '/projects/foo/package.json'); + expect(pkgJson.exports['./secondary']).toBe('./secondary/src/my-entry.ts'); + }); + + it('should strip .ts suffix from entryFile option', async () => { + const tree = await schematicRunner.runSchematic( + 'entry-point', + { + ...defaultOptions, + entryFile: 'my-entry.ts', + }, + workspaceTree, + ); + + expect(tree.files).toContain('/projects/foo/secondary/src/my-entry.ts'); + expect(tree.files).not.toContain('/projects/foo/secondary/src/my-entry.ts.ts'); + const pkgJson = getJsonFileContent(tree, '/projects/foo/package.json'); + expect(pkgJson.exports['./secondary']).toBe('./secondary/src/my-entry.ts'); + }); + + it('should update library package.json exports', async () => { + const tree = await schematicRunner.runSchematic('entry-point', defaultOptions, workspaceTree); + + const pkgJson = getJsonFileContent(tree, '/projects/foo/package.json'); + expect(pkgJson.exports).toEqual({ + '.': './src/public-api.ts', + './secondary': './secondary/src/public-api.ts', + }); + expect(Object.keys(pkgJson)[0]).toBe('name'); + }); + + it('should convert string exports in library package.json to an exports object', async () => { + const pkgJson = getJsonFileContent(workspaceTree, '/projects/foo/package.json'); + pkgJson.exports = './src/public-api.ts'; + workspaceTree.overwrite('/projects/foo/package.json', JSON.stringify(pkgJson, null, 2)); + + const tree = await schematicRunner.runSchematic('entry-point', defaultOptions, workspaceTree); + + const updatedPkgJson = getJsonFileContent(tree, '/projects/foo/package.json'); + expect(updatedPkgJson.exports).toEqual({ + '.': './src/public-api.ts', + './secondary': './secondary/src/public-api.ts', + }); + }); + + it('should convert array exports in library package.json to an exports object', async () => { + const pkgJson = getJsonFileContent(workspaceTree, '/projects/foo/package.json'); + pkgJson.exports = ['./src/public-api.ts']; + workspaceTree.overwrite('/projects/foo/package.json', JSON.stringify(pkgJson, null, 2)); + + const tree = await schematicRunner.runSchematic('entry-point', defaultOptions, workspaceTree); + + const updatedPkgJson = getJsonFileContent(tree, '/projects/foo/package.json'); + expect(updatedPkgJson.exports).toEqual({ + '.': ['./src/public-api.ts'], + './secondary': './secondary/src/public-api.ts', + }); + }); + + it('should convert conditional root exports object in library package.json to an exports object', async () => { + const pkgJson = getJsonFileContent(workspaceTree, '/projects/foo/package.json'); + pkgJson.exports = { default: './src/public-api.ts' }; + workspaceTree.overwrite('/projects/foo/package.json', JSON.stringify(pkgJson, null, 2)); + + const tree = await schematicRunner.runSchematic('entry-point', defaultOptions, workspaceTree); + + const updatedPkgJson = getJsonFileContent(tree, '/projects/foo/package.json'); + expect(updatedPkgJson.exports).toEqual({ + '.': { default: './src/public-api.ts' }, + './secondary': './secondary/src/public-api.ts', + }); + }); + + it('should add wildcard paths mapping to root tsconfig.json', async () => { + const tree = await schematicRunner.runSchematic('entry-point', defaultOptions, workspaceTree); + + const tsConfigJson = getJsonFileContent(tree, '/tsconfig.json'); + expect(tsConfigJson.compilerOptions.paths['foo']).toEqual(['./dist/foo']); + expect(tsConfigJson.compilerOptions.paths['foo/*']).toEqual(['./dist/foo/*']); + }); + + it('should not duplicate existing paths mapping in root tsconfig.json', async () => { + const tsConfig = getJsonFileContent(workspaceTree, '/tsconfig.json'); + tsConfig.compilerOptions.paths['foo/*'] = ['./dist/foo/*']; + workspaceTree.overwrite('/tsconfig.json', JSON.stringify(tsConfig, null, 2)); + + const tree = await schematicRunner.runSchematic('entry-point', defaultOptions, workspaceTree); + + const tsConfigJson = getJsonFileContent(tree, '/tsconfig.json'); + expect(tsConfigJson.compilerOptions.paths['foo/*']).toEqual(['./dist/foo/*']); + }); + + it('should respect custom outputPath in angular.json for tsconfig paths mapping', async () => { + const angularJson = getJsonFileContent(workspaceTree, '/angular.json'); + angularJson.projects.foo.architect.build.options.outputPath = 'custom-dist/foo'; + workspaceTree.overwrite('/angular.json', JSON.stringify(angularJson, null, 2)); + + const tree = await schematicRunner.runSchematic('entry-point', defaultOptions, workspaceTree); + + const tsConfigJson = getJsonFileContent(tree, '/tsconfig.json'); + expect(tsConfigJson.compilerOptions.paths['foo/*']).toEqual(['./custom-dist/foo/*']); + }); + + it('should not modify root tsconfig.json when skipTsConfig is true', async () => { + const tree = await schematicRunner.runSchematic( + 'entry-point', + { + ...defaultOptions, + skipTsConfig: true, + }, + workspaceTree, + ); + + const tsConfigJson = getJsonFileContent(tree, '/tsconfig.json'); + expect(tsConfigJson.compilerOptions.paths['foo/*']).toBeUndefined(); + }); + + it('should inherit prefix from the library project in angular.json', async () => { + let tree = await schematicRunner.runSchematic( + 'library', + { + ...defaultLibraryOptions, + name: 'prefixed-lib', + prefix: 'custom', + }, + workspaceTree, + ); + + tree = await schematicRunner.runSchematic( + 'entry-point', + { + name: 'secondary', + project: 'prefixed-lib', + }, + tree, + ); + + const componentContent = tree.readContent('/projects/prefixed-lib/secondary/src/secondary.ts'); + expect(componentContent).toContain("selector: 'custom-secondary'"); + }); + + it('should fallback to "lib" prefix when project prefix is empty in angular.json', async () => { + const angularJson = getJsonFileContent(workspaceTree, '/angular.json'); + angularJson.projects.foo.prefix = ''; + workspaceTree.overwrite('/angular.json', JSON.stringify(angularJson, null, 2)); + + const tree = await schematicRunner.runSchematic('entry-point', defaultOptions, workspaceTree); + + const componentContent = tree.readContent('/projects/foo/secondary/src/secondary.ts'); + expect(componentContent).toContain("selector: 'lib-secondary'"); + }); + + it('should allow overriding prefix via option', async () => { + const tree = await schematicRunner.runSchematic( + 'entry-point', + { + ...defaultOptions, + prefix: 'override', + }, + workspaceTree, + ); + + const componentContent = tree.readContent('/projects/foo/secondary/src/secondary.ts'); + expect(componentContent).toContain("selector: 'override-secondary'"); + }); + + it('should support scoped libraries', async () => { + let tree = await schematicRunner.runSchematic( + 'library', + { + ...defaultLibraryOptions, + name: '@myScope/myLib', + }, + workspaceTree, + ); + + tree = await schematicRunner.runSchematic( + 'entry-point', + { + name: 'secondary', + project: '@myScope/myLib', + }, + tree, + ); + + expect(tree.files).toEqual( + jasmine.arrayContaining([ + '/projects/my-scope/my-lib/secondary/src/public-api.ts', + '/projects/my-scope/my-lib/secondary/src/secondary.ts', + '/projects/my-scope/my-lib/secondary/src/secondary.spec.ts', + ]), + ); + + const pkgJson = getJsonFileContent(tree, '/projects/my-scope/my-lib/package.json'); + expect(pkgJson.exports).toEqual({ + '.': './src/public-api.ts', + './secondary': './secondary/src/public-api.ts', + }); + + const tsConfigJson = getJsonFileContent(tree, '/tsconfig.json'); + expect(tsConfigJson.compilerOptions.paths['@my-scope/my-lib/*']).toEqual([ + './dist/my-scope/my-lib/*', + ]); + }); + + it('should respect skipTests option', async () => { + const tree = await schematicRunner.runSchematic( + 'entry-point', + { + ...defaultOptions, + skipTests: true, + }, + workspaceTree, + ); + + expect(tree.files).toContain('/projects/foo/secondary/src/secondary.ts'); + expect(tree.files).not.toContain('/projects/foo/secondary/src/secondary.spec.ts'); + }); + + it('should not be affected by workspace component schematic type and exportDefault defaults', async () => { + const angularJson = getJsonFileContent(workspaceTree, '/angular.json'); + angularJson.schematics = { + '@schematics/angular:component': { + type: 'component', + exportDefault: true, + }, + }; + workspaceTree.overwrite('/angular.json', JSON.stringify(angularJson, null, 2)); + + const tree = await schematicRunner.runSchematic('entry-point', defaultOptions, workspaceTree); + + expect(tree.files).toContain('/projects/foo/secondary/src/secondary.ts'); + const componentContent = tree.readContent('/projects/foo/secondary/src/secondary.ts'); + expect(componentContent).toContain('export class Secondary'); + expect(componentContent).not.toContain('export default'); + }); + + describe('standalone=false', () => { + it('should create an NgModule and export it in the entry file', async () => { + const tree = await schematicRunner.runSchematic( + 'entry-point', + { + ...defaultOptions, + standalone: false, + }, + workspaceTree, + ); + + expect(tree.files).toEqual( + jasmine.arrayContaining([ + '/projects/foo/secondary/src/public-api.ts', + '/projects/foo/secondary/src/secondary-module.ts', + '/projects/foo/secondary/src/secondary.ts', + '/projects/foo/secondary/src/secondary.spec.ts', + ]), + ); + + const publicApiContent = tree.readContent('/projects/foo/secondary/src/public-api.ts'); + expect(publicApiContent).toContain("export * from './secondary';"); + expect(publicApiContent).toContain("export * from './secondary-module';"); + }); + }); + + describe('error handling', () => { + it('should throw if the project does not exist', async () => { + await expectAsync( + schematicRunner.runSchematic( + 'entry-point', + { + name: 'secondary', + project: 'non-existent', + }, + workspaceTree, + ), + ).toBeRejectedWithError('Project "non-existent" does not exist.'); + }); + + it('should throw if the project is not a library project', async () => { + const appOptions: ApplicationOptions = { + name: 'my-app', + skipInstall: true, + }; + const tree = await schematicRunner.runSchematic('application', appOptions, workspaceTree); + + await expectAsync( + schematicRunner.runSchematic( + 'entry-point', + { + name: 'secondary', + project: 'my-app', + }, + tree, + ), + ).toBeRejectedWithError('Project "my-app" is not a library project.'); + }); + + it('should throw if the library project does not use @angular/build:library', async () => { + const angularJson = getJsonFileContent(workspaceTree, '/angular.json'); + angularJson.projects.foo.architect.build.builder = '@angular/build:ng-packagr'; + workspaceTree.overwrite('/angular.json', JSON.stringify(angularJson, null, 2)); + + await expectAsync( + schematicRunner.runSchematic('entry-point', defaultOptions, workspaceTree), + ).toBeRejectedWithError('Project "foo" does not use the "@angular/build:library" builder.'); + }); + + it('should throw if the library package.json is missing', async () => { + workspaceTree.delete('/projects/foo/package.json'); + + await expectAsync( + schematicRunner.runSchematic('entry-point', defaultOptions, workspaceTree), + ).toBeRejectedWithError('Could not find package.json at "projects/foo/package.json".'); + }); + + it('should throw if the entry-point name is "package.json"', async () => { + await expectAsync( + schematicRunner.runSchematic( + 'entry-point', + { + name: 'package.json', + project: 'foo', + }, + workspaceTree, + ), + ).toBeRejectedWithError('Entry point name "package.json" is reserved.'); + }); + + it('should reject invalid entry-point names via schema validation', async () => { + for (const invalidName of ['.', '../foo', 'foo..bar', '/secondary', 'secondary/', './sub']) { + await expectAsync( + schematicRunner.runSchematic( + 'entry-point', + { + name: invalidName, + project: 'foo', + }, + workspaceTree, + ), + ).toBeRejected(); + } + }); + + it('should throw if the entry point already exists in package.json exports', async () => { + const tree = await schematicRunner.runSchematic('entry-point', defaultOptions, workspaceTree); + + await expectAsync( + schematicRunner.runSchematic('entry-point', defaultOptions, tree), + ).toBeRejectedWithError( + 'Entry point "./secondary" already exists in "projects/foo/package.json".', + ); + }); + }); +}); diff --git a/packages/schematics/angular/entry-point/schema.json b/packages/schematics/angular/entry-point/schema.json new file mode 100644 index 000000000000..743ffb795672 --- /dev/null +++ b/packages/schematics/angular/entry-point/schema.json @@ -0,0 +1,56 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "SchematicsAngularEntryPoint", + "title": "Angular Entry Point Options Schema", + "type": "object", + "description": "Creates a new secondary entry point in an Angular library project.", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "The name of the secondary entry point (for example, 'testing' or 'feature/sub').", + "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 entry point?" + }, + "project": { + "type": "string", + "description": "The name of the library project where the secondary entry point should be added. If not specified, the CLI will determine the project from the current directory.", + "$default": { + "$source": "projectName" + } + }, + "entryFile": { + "type": "string", + "format": "path", + "description": "The file name at which to create the secondary entry point's public API file, without the `.ts` extension.", + "default": "public-api" + }, + "prefix": { + "type": "string", + "format": "html-selector", + "description": "A prefix to apply to generated selectors.", + "alias": "p" + }, + "standalone": { + "description": "Creates a secondary entry point based upon the standalone API, without NgModules.", + "type": "boolean", + "default": true, + "x-user-analytics": "ep.ng_standalone" + }, + "skipTests": { + "type": "boolean", + "default": false, + "description": "Do not create `spec.ts` test files for the new secondary entry point." + }, + "skipTsConfig": { + "type": "boolean", + "default": false, + "description": "Do not update `tsconfig.json` to add a path mapping for the new secondary entry point." + } + }, + "required": ["name", "project"] +} diff --git a/packages/schematics/angular/library/files/ng-package.json.template b/packages/schematics/angular/library/files/ng-package.json.template deleted file mode 100644 index c0f915caf7b2..000000000000 --- a/packages/schematics/angular/library/files/ng-package.json.template +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "<%= relativePathToWorkspaceRoot %>/node_modules/ng-packagr/ng-package.schema.json", - "dest": "<%= relativePathToWorkspaceRoot %>/<%= distRoot %>", - "lib": { - "entryFile": "src/<%= entryFile %>.ts" - } -} \ No newline at end of file diff --git a/packages/schematics/angular/library/files/package.json.template b/packages/schematics/angular/library/files/package.json.template index 1d50c8cbcc34..992af68cfa44 100644 --- a/packages/schematics/angular/library/files/package.json.template +++ b/packages/schematics/angular/library/files/package.json.template @@ -5,8 +5,5 @@ "@angular/common": "^<%= angularLatestVersion %>", "@angular/core": "^<%= angularLatestVersion %>" }, - "dependencies": { - "tslib": "^<%= tsLibLatestVersion %>" - }, "sideEffects": false } diff --git a/packages/schematics/angular/library/files/src/__entryFile__.ts.template b/packages/schematics/angular/library/files/src/__entryFile__.ts.template deleted file mode 100644 index 2d13ba01016f..000000000000 --- a/packages/schematics/angular/library/files/src/__entryFile__.ts.template +++ /dev/null @@ -1,6 +0,0 @@ -/* - * Public API Surface of <%= dasherize(name) %> - */ - -export * from './lib/<%= dasherize(name) %>';<% if (!standalone) { %> -export * from './lib/<%= dasherize(name) %>-module';<% } %> diff --git a/packages/schematics/angular/library/files/tsconfig.lib.json.template b/packages/schematics/angular/library/files/tsconfig.lib.json.template index 779ab5549814..6f429ec7d056 100644 --- a/packages/schematics/angular/library/files/tsconfig.lib.json.template +++ b/packages/schematics/angular/library/files/tsconfig.lib.json.template @@ -3,13 +3,11 @@ { "extends": "<%= relativePathToWorkspaceRoot %>/tsconfig.json", "compilerOptions": { - "outDir": "<%= relativePathToWorkspaceRoot %>/out-tsc/lib", "declaration": true, - "declarationMap": true, "types": [] }, "include": [ - "src/**/*.ts" + "**/*.ts" ], "exclude": [ "**/*.spec.ts" diff --git a/packages/schematics/angular/library/files/tsconfig.lib.prod.json.template b/packages/schematics/angular/library/files/tsconfig.lib.prod.json.template deleted file mode 100644 index 9215caac46fb..000000000000 --- a/packages/schematics/angular/library/files/tsconfig.lib.prod.json.template +++ /dev/null @@ -1,11 +0,0 @@ -/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ -/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ -{ - "extends": "./tsconfig.lib.json", - "compilerOptions": { - "declarationMap": false - }, - "angularCompilerOptions": { - "compilationMode": "partial" - } -} diff --git a/packages/schematics/angular/library/files/tsconfig.spec.json.template b/packages/schematics/angular/library/files/tsconfig.spec.json.template index b2370befce4f..4f8ee2c037bb 100644 --- a/packages/schematics/angular/library/files/tsconfig.spec.json.template +++ b/packages/schematics/angular/library/files/tsconfig.spec.json.template @@ -9,7 +9,7 @@ ] }, "include": [ - "src/**/*.d.ts", - "src/**/*<% if (standalone) { %>.spec<% } %>.ts" + "**/*.d.ts", + "**/*<% if (standalone) { %>.spec<% } %>.ts" ] } diff --git a/packages/schematics/angular/library/index.ts b/packages/schematics/angular/library/index.ts index 3069664c02d7..5a832948f1bf 100644 --- a/packages/schematics/angular/library/index.ts +++ b/packages/schematics/angular/library/index.ts @@ -7,19 +7,19 @@ */ import { - Rule, - Tree, + type Rule, + type Tree, apply, applyTemplates, chain, mergeWith, move, noop, - schematic, strings, url, } from '@angular-devkit/schematics'; import { join } from 'node:path/posix'; +import addEntryPoint from '../entry-point'; import { addTestRunnerDependencies } from '../utility/dependencies'; import { DependencyType, @@ -33,28 +33,14 @@ import { latestVersions } from '../utility/latest-versions'; import { relativePathToWorkspaceRoot } from '../utility/paths'; import { getWorkspace, updateWorkspace } from '../utility/workspace'; import { Builders, ProjectType } from '../utility/workspace-models'; -import { Schema as LibraryOptions, TestRunner } from './schema'; +import { type Schema as LibraryOptions, TestRunner } from './schema'; const LIBRARY_DEV_DEPENDENCIES = [ { name: '@angular/compiler-cli', version: latestVersions.Angular }, { name: '@angular/build', version: latestVersions.AngularBuild }, - { name: 'ng-packagr', version: latestVersions.NgPackagr }, { name: 'typescript', version: latestVersions['typescript'] }, ]; -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); - }; -} - function addTsProjectReference(...paths: string[]) { return (host: Tree) => { if (!host.exists('tsconfig.json')) { @@ -92,25 +78,30 @@ function addLibToWorkspaceFile( options: LibraryOptions, projectRoot: string, projectName: string, + folderName: string, + distRoot: string, hasZoneDependency: boolean, ): Rule { return updateWorkspace((workspace) => { workspace.projects.add({ name: projectName, root: projectRoot, - sourceRoot: `${projectRoot}/src`, + sourceRoot: projectRoot, projectType: ProjectType.Library, prefix: options.prefix, targets: { build: { - builder: Builders.BuildNgPackagr, + builder: Builders.BuildLibrary, defaultConfiguration: 'production', + options: { + tsConfig: `${projectRoot}/tsconfig.lib.json`, + ...(folderName !== projectName ? { outputPath: distRoot } : {}), + }, configurations: { - production: { - tsConfig: `${projectRoot}/tsconfig.lib.prod.json`, - }, + production: {}, development: { - tsConfig: `${projectRoot}/tsconfig.lib.json`, + compilationMode: 'full', + declarationMap: true, }, }, }, @@ -137,13 +128,7 @@ function addLibToWorkspaceFile( export default function (options: LibraryOptions): Rule { return async (host: Tree) => { const prefix = options.prefix; - - // If scoped project (i.e. "@foo/bar"), convert projectDir to "foo/bar". const packageName = options.name; - if (/^@.*\/.*/.test(options.name)) { - const [, name] = options.name.split('/'); - options.name = name; - } const workspace = await getWorkspace(host); const newProjectRoot = (workspace.extensions.newProjectRoot as string | undefined) || ''; @@ -159,20 +144,18 @@ export default function (options: LibraryOptions): Rule { : join(newProjectRoot, folderName); const distRoot = `dist/${folderName}`; - const sourceDir = `${libDir}/src/lib`; + + // If scoped project (i.e. "@foo/bar"), convert name to "bar" for templates. + const name = /^@.*\/.*/.test(options.name) ? options.name.split('/')[1] : options.name; const templateSource = apply(url('./files'), [ applyTemplates({ ...strings, ...options, + name, packageName, - libDir, - distRoot, relativePathToWorkspaceRoot: relativePathToWorkspaceRoot(libDir), - prefix, angularLatestVersion: latestVersions.Angular.replace(/~|\^/, ''), - tsLibLatestVersion: latestVersions['tslib'].replace(/~|\^/, ''), - folderName, testTypesPackage: options.testRunner === TestRunner.Vitest ? 'vitest/globals' : 'jasmine', }), move(libDir), @@ -182,41 +165,21 @@ export default function (options: LibraryOptions): Rule { return chain([ mergeWith(templateSource), - addLibToWorkspaceFile(options, libDir, packageName, hasZoneDependency), + addLibToWorkspaceFile(options, libDir, packageName, folderName, distRoot, hasZoneDependency), options.skipPackageJson ? noop() : addDependenciesToPackageJson(options), - options.skipTsConfig ? noop() : updateTsConfig(packageName, './' + distRoot), options.skipTsConfig ? noop() : addTsProjectReference( './' + join(libDir, 'tsconfig.lib.json'), './' + join(libDir, 'tsconfig.spec.json'), ), - options.standalone - ? noop() - : schematic('module', { - name: options.name, - commonModule: false, - flat: true, - path: sourceDir, - project: packageName, - // Explicitly set the `typeSeparator` this also ensures that the generated files are valid even if the `module` schematic - // inherits its `typeSeparator` from the workspace. - typeSeparator: '-', - }), - schematic('component', { - name: options.name, - selector: `${prefix}-${options.name}`, - inlineStyle: true, - inlineTemplate: true, - flat: true, - path: sourceDir, - export: true, - standalone: options.standalone, + addEntryPoint({ + name: '.', project: packageName, - // Explicitly set an empty `type` since it doesn't necessarily make sense in a library. - // This also ensures that the generated files are valid even if the `component` schematic - // inherits its `type` from the workspace. - type: '', + entryFile: options.entryFile, + prefix, + standalone: options.standalone, + skipTsConfig: options.skipTsConfig, }), ]); }; diff --git a/packages/schematics/angular/library/index_spec.ts b/packages/schematics/angular/library/index_spec.ts index 9d76bb913d17..a1836bedd78b 100644 --- a/packages/schematics/angular/library/index_spec.ts +++ b/packages/schematics/angular/library/index_spec.ts @@ -6,12 +6,11 @@ * found in the LICENSE file at https://angular.dev/license */ -import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; -import { parse as parseJson } from 'jsonc-parser'; -import { Schema as ComponentOptions } from '../component/schema'; +import { SchematicTestRunner, type UnitTestTree } from '@angular-devkit/schematics/testing'; +import type { Schema as ComponentOptions } from '../component/schema'; import { latestVersions } from '../utility/latest-versions'; -import { Schema as WorkspaceOptions } from '../workspace/schema'; -import { Schema as GenerateLibrarySchema } from './schema'; +import type { Schema as WorkspaceOptions } from '../workspace/schema'; +import type { Schema as GenerateLibrarySchema } from './schema'; // eslint-disable-next-line @typescript-eslint/no-explicit-any function getJsonFileContent(tree: UnitTestTree, path: string): any { @@ -20,7 +19,7 @@ function getJsonFileContent(tree: UnitTestTree, path: string): any { describe('Library Schematic', () => { const schematicRunner = new SchematicTestRunner( - '@schematics/ng_packagr', + '@schematics/angular', require.resolve('../collection.json'), ); const defaultOptions: GenerateLibrarySchema = { @@ -48,16 +47,16 @@ describe('Library Schematic', () => { const files = tree.files; expect(files).toEqual( jasmine.arrayContaining([ - '/projects/foo/ng-package.json', '/projects/foo/package.json', '/projects/foo/README.md', '/projects/foo/tsconfig.lib.json', - '/projects/foo/tsconfig.lib.prod.json', '/projects/foo/src/my-index.ts', '/projects/foo/src/lib/foo.spec.ts', '/projects/foo/src/lib/foo.ts', ]), ); + expect(files).not.toContain('/projects/foo/ng-package.json'); + expect(files).not.toContain('/projects/foo/tsconfig.lib.prod.json'); }); it('should not add reference to module file in entry-file', async () => { @@ -91,16 +90,16 @@ describe('Library Schematic', () => { const files = tree.files; expect(files).toEqual( jasmine.arrayContaining([ - '/some/other/directory/bar/ng-package.json', '/some/other/directory/bar/package.json', '/some/other/directory/bar/README.md', '/some/other/directory/bar/tsconfig.lib.json', - '/some/other/directory/bar/tsconfig.lib.prod.json', '/some/other/directory/bar/src/my-index.ts', '/some/other/directory/bar/src/lib/foo.spec.ts', '/some/other/directory/bar/src/lib/foo.ts', ]), ); + expect(files).not.toContain('/some/other/directory/bar/ng-package.json'); + expect(files).not.toContain('/some/other/directory/bar/tsconfig.lib.prod.json'); }); it(`should add library to workspace`, async () => { @@ -149,15 +148,20 @@ describe('Library Schematic', () => { const fileContent = getJsonFileContent(tree, '/projects/foo/tsconfig.lib.json'); expect(fileContent).toBeDefined(); + expect(fileContent.compilerOptions.declaration).toBe(true); + expect(fileContent.compilerOptions.declarationMap).toBeUndefined(); + expect(fileContent.include).toEqual(['**/*.ts']); + expect(fileContent.exclude).toEqual(['**/*.spec.ts']); }); - it('should create a ng-package.json with ngPackage conf', async () => { + it('should configure exports and not include tslib in library package.json', async () => { const tree = await schematicRunner.runSchematic('library', defaultOptions, workspaceTree); - const fileContent = getJsonFileContent(tree, '/projects/foo/ng-package.json'); - expect(fileContent.lib).toBeDefined(); - expect(fileContent.lib.entryFile).toEqual('src/my-index.ts'); - expect(fileContent.dest).toEqual('../../dist/foo'); + const fileContent = getJsonFileContent(tree, '/projects/foo/package.json'); + expect(fileContent.exports).toEqual({ + '.': './src/my-index.ts', + }); + expect(fileContent.dependencies).toBeUndefined(); }); it('should use default value for baseDir and entryFile', async () => { @@ -170,6 +174,10 @@ describe('Library Schematic', () => { ); expect(tree.files).toContain('/projects/foobar/src/public-api.ts'); + const fileContent = getJsonFileContent(tree, '/projects/foobar/package.json'); + expect(fileContent.exports).toEqual({ + '.': './src/public-api.ts', + }); }); it(`should add library to workspace`, async () => { @@ -209,16 +217,18 @@ describe('Library Schematic', () => { const project = config.projects.pascalCasedName; expect(project).toBeDefined(); expect(project.root).toEqual('projects/pascal-cased-name'); + expect(project.architect.build.options.outputPath).toEqual('dist/pascal-cased-name'); const svcContent = tree.readContent('/projects/pascal-cased-name/src/lib/pascal-cased-name.ts'); expect(svcContent).toContain('@Component'); }); describe(`update package.json`, () => { - it(`should add ng-packagr to devDependencies`, async () => { + it(`should add @angular/build and not ng-packagr to devDependencies`, async () => { const tree = await schematicRunner.runSchematic('library', defaultOptions, workspaceTree); const packageJson = getJsonFileContent(tree, 'package.json'); - expect(packageJson.devDependencies['ng-packagr']).toBeDefined(); + expect(packageJson.devDependencies['@angular/build']).toBe(latestVersions.AngularBuild); + expect(packageJson.devDependencies['ng-packagr']).toBeUndefined(); }); it('should use the latest known versions in package.json', async () => { @@ -256,7 +266,7 @@ describe('Library Schematic', () => { ); const packageJson = getJsonFileContent(tree, 'package.json'); - expect(packageJson.devDependencies['ng-packagr']).toBeUndefined(); + expect(packageJson.devDependencies['@angular/build']).toBeUndefined(); expect(packageJson.devDependencies['@angular-devkit/build-angular']).toBeUndefined(); }); }); @@ -348,6 +358,9 @@ describe('Library Schematic', () => { const cfg = JSON.parse(tree.readContent('/angular.json')); expect(cfg.projects['@myscope/mylib']).toBeDefined(); + expect(cfg.projects['@myscope/mylib'].architect.build.options.outputPath).toEqual( + 'dist/myscope/mylib', + ); const rootTsCfg = getJsonFileContent(tree, '/tsconfig.json'); expect(rootTsCfg.compilerOptions.paths['@myscope/mylib']).toEqual(['./dist/myscope/mylib']); @@ -363,14 +376,19 @@ describe('Library Schematic', () => { const pkgJsonPath = '/projects/my-scope/my-lib/package.json'; expect(tree.readContent(pkgJsonPath)).toContain(expectedScopeName); - const ngPkgJsonPath = '/projects/my-scope/my-lib/ng-package.json'; - expect(tree.readContent(ngPkgJsonPath)).toContain(expectedFolderName); - const pkgJson = JSON.parse(tree.readContent(pkgJsonPath)); expect(pkgJson.name).toEqual(expectedScopeName); const cfg = JSON.parse(tree.readContent('/angular.json')); expect(cfg.projects['@myScope/myLib']).toBeDefined(); + expect(cfg.projects['@myScope/myLib'].architect.build.options.outputPath).toEqual( + `dist/${expectedFolderName}`, + ); + + const rootTsCfg = getJsonFileContent(tree, '/tsconfig.json'); + expect(rootTsCfg.compilerOptions.paths['@my-scope/my-lib']).toEqual([ + `./dist/${expectedFolderName}`, + ]); }); it(`should create correct paths when 'newProjectRoot' is blank`, async () => { @@ -385,7 +403,9 @@ describe('Library Schematic', () => { const project = config.projects.foo; expect(project.root).toEqual('foo'); const { options, configurations } = project.architect.build; - expect(configurations.production.tsConfig).toEqual('foo/tsconfig.lib.prod.json'); + expect(options.tsConfig).toEqual('foo/tsconfig.lib.json'); + expect(options.outputPath).toBeUndefined(); + expect(configurations.production).toEqual({}); const libTsConfig = getJsonFileContent(tree, '/foo/tsconfig.lib.json'); expect(libTsConfig.extends).toEqual('../tsconfig.json'); @@ -397,14 +417,23 @@ describe('Library Schematic', () => { const tree = await schematicRunner.runSchematic('library', defaultOptions, workspaceTree); const workspace = JSON.parse(tree.readContent('/angular.json')); - expect(workspace.projects.foo.architect.build.configurations.development).toBeDefined(); + expect(workspace.projects.foo.architect.build.configurations.development).toEqual({ + compilationMode: 'full', + declarationMap: true, + }); }); - it(`should add 'ng-packagr' builder`, async () => { + it(`should add '@angular/build:library' builder`, async () => { const tree = await schematicRunner.runSchematic('library', defaultOptions, workspaceTree); const workspace = JSON.parse(tree.readContent('/angular.json')); - expect(workspace.projects.foo.architect.build.builder).toBe('@angular/build:ng-packagr'); + expect(workspace.projects.foo.architect.build.builder).toBe('@angular/build:library'); + expect(workspace.projects.foo.architect.build.defaultConfiguration).toBe('production'); + expect(workspace.projects.foo.architect.build.options.tsConfig).toBe( + 'projects/foo/tsconfig.lib.json', + ); + expect(workspace.projects.foo.architect.build.options.outputPath).toBeUndefined(); + expect(workspace.projects.foo.architect.build.configurations.production).toEqual({}); }); it(`should add 'unit-test' test builder`, async () => { @@ -412,6 +441,9 @@ describe('Library Schematic', () => { const workspace = JSON.parse(tree.readContent('/angular.json')); expect(workspace.projects.foo.architect.test.builder).toBe('@angular/build:unit-test'); + + const specTsConfig = getJsonFileContent(tree, '/projects/foo/tsconfig.spec.json'); + expect(specTsConfig.include).toEqual(['**/*.d.ts', '**/*.spec.ts']); }); it(`should add 'unit-test' test builder when vitest is available`, async () => { @@ -449,11 +481,9 @@ describe('Library Schematic', () => { const files = tree.files; expect(files).toEqual( jasmine.arrayContaining([ - '/projects/foo/ng-package.json', '/projects/foo/package.json', '/projects/foo/README.md', '/projects/foo/tsconfig.lib.json', - '/projects/foo/tsconfig.lib.prod.json', '/projects/foo/src/my-index.ts', '/projects/foo/src/lib/foo-module.ts', '/projects/foo/src/lib/foo.spec.ts', diff --git a/packages/schematics/angular/utility/workspace.ts b/packages/schematics/angular/utility/workspace.ts index f8567fc534a3..9d8b9b475dcd 100644 --- a/packages/schematics/angular/utility/workspace.ts +++ b/packages/schematics/angular/utility/workspace.ts @@ -116,7 +116,16 @@ export async function writeWorkspace( * @param project The project which will have its default path generated. */ export function buildDefaultPath(project: workspaces.ProjectDefinition): string { - const root = project.sourceRoot ? `/${project.sourceRoot}/` : `/${project.root}/src/`; + let root: string; + if (project.extensions['projectType'] === ProjectType.Library) { + root = + project.sourceRoot && project.sourceRoot !== project.root + ? `/${project.sourceRoot}/` + : `/${project.root}/src/`; + } else { + root = `/${project.sourceRoot || project.root}/`; // fallback for non-library projects + } + const projectDirName = project.extensions['projectType'] === ProjectType.Application ? 'app' : 'lib'; diff --git a/tests/e2e/tests/build/library-with-demo-app.ts b/tests/e2e/tests/build/library-with-demo-app.ts index 8e8d14ceb54f..0158e41d42d5 100644 --- a/tests/e2e/tests/build/library-with-demo-app.ts +++ b/tests/e2e/tests/build/library-with-demo-app.ts @@ -6,19 +6,14 @@ * found in the LICENSE file at https://angular.dev/license */ -import { appendToFile, createDir, writeFile } from '../../utils/fs'; +import { appendToFile } from '../../utils/fs'; import { ng } from '../../utils/process'; import { updateJsonFile } from '../../utils/project'; export default async function () { await ng('generate', 'library', 'mylib'); - await createLibraryEntryPoint('secondary'); - await createLibraryEntryPoint('another'); - - // Scenario #1 where we use wildcard path mappings for secondary entry-points. - await updateJsonFile('tsconfig.json', (json) => { - json.compilerOptions.paths = { 'mylib': ['./dist/mylib'], 'mylib/*': ['./dist/mylib/*'] }; - }); + await ng('generate', 'entry-point', 'secondary', '--project', 'mylib'); + await ng('generate', 'entry-point', 'another', '--project', 'mylib'); await appendToFile( 'src/app/app.config.ts', @@ -30,9 +25,10 @@ export default async function () { secondary, another }); - `, + `, ); + // Scenario #1 where we use wildcard path mappings as generated by the schematics. await ng('build', 'mylib'); await ng('build'); @@ -47,17 +43,3 @@ export default async function () { await ng('build'); } - -async function createLibraryEntryPoint(name: string): Promise { - await createDir(`projects/mylib/${name}`); - await writeFile(`projects/mylib/${name}/index.ts`, `export const foo = 'foo';`); - - await writeFile( - `projects/mylib/${name}/ng-package.json`, - JSON.stringify({ - lib: { - entryFile: 'index.ts', - }, - }), - ); -} diff --git a/tests/e2e/tests/vitest/library.ts b/tests/e2e/tests/vitest/library.ts index 0bb989057279..5d515681272b 100644 --- a/tests/e2e/tests/vitest/library.ts +++ b/tests/e2e/tests/vitest/library.ts @@ -1,3 +1,11 @@ +/** + * @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 assert from 'node:assert/strict'; import { updateJsonFile } from '../../utils/project'; import { ng } from '../../utils/process'; @@ -12,11 +20,11 @@ export default async function (): Promise { await createDir('projects/my-lib/src/styles'); await writeFile('projects/my-lib/src/styles/_vars.scss', '$primary-color: red;'); - // 2. Update ng-package.json to include the styles directory - await updateJsonFile('projects/my-lib/ng-package.json', (json) => { - json['lib'] = { - ...json['lib'], - styleIncludePaths: ['./src/styles'], + // 2. Update angular.json to include the styles directory + await updateJsonFile('angular.json', (json) => { + const buildOptions = (json['projects']['my-lib']['architect']['build']['options'] ??= {}); + buildOptions.stylePreprocessorOptions = { + includePaths: ['projects/my-lib/src/styles'], }; }); @@ -32,9 +40,12 @@ export default async function (): Promise { `, ); + // 4. Generate a secondary entry point and verify its tests are also executed + await ng('generate', 'entry-point', 'secondary', '--project=my-lib'); + // Run the library tests const { stdout } = await ng('test', 'my-lib'); - // Expect tests to pass - assert.match(stdout, /passed/, 'Expected library tests to pass.'); + // Expect tests to pass (my-lib, styled-comp, and secondary) + assert.match(stdout, /3 passed/, 'Expected 3 library tests to pass.'); }