diff --git a/packages/core/schematics/BUILD.bazel b/packages/core/schematics/BUILD.bazel index cdd99beec8..4270158582 100644 --- a/packages/core/schematics/BUILD.bazel +++ b/packages/core/schematics/BUILD.bazel @@ -13,6 +13,7 @@ npm_package( "//packages/core/schematics/migrations/dynamic-queries", "//packages/core/schematics/migrations/missing-injectable", "//packages/core/schematics/migrations/move-document", + "//packages/core/schematics/migrations/postinstall-ngcc", "//packages/core/schematics/migrations/renderer-to-renderer2", "//packages/core/schematics/migrations/static-queries", "//packages/core/schematics/migrations/template-var-assignment", diff --git a/packages/core/schematics/migrations.json b/packages/core/schematics/migrations.json index b32935f395..88c78ebfd8 100644 --- a/packages/core/schematics/migrations.json +++ b/packages/core/schematics/migrations.json @@ -39,6 +39,11 @@ "version": "9-beta", "description": "Removes the `static` flag from dynamic queries.", "factory": "./migrations/dynamic-queries/index" + }, + "migration-v9-postinstall-ngcc": { + "version": "9-beta", + "description": "Adds an ngcc call as a postinstall hook in package.json", + "factory": "./migrations/postinstall-ngcc/index" } } } diff --git a/packages/core/schematics/migrations/postinstall-ngcc/BUILD.bazel b/packages/core/schematics/migrations/postinstall-ngcc/BUILD.bazel new file mode 100644 index 0000000000..5b989a28f7 --- /dev/null +++ b/packages/core/schematics/migrations/postinstall-ngcc/BUILD.bazel @@ -0,0 +1,16 @@ +load("//tools:defaults.bzl", "ts_library") + +ts_library( + name = "postinstall-ngcc", + srcs = glob(["**/*.ts"]), + tsconfig = "//packages/core/schematics:tsconfig.json", + visibility = [ + "//packages/core/schematics:__pkg__", + "//packages/core/schematics/test:__pkg__", + ], + deps = [ + "@npm//@angular-devkit/core", + "@npm//@angular-devkit/schematics", + "@npm//@schematics/angular", + ], +) diff --git a/packages/core/schematics/migrations/postinstall-ngcc/README.md b/packages/core/schematics/migrations/postinstall-ngcc/README.md new file mode 100644 index 0000000000..6642a09935 --- /dev/null +++ b/packages/core/schematics/migrations/postinstall-ngcc/README.md @@ -0,0 +1,22 @@ +## Postinstall ngcc migration + +Automatically adds a postinstall script to `package.json` to run `ngcc`. +If a postinstall script is already there and does not call `ngcc`, the call will be prepended. + +#### Before +```json +{ + "scripts": { + "postinstall": "do-something" + } +} +``` + +#### After +```json +{ + "scripts": { + "postinstall": "ngcc ... && do-something" + } +} +``` \ No newline at end of file diff --git a/packages/core/schematics/migrations/postinstall-ngcc/index.ts b/packages/core/schematics/migrations/postinstall-ngcc/index.ts new file mode 100644 index 0000000000..e08d81b636 --- /dev/null +++ b/packages/core/schematics/migrations/postinstall-ngcc/index.ts @@ -0,0 +1,79 @@ +/** + * @license + * Copyright Google Inc. 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 {JsonParseMode, parseJsonAst} from '@angular-devkit/core'; +import {Rule, SchematicContext, SchematicsException, Tree} from '@angular-devkit/schematics'; +import {NodePackageInstallTask} from '@angular-devkit/schematics/tasks'; +import {appendPropertyInAstObject, findPropertyInAstObject, insertPropertyInAstObjectInOrder} from '@schematics/angular/utility/json-utils'; + + +/** + * Runs the ngcc postinstall migration for the current CLI workspace. + */ +export default function(): Rule { + return (tree: Tree, context: SchematicContext) => { + addPackageJsonScript( + tree, 'postinstall', + 'ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points'); + context.addTask(new NodePackageInstallTask()); + }; +} + +function addPackageJsonScript(tree: Tree, scriptName: string, script: string): void { + const pkgJsonPath = '/package.json'; + + // Read package.json and turn it into an AST. + const buffer = tree.read(pkgJsonPath); + if (buffer === null) { + throw new SchematicsException('Could not read package.json.'); + } + const content = buffer.toString(); + + const packageJsonAst = parseJsonAst(content, JsonParseMode.Strict); + if (packageJsonAst.kind != 'object') { + throw new SchematicsException('Invalid package.json. Was expecting an object.'); + } + + // Begin recording changes. + const recorder = tree.beginUpdate(pkgJsonPath); + const scriptsNode = findPropertyInAstObject(packageJsonAst, 'scripts'); + + if (!scriptsNode) { + // Haven't found the scripts key, add it to the root of the package.json. + appendPropertyInAstObject( + recorder, packageJsonAst, 'scripts', { + [scriptName]: script, + }, + 2); + } else if (scriptsNode.kind === 'object') { + // Check if the script is already there. + const scriptNode = findPropertyInAstObject(scriptsNode, scriptName); + + if (!scriptNode) { + // Script not found, add it. + insertPropertyInAstObjectInOrder(recorder, scriptsNode, scriptName, script, 4); + } else { + // Script found, prepend the new script with &&. + const currentScript = scriptNode.value; + if (typeof currentScript == 'string') { + // Only add script if there's no ngcc call there already. + if (!currentScript.includes('ngcc')) { + const {start, end} = scriptNode; + recorder.remove(start.offset, end.offset - start.offset); + recorder.insertRight(start.offset, JSON.stringify(`${script} && ${currentScript}`)); + } + } else { + throw new SchematicsException( + 'Invalid postinstall script in package.json. Was expecting a string.'); + } + } + } + + // Write the changes. + tree.commitUpdate(recorder); +} diff --git a/packages/core/schematics/test/BUILD.bazel b/packages/core/schematics/test/BUILD.bazel index c14f4c8010..e983e354f5 100644 --- a/packages/core/schematics/test/BUILD.bazel +++ b/packages/core/schematics/test/BUILD.bazel @@ -11,6 +11,7 @@ ts_library( "//packages/core/schematics/migrations/dynamic-queries", "//packages/core/schematics/migrations/missing-injectable", "//packages/core/schematics/migrations/move-document", + "//packages/core/schematics/migrations/postinstall-ngcc", "//packages/core/schematics/migrations/renderer-to-renderer2", "//packages/core/schematics/migrations/static-queries", "//packages/core/schematics/migrations/template-var-assignment", diff --git a/packages/core/schematics/test/postinstall_ngcc_spec.ts b/packages/core/schematics/test/postinstall_ngcc_spec.ts new file mode 100644 index 0000000000..f31ee2cbbf --- /dev/null +++ b/packages/core/schematics/test/postinstall_ngcc_spec.ts @@ -0,0 +1,58 @@ +/** + * @license + * Copyright Google Inc. 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'; + + +describe('postinstall ngcc migration', () => { + let runner: SchematicTestRunner; + let tree: UnitTestTree; + const pkgJsonPath = '/package.json'; + const ngccPostinstall = + `"postinstall": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points"`; + + beforeEach(() => { + runner = new SchematicTestRunner('test', require.resolve('../migrations.json')); + tree = new UnitTestTree(new EmptyTree()); + }); + + it(`should add postinstall if scripts object is missing`, async() => { + tree.create(pkgJsonPath, JSON.stringify({}, null, 2)); + await runMigration(); + expect(tree.readContent(pkgJsonPath)).toContain(ngccPostinstall); + }); + + it(`should add postinstall if the script is missing`, async() => { + tree.create(pkgJsonPath, JSON.stringify({scripts: {}}, null, 2)); + await runMigration(); + expect(tree.readContent(pkgJsonPath)).toContain(ngccPostinstall); + }); + + it(`should prepend to postinstall if script already exists`, async() => { + tree.create(pkgJsonPath, JSON.stringify({scripts: {postinstall: 'do-something'}}, null, 2)); + await runMigration(); + expect(tree.readContent(pkgJsonPath)) + .toContain( + `"postinstall": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points && do-something"`); + }); + + it(`should not prepend to postinstall if script contains ngcc`, async() => { + tree.create(pkgJsonPath, JSON.stringify({scripts: {postinstall: 'ngcc --something'}}, null, 2)); + await runMigration(); + expect(tree.readContent(pkgJsonPath)).toContain(`"postinstall": "ngcc --something"`); + expect(tree.readContent(pkgJsonPath)).not.toContain(ngccPostinstall); + expect(tree.readContent(pkgJsonPath)) + .not.toContain( + `ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points`); + }); + + function runMigration() { + return runner.runSchematicAsync('migration-v9-postinstall-ngcc', {}, tree).toPromise(); + } +});