From 42fb8b9f74a2f40c920f8ed16902d415357c454c Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Sat, 17 Aug 2024 22:33:53 +0200 Subject: [PATCH 1/2] feat(core): Add a schematics to migrate to `standalone: false`. With the framework enabling `standalone` by default (making module based an opt-in), the migration will migrate none-standalone existing components and add `standalone: false` to the decorator. --- packages/core/schematics/BUILD.bazel | 2 + packages/core/schematics/migrations.json | 8 +- .../migrations/standalone-false/BUILD.bazel | 21 +++ .../migrations/standalone-false/index.ts | 58 ++++++ .../migrations/standalone-false/migration.ts | 126 +++++++++++++ .../schematics/test/standalone_false_spec.ts | 174 ++++++++++++++++++ 6 files changed, 388 insertions(+), 1 deletion(-) create mode 100644 packages/core/schematics/migrations/standalone-false/BUILD.bazel create mode 100644 packages/core/schematics/migrations/standalone-false/index.ts create mode 100644 packages/core/schematics/migrations/standalone-false/migration.ts create mode 100644 packages/core/schematics/test/standalone_false_spec.ts diff --git a/packages/core/schematics/BUILD.bazel b/packages/core/schematics/BUILD.bazel index 206ce03ae3ae..da1473805ffb 100644 --- a/packages/core/schematics/BUILD.bazel +++ b/packages/core/schematics/BUILD.bazel @@ -33,6 +33,7 @@ rollup_bundle( "//packages/core/schematics/ng-generate/inject-migration:index.ts": "inject-migration", "//packages/core/schematics/ng-generate/route-lazy-loading:index.ts": "route-lazy-loading", "//packages/core/schematics/ng-generate/standalone-migration:index.ts": "standalone-migration", + "//packages/core/schematics/migrations/standalone-false:index.ts": "standalone-false", }, format = "cjs", link_workspace_root = True, @@ -42,6 +43,7 @@ rollup_bundle( "//packages/core/schematics/test:__pkg__", ], deps = [ + "//packages/core/schematics/migrations/standalone-false", "//packages/core/schematics/ng-generate/control-flow-migration", "//packages/core/schematics/ng-generate/inject-migration", "//packages/core/schematics/ng-generate/route-lazy-loading", diff --git a/packages/core/schematics/migrations.json b/packages/core/schematics/migrations.json index 63001b445889..c04ff27d20a9 100644 --- a/packages/core/schematics/migrations.json +++ b/packages/core/schematics/migrations.json @@ -1,3 +1,9 @@ { - "schematics": {} + "schematics": { + "standalone-false": { + "version": "19.0.0", + "description": "Updates non-standalone Directives, Component and Pipes to standalone:false", + "factory": "./bundles/standalone-false#migrate" + } + } } diff --git a/packages/core/schematics/migrations/standalone-false/BUILD.bazel b/packages/core/schematics/migrations/standalone-false/BUILD.bazel new file mode 100644 index 000000000000..582c3a457987 --- /dev/null +++ b/packages/core/schematics/migrations/standalone-false/BUILD.bazel @@ -0,0 +1,21 @@ +load("//tools:defaults.bzl", "ts_library") + +package( + default_visibility = [ + "//packages/core/schematics:__pkg__", + "//packages/core/schematics/migrations/google3:__pkg__", + "//packages/core/schematics/test:__pkg__", + ], +) + +ts_library( + name = "standalone-false", + srcs = glob(["**/*.ts"]), + tsconfig = "//packages/core/schematics:tsconfig.json", + deps = [ + "//packages/core/schematics/utils", + "@npm//@angular-devkit/schematics", + "@npm//@types/node", + "@npm//typescript", + ], +) diff --git a/packages/core/schematics/migrations/standalone-false/index.ts b/packages/core/schematics/migrations/standalone-false/index.ts new file mode 100644 index 000000000000..e459ef1fce09 --- /dev/null +++ b/packages/core/schematics/migrations/standalone-false/index.ts @@ -0,0 +1,58 @@ +/** + * @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 {Rule, SchematicsException, Tree, UpdateRecorder} from '@angular-devkit/schematics'; +import {relative} from 'path'; +import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; +import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host'; +import {migrateFile} from './migration'; + +export function migrate(): Rule { + return async (tree: Tree) => { + const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); + const basePath = process.cwd(); + const allPaths = [...buildPaths, ...testPaths]; + + if (!allPaths.length) { + throw new SchematicsException( + 'Could not find any tsconfig file. Cannot run the standalone:false migration.', + ); + } + + for (const tsconfigPath of allPaths) { + runMigration(tree, tsconfigPath, basePath); + } + }; +} + +function runMigration(tree: Tree, tsconfigPath: string, basePath: string) { + const program = createMigrationProgram(tree, tsconfigPath, basePath); + const sourceFiles = program + .getSourceFiles() + .filter((sourceFile) => canMigrateFile(basePath, sourceFile, program)); + + for (const sourceFile of sourceFiles) { + let update: UpdateRecorder | null = null; + + const rewriter = (startPos: number, width: number, text: string | null) => { + if (update === null) { + // Lazily initialize update, because most files will not require migration. + update = tree.beginUpdate(relative(basePath, sourceFile.fileName)); + } + update.remove(startPos, width); + if (text !== null) { + update.insertLeft(startPos, text); + } + }; + migrateFile(sourceFile, rewriter); + + if (update !== null) { + tree.commitUpdate(update); + } + } +} diff --git a/packages/core/schematics/migrations/standalone-false/migration.ts b/packages/core/schematics/migrations/standalone-false/migration.ts new file mode 100644 index 000000000000..d3ca5407e0c3 --- /dev/null +++ b/packages/core/schematics/migrations/standalone-false/migration.ts @@ -0,0 +1,126 @@ +/** + * @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 ts from 'typescript'; +import {ChangeTracker} from '../../utils/change_tracker'; +import {getImportSpecifier, getNamedImports} from '../../utils/typescript/imports'; + +const CORE = '@angular/core'; +const DIRECTIVE = 'Directive'; +const COMPONENT = 'Component'; +const PIPE = 'Pipe'; + +type RewriteFn = (startPos: number, width: number, text: string) => void; + +export function migrateFile(sourceFile: ts.SourceFile, rewriteFn: RewriteFn) { + const changeTracker = new ChangeTracker(ts.createPrinter()); + + // Check if there are any imports of the `AfterRenderPhase` enum. + const coreImports = getNamedImports(sourceFile, CORE); + if (!coreImports) { + return; + } + const directive = getImportSpecifier(sourceFile, CORE, DIRECTIVE); + const component = getImportSpecifier(sourceFile, CORE, COMPONENT); + const pipe = getImportSpecifier(sourceFile, CORE, PIPE); + + if (!directive && !component && !pipe) { + return; + } + + ts.forEachChild(sourceFile, function visit(node: ts.Node) { + ts.forEachChild(node, visit); + + // First we need to check for class declarations + // Decorators will come after + if (!ts.isClassDeclaration(node)) { + return; + } + + ts.getDecorators(node)?.forEach((decorator) => { + if (!ts.isDecorator(decorator)) { + return; + } + + const callExpression = decorator.expression; + if (!ts.isCallExpression(callExpression)) { + return; + } + + const decoratorIdentifier = callExpression.expression; + if (!ts.isIdentifier(decoratorIdentifier)) { + return; + } + + // Checking the identifier of the decorator by comparing to the import specifier + switch (decoratorIdentifier.text) { + case directive?.name.text: + case component?.name.text: + case pipe?.name.text: + break; + default: + // It's not a decorator to migrate + return; + } + + const [firstArg] = callExpression.arguments; + if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) { + return; + } + const properties = firstArg.properties; + const standaloneProp = getStandaloneProperty(properties); + + // Need to take care of 3 cases + // - standalone: true => remove the property + // - standalone: false => nothing + // - No standalone property => add a standalone: false property + + let newProperties; + if (!standaloneProp) { + const standaloneFalseProperty = ts.factory.createPropertyAssignment( + 'standalone', + ts.factory.createFalse(), + ); + + newProperties = [...properties, standaloneFalseProperty]; + } else if (standaloneProp.value === ts.SyntaxKind.TrueKeyword) { + newProperties = properties.filter((p) => p !== standaloneProp.property); + } + + if (newProperties) { + // At this point we know that we need to add standalone: false or + // remove an existing standalone: true property. + const newPropsArr = ts.factory.createNodeArray(newProperties); + const newFirstArg = ts.factory.createObjectLiteralExpression(newPropsArr, true); + changeTracker.replaceNode(firstArg, newFirstArg); + } + }); + }); + + // Write the changes. + for (const changesInFile of changeTracker.recordChanges().values()) { + for (const change of changesInFile) { + rewriteFn(change.start, change.removeLength ?? 0, change.text); + } + } +} + +function getStandaloneProperty(properties: ts.NodeArray) { + for (const prop of properties) { + if ( + ts.isPropertyAssignment(prop) && + ts.isIdentifier(prop.name) && + prop.name.text === 'standalone' && + (prop.initializer.kind === ts.SyntaxKind.TrueKeyword || + prop.initializer.kind === ts.SyntaxKind.FalseKeyword) + ) { + return {property: prop, value: prop.initializer.kind}; + } + } + return undefined; +} diff --git a/packages/core/schematics/test/standalone_false_spec.ts b/packages/core/schematics/test/standalone_false_spec.ts new file mode 100644 index 000000000000..4c10667130a5 --- /dev/null +++ b/packages/core/schematics/test/standalone_false_spec.ts @@ -0,0 +1,174 @@ +/** + * @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 {getSystemPath, normalize, virtualFs} from '@angular-devkit/core'; +import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing'; +import {HostTree} from '@angular-devkit/schematics'; +import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing'; +import {runfiles} from '@bazel/runfiles'; +import shx from 'shelljs'; + +describe('standalone:false migration', () => { + let runner: SchematicTestRunner; + let host: TempScopedNodeJsSyncHost; + let tree: UnitTestTree; + let tmpDirPath: string; + let previousWorkingDir: string; + + function writeFile(filePath: string, contents: string) { + host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); + } + + function runMigration() { + return runner.runSchematic('standalone-false', {}, tree); + } + + beforeEach(() => { + runner = new SchematicTestRunner('test', runfiles.resolvePackageRelative('../migrations.json')); + host = new TempScopedNodeJsSyncHost(); + tree = new UnitTestTree(new HostTree(host)); + + writeFile( + '/tsconfig.json', + JSON.stringify({ + compilerOptions: { + lib: ['es2015'], + strictNullChecks: true, + }, + }), + ); + + writeFile( + '/angular.json', + JSON.stringify({ + version: 1, + projects: {t: {root: '', architect: {build: {options: {tsConfig: './tsconfig.json'}}}}}, + }), + ); + + previousWorkingDir = shx.pwd(); + tmpDirPath = getSystemPath(host.root); + + // Switch into the temporary directory path. This allows us to run + // the schematic against our custom unit test tree. + shx.cd(tmpDirPath); + }); + + it('should update standalone to false for Directive', async () => { + writeFile( + '/index.ts', + ` + import { Directive } from '@angular/core'; + + @Directive({ + selector: '[someDirective]' + }) + export class SomeDirective { + }`, + ); + + await runMigration(); + + const content = tree.readContent('/index.ts').replace(/\s+/g, ' '); + expect(content).toContain('standalone: false'); + }); + + it('should update standalone to false for Component', async () => { + writeFile( + '/index.ts', + ` + import { Component } from '@angular/core'; + + @Component({ + selector: '[someComponent]' + }) + export class SomeComponent { + }`, + ); + + await runMigration(); + + const content = tree.readContent('/index.ts').replace(/\s+/g, ' '); + expect(content).toContain('standalone: false'); + }); + + it('should update standalone to false for Pipe', async () => { + writeFile( + '/index.ts', + ` + import { Pipe } from '@angular/core'; + + @Pipe({ + name: 'somePipe' + }) + export class SomePipe { + }`, + ); + + await runMigration(); + + const content = tree.readContent('/index.ts').replace(/\s+/g, ' '); + expect(content).toContain('standalone: false'); + }); + + it('should remove standalone:true', async () => { + writeFile( + '/index.ts', + ` + import { Directive } from '@angular/core'; + + @Directive({ + selector: '[someDirective]', + standalone: true + }) + export class SomeDirective { + }`, + ); + + await runMigration(); + + const content = tree.readContent('/index.ts').replace(/\s+/g, ' '); + expect(content).not.toContain('standalone'); + }); + + it('should not update a directive with standalone:false', async () => { + writeFile( + '/index.ts', + ` + import { Directive } from '@angular/core'; + + @Directive({ + selector: '[someDirective]', + standalone: false + }) + export class SomeDirective { + }`, + ); + + await runMigration(); + + const content = tree.readContent('/index.ts').replace(/\s+/g, ' '); + expect(content).not.toContain('standalone: true'); + expect(content).toContain('standalone: false'); + }); + + it('should not update an empty directive', async () => { + writeFile( + '/index.ts', + ` + import { Directive } from '@angular/core'; + @Directive() + export class SomeDirective {}`, + ); + + await runMigration(); + + const content = tree.readContent('/index.ts').replace(/\s+/g, ' '); + expect(content).not.toContain('standalone'); + }); +}); From e4d7ddb84d0e6dbb7b1b3cfdbf89873e82eae93d Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Fri, 6 Sep 2024 10:57:00 +0200 Subject: [PATCH 2/2] fixup! feat(core): Add a schematics to migrate to `standalone: false`. --- packages/core/schematics/BUILD.bazel | 4 ++-- packages/core/schematics/migrations.json | 4 ++-- .../BUILD.bazel | 2 +- .../{standalone-false => explicit-standalone-flag}/index.ts | 0 .../migration.ts | 0 ...ndalone_false_spec.ts => explicit_standalone_flag_spec.ts} | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) rename packages/core/schematics/migrations/{standalone-false => explicit-standalone-flag}/BUILD.bazel (93%) rename packages/core/schematics/migrations/{standalone-false => explicit-standalone-flag}/index.ts (100%) rename packages/core/schematics/migrations/{standalone-false => explicit-standalone-flag}/migration.ts (100%) rename packages/core/schematics/test/{standalone_false_spec.ts => explicit_standalone_flag_spec.ts} (98%) diff --git a/packages/core/schematics/BUILD.bazel b/packages/core/schematics/BUILD.bazel index da1473805ffb..f98b0014a48f 100644 --- a/packages/core/schematics/BUILD.bazel +++ b/packages/core/schematics/BUILD.bazel @@ -33,7 +33,7 @@ rollup_bundle( "//packages/core/schematics/ng-generate/inject-migration:index.ts": "inject-migration", "//packages/core/schematics/ng-generate/route-lazy-loading:index.ts": "route-lazy-loading", "//packages/core/schematics/ng-generate/standalone-migration:index.ts": "standalone-migration", - "//packages/core/schematics/migrations/standalone-false:index.ts": "standalone-false", + "//packages/core/schematics/migrations/explicit-standalone-flag:index.ts": "explicit-standalone-flag", }, format = "cjs", link_workspace_root = True, @@ -43,7 +43,7 @@ rollup_bundle( "//packages/core/schematics/test:__pkg__", ], deps = [ - "//packages/core/schematics/migrations/standalone-false", + "//packages/core/schematics/migrations/explicit-standalone-flag", "//packages/core/schematics/ng-generate/control-flow-migration", "//packages/core/schematics/ng-generate/inject-migration", "//packages/core/schematics/ng-generate/route-lazy-loading", diff --git a/packages/core/schematics/migrations.json b/packages/core/schematics/migrations.json index c04ff27d20a9..b36f6ad5eb14 100644 --- a/packages/core/schematics/migrations.json +++ b/packages/core/schematics/migrations.json @@ -1,9 +1,9 @@ { "schematics": { - "standalone-false": { + "explicit-standalone-flag": { "version": "19.0.0", "description": "Updates non-standalone Directives, Component and Pipes to standalone:false", - "factory": "./bundles/standalone-false#migrate" + "factory": "./bundles/explicit-standalone-flag#migrate" } } } diff --git a/packages/core/schematics/migrations/standalone-false/BUILD.bazel b/packages/core/schematics/migrations/explicit-standalone-flag/BUILD.bazel similarity index 93% rename from packages/core/schematics/migrations/standalone-false/BUILD.bazel rename to packages/core/schematics/migrations/explicit-standalone-flag/BUILD.bazel index 582c3a457987..ed322928b253 100644 --- a/packages/core/schematics/migrations/standalone-false/BUILD.bazel +++ b/packages/core/schematics/migrations/explicit-standalone-flag/BUILD.bazel @@ -9,7 +9,7 @@ package( ) ts_library( - name = "standalone-false", + name = "explicit-standalone-flag", srcs = glob(["**/*.ts"]), tsconfig = "//packages/core/schematics:tsconfig.json", deps = [ diff --git a/packages/core/schematics/migrations/standalone-false/index.ts b/packages/core/schematics/migrations/explicit-standalone-flag/index.ts similarity index 100% rename from packages/core/schematics/migrations/standalone-false/index.ts rename to packages/core/schematics/migrations/explicit-standalone-flag/index.ts diff --git a/packages/core/schematics/migrations/standalone-false/migration.ts b/packages/core/schematics/migrations/explicit-standalone-flag/migration.ts similarity index 100% rename from packages/core/schematics/migrations/standalone-false/migration.ts rename to packages/core/schematics/migrations/explicit-standalone-flag/migration.ts diff --git a/packages/core/schematics/test/standalone_false_spec.ts b/packages/core/schematics/test/explicit_standalone_flag_spec.ts similarity index 98% rename from packages/core/schematics/test/standalone_false_spec.ts rename to packages/core/schematics/test/explicit_standalone_flag_spec.ts index 4c10667130a5..f4160c652629 100644 --- a/packages/core/schematics/test/standalone_false_spec.ts +++ b/packages/core/schematics/test/explicit_standalone_flag_spec.ts @@ -25,7 +25,7 @@ describe('standalone:false migration', () => { } function runMigration() { - return runner.runSchematic('standalone-false', {}, tree); + return runner.runSchematic('explicit-standalone-flag', {}, tree); } beforeEach(() => {