diff --git a/packages/schematics/angular/migrations/migration-collection.json b/packages/schematics/angular/migrations/migration-collection.json index acfcf643db4d..988b8498cf8b 100644 --- a/packages/schematics/angular/migrations/migration-collection.json +++ b/packages/schematics/angular/migrations/migration-collection.json @@ -139,6 +139,11 @@ "version": "13.0.0", "factory": "./update-13/update-angular-config", "description": "Remove deprecated options from 'angular.json' that are no longer present in v13." + }, + "update-libraries-v13": { + "version": "13.0.0", + "factory": "./update-13/update-libraries", + "description": "Update library projects to be published in partial mode and removed deprecated options from ng-packagr configuration." } } } diff --git a/packages/schematics/angular/migrations/update-13/update-libraries.ts b/packages/schematics/angular/migrations/update-13/update-libraries.ts new file mode 100644 index 000000000000..361aad7b4f92 --- /dev/null +++ b/packages/schematics/angular/migrations/update-13/update-libraries.ts @@ -0,0 +1,85 @@ +/** + * @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.io/license + */ + +import { join } from '@angular-devkit/core'; +import { DirEntry, Rule } from '@angular-devkit/schematics'; +import { JSONFile } from '../../utility/json-file'; +import { allTargetOptions, getWorkspace } from '../../utility/workspace'; + +function* visit(directory: DirEntry): IterableIterator { + for (const path of directory.subfiles) { + if (path === 'ng-package.json') { + yield join(directory.path, path); + } + } + + for (const path of directory.subdirs) { + if (path === 'node_modules' || path.startsWith('.')) { + continue; + } + + yield* visit(directory.dir(path)); + } +} + +export default function (): Rule { + const ENABLE_IVY_JSON_PATH = ['angularCompilerOptions', 'enableIvy']; + const COMPILATION_MODE_JSON_PATH = ['angularCompilerOptions', 'compilationMode']; + const NG_PACKAGR_DEPRECATED_OPTIONS_PATHS = [ + ['lib', 'umdModuleIds'], + ['lib', 'amdId'], + ['lib', 'umdId'], + ]; + + return async (tree) => { + const workspace = await getWorkspace(tree); + const librariesTsConfig = new Set(); + const ngPackagrConfig = new Set(); + + for (const [, project] of workspace.projects) { + for (const [_, target] of project.targets) { + if (target.builder !== '@angular-devkit/build-angular:ng-packagr') { + continue; + } + + for (const [, options] of allTargetOptions(target)) { + if (typeof options.tsConfig === 'string') { + librariesTsConfig.add(options.tsConfig); + } + + if (typeof options.project === 'string') { + ngPackagrConfig.add(options.project); + } + } + } + } + + // Gather configurations which are not referecned in angular.json + // (This happens when users have secondary entry-points) + for (const p of visit(tree.root)) { + ngPackagrConfig.add(p); + } + + // Update ng-packagr configuration + for (const config of ngPackagrConfig) { + const json = new JSONFile(tree, config); + for (const optionPath of NG_PACKAGR_DEPRECATED_OPTIONS_PATHS) { + json.remove(optionPath); + } + } + + // Update tsconfig files + for (const tsConfig of librariesTsConfig) { + const json = new JSONFile(tree, tsConfig); + if (json.get(ENABLE_IVY_JSON_PATH) === false) { + json.remove(ENABLE_IVY_JSON_PATH); + json.modify(COMPILATION_MODE_JSON_PATH, 'partial'); + } + } + }; +} diff --git a/packages/schematics/angular/migrations/update-13/update-libraries_spec.ts b/packages/schematics/angular/migrations/update-13/update-libraries_spec.ts new file mode 100644 index 000000000000..c30568614391 --- /dev/null +++ b/packages/schematics/angular/migrations/update-13/update-libraries_spec.ts @@ -0,0 +1,138 @@ +/** + * @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.io/license + */ + +import { EmptyTree } from '@angular-devkit/schematics'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; +import { Builders, ProjectType, WorkspaceSchema } from '../../utility/workspace-models'; + +function readJsonFile(tree: UnitTestTree, path: string): Record> { + return JSON.parse(tree.readContent(path)); +} + +function createWorkSpaceConfig(tree: UnitTestTree) { + const angularConfig: WorkspaceSchema = { + version: 1, + projects: { + app: { + root: '', + sourceRoot: 'src', + projectType: ProjectType.Library, + prefix: 'app', + architect: { + build: { + builder: Builders.NgPackagr, + options: { + project: 'ngpackage.json', + tsConfig: 'tsconfig.lib.json', + }, + configurations: { + production: { + tsConfig: 'tsconfig.lib.prod.json', + }, + }, + }, + }, + }, + }, + }; + + tree.create('/angular.json', JSON.stringify(angularConfig, undefined, 2)); + tree.create( + '/tsconfig.lib.json', + JSON.stringify( + { angularCompilerOptions: { enableIvy: true, fullTemplateTypeCheck: true } }, + undefined, + 2, + ), + ); + tree.create( + '/tsconfig.lib.prod.json', + JSON.stringify( + { angularCompilerOptions: { enableIvy: false, fullTemplateTypeCheck: true } }, + undefined, + 2, + ), + ); + + tree.create( + '/ngpackage.json', + JSON.stringify( + { + lib: { entryFile: 'src/public-api.ts', amdId: 'foo', umdId: 'foo', umdModuleIds: ['foo'] }, + }, + undefined, + 2, + ), + ); +} + +const schematicName = 'update-libraries-v13'; + +describe(`Migration to update library projects. ${schematicName}`, () => { + const schematicRunner = new SchematicTestRunner( + 'migrations', + require.resolve('../migration-collection.json'), + ); + + let tree: UnitTestTree; + beforeEach(() => { + tree = new UnitTestTree(new EmptyTree()); + createWorkSpaceConfig(tree); + }); + + describe('TypeScript Config', () => { + it(`should replace "enableIvy: false" with "compilationMode: "partial" `, async () => { + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + const { angularCompilerOptions } = readJsonFile(newTree, 'tsconfig.lib.prod.json'); + expect(angularCompilerOptions.compilationMode).toBe('partial'); + expect(angularCompilerOptions.enableIvy).toBeUndefined(); + }); + + it(`should not replace "enableIvy: true"`, async () => { + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + const { angularCompilerOptions } = readJsonFile(newTree, 'tsconfig.lib.json'); + expect(angularCompilerOptions.enableIvy).toBeTrue(); + }); + }); + + describe('Ng-packagr Config', () => { + it(`should remove UMD related options from ng-packagr configuration referenced from angular.json`, async () => { + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + const { lib } = readJsonFile(newTree, 'ngpackage.json'); + expect(lib.entryFile).toBeDefined(); + expect(lib.amdId).toBeUndefined(); + expect(lib.umdId).toBeUndefined(); + expect(lib.umdModuleIds).toBeUndefined(); + }); + + it(`should remove UMD related options from un-referenced ng-packagr configuration (secondary entry-points)`, async () => { + tree.create( + '/testing/ng-package.json', + JSON.stringify( + { + lib: { + entryFile: 'src/public-api.ts', + amdId: 'foo', + umdId: 'foo', + umdModuleIds: ['foo'], + }, + }, + undefined, + 2, + ), + ); + + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + const { lib } = readJsonFile(newTree, 'testing/ng-package.json'); + expect(lib.entryFile).toBeDefined(); + expect(lib.amdId).toBeUndefined(); + expect(lib.umdId).toBeUndefined(); + expect(lib.umdModuleIds).toBeUndefined(); + }); + }); +});