Skip to content

Commit

Permalink
feat(migrations): Schematics for TransferState, StateKey and `mak…
Browse files Browse the repository at this point in the history
…eStateKey` migration. (#49594)

These 3 classes have been moved from platform-browser to core by #49563

PR Close #49594
  • Loading branch information
JeanMeche authored and atscott committed Oct 10, 2023
1 parent 8b91864 commit 965ce5a
Show file tree
Hide file tree
Showing 10 changed files with 332 additions and 8 deletions.
1 change: 1 addition & 0 deletions packages/core/schematics/BUILD.bazel
Expand Up @@ -20,6 +20,7 @@ pkg_npm(
deps = [
"//packages/core/schematics/migrations/block-template-entities:bundle",
"//packages/core/schematics/migrations/compiler-options:bundle",
"//packages/core/schematics/migrations/transfer-state:bundle",
"//packages/core/schematics/ng-generate/control-flow-migration:bundle",
"//packages/core/schematics/ng-generate/standalone-migration:bundle",
],
Expand Down
5 changes: 5 additions & 0 deletions packages/core/schematics/migrations.json
Expand Up @@ -9,6 +9,11 @@
"version": "17.0.0",
"description": "CompilerOption.useJit and CompilerOption.missingTranslation are unused under Ivy. This migration removes their usage",
"factory": "./migrations/compiler-options/bundle"
},
"migration-transfer-state": {
"version": "17.0.0",
"description": "Updates `TransferState`, `makeStateKey`, `StateKey` imports from `@angular/platform-browser` to `@angular/core`.",
"factory": "./migrations/transfer-state/bundle"
}
}
}
33 changes: 33 additions & 0 deletions packages/core/schematics/migrations/transfer-state/BUILD.bazel
@@ -0,0 +1,33 @@
load("//tools:defaults.bzl", "esbuild", "ts_library")

package(
default_visibility = [
"//packages/core/schematics:__pkg__",
"//packages/core/schematics/migrations/google3:__pkg__",
"//packages/core/schematics/test:__pkg__",
],
)

ts_library(
name = "transfer-state",
srcs = glob(["**/*.ts"]),
tsconfig = "//packages/core/schematics:tsconfig.json",
deps = [
"//packages/core/schematics/utils",
"@npm//@angular-devkit/schematics",
"@npm//@types/node",
"@npm//typescript",
],
)

esbuild(
name = "bundle",
entry_point = ":index.ts",
external = [
"@angular-devkit/*",
"typescript",
],
format = "cjs",
platform = "node",
deps = [":transfer-state"],
)
19 changes: 19 additions & 0 deletions packages/core/schematics/migrations/transfer-state/README.md
@@ -0,0 +1,19 @@
## Import TransferState-related symbols from `@angular/core`

The following symbols were moved from `@angular/platform-browser` to `@angular/core`:

* `TransferState`
* `makeStateKey`
* `StateKey`

This migration updates symbol imports to use `@angular/core`.

#### Before
```ts
import { TransferState, makeStateKey, StateKey } from '@angular/platform-browser';
```

#### After
```ts
import { TransferState, makeStateKey, StateKey } from '@angular/core';
```
59 changes: 59 additions & 0 deletions packages/core/schematics/migrations/transfer-state/index.ts
@@ -0,0 +1,59 @@
/**
* @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 './utils';


export default function(): 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 transfer state 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);
}
}
}
75 changes: 75 additions & 0 deletions packages/core/schematics/migrations/transfer-state/utils.ts
@@ -0,0 +1,75 @@
/**
* @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 {getImportSpecifiers, removeSymbolFromNamedImports} from '../../utils/typescript/imports';
import {closestNode} from '../../utils/typescript/nodes';

export const symbolsToUpdate = new Set(['makeStateKey', 'StateKey', 'TransferState']);
export const platformBrowserModule = '@angular/platform-browser';
export const coreModule = '@angular/core';

export type RewriteFn = (startPos: number, width: number, text: string) => void;

export function migrateFile(sourceFile: ts.SourceFile, rewriteFn: RewriteFn) {
const exposedImports =
getImportSpecifiers(sourceFile, platformBrowserModule, [...symbolsToUpdate]);
if (exposedImports.length === 0) {
return;
}

migrateImports(sourceFile, rewriteFn);
}

function migrateImports(sourceFile: ts.SourceFile, rewriteFn: RewriteFn) {
let changeTracker = new ChangeTracker(ts.createPrinter());
const updatedImports = new Map<ts.NamedImports, ts.NamedImports>();
const addedImports = new Array();
const importSpecifiers =
getImportSpecifiers(sourceFile, platformBrowserModule, [...symbolsToUpdate]);
for (const importSpecifier of importSpecifiers) {
const namedImports = closestNode(importSpecifier, ts.isNamedImports)!;
const importToUpdate = updatedImports.get(namedImports) ?? namedImports;
const rewrittenNamedImports = removeSymbolFromNamedImports(importToUpdate, importSpecifier);
updatedImports.set(namedImports, rewrittenNamedImports);
addedImports.push(importSpecifier.name.getText());
}

// Remove the existing imports
for (const [originalNode, rewrittenNode] of updatedImports.entries()) {
if (rewrittenNode.elements.length > 0) {
changeTracker.replaceNode(originalNode, rewrittenNode);
} else {
const importDeclaration = originalNode.parent.parent;
changeTracker.removeNode(importDeclaration);
}
}

// Apply the removal changes
for (const changesInFile of changeTracker.recordChanges().values()) {
for (const change of changesInFile) {
rewriteFn(change.start, change.removeLength ?? 0, change.text);
}
}

changeTracker.clearChanges();

// Add the new imports
for (const i of addedImports) {
changeTracker.addImport(sourceFile, i, coreModule, null, true);
}

// Apply the adding changes
for (const changesInFile of changeTracker.recordChanges().values()) {
for (const change of changesInFile) {
rewriteFn(change.start, change.removeLength ?? 0, change.text);
}
}
}
2 changes: 2 additions & 0 deletions packages/core/schematics/test/BUILD.bazel
Expand Up @@ -23,6 +23,8 @@ jasmine_node_test(
"//packages/core/schematics/migrations/block-template-entities:bundle",
"//packages/core/schematics/migrations/compiler-options",
"//packages/core/schematics/migrations/compiler-options:bundle",
"//packages/core/schematics/migrations/transfer-state",
"//packages/core/schematics/migrations/transfer-state:bundle",
"//packages/core/schematics/ng-generate/control-flow-migration",
"//packages/core/schematics/ng-generate/control-flow-migration:bundle",
"//packages/core/schematics/ng-generate/control-flow-migration:static_files",
Expand Down
119 changes: 119 additions & 0 deletions packages/core/schematics/test/transfer_state_spec.ts
@@ -0,0 +1,119 @@
/**
* @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('TransferState 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('migration-transfer-state', {}, 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);
});

afterEach(() => {
shx.cd(previousWorkingDir);
shx.rm('-r', tmpDirPath);
});

it('should change imports', async () => {
writeFile('/index.ts', `import { TransferState } from '@angular/platform-browser';`);

await runMigration();

const content = tree.readContent('/index.ts');
expect(content).toContain(`import { TransferState } from '@angular/core'`);
});

it('should change imports', async () => {
writeFile(
'/index.ts',
`import { TransferState, StateKey, makeStateKey } from '@angular/platform-browser';`);

await runMigration();

const content = tree.readContent('/index.ts');
expect(content).not.toContain(`@angular/platform-browser`);
expect(content).toContain(
`import { makeStateKey, StateKey, TransferState } from '@angular/core'`);
});

it('should change imports with existing core import', async () => {
writeFile('/index.ts', `
import { TransferState, StateKey, makeStateKey } from '@angular/platform-browser';
import { NgOnInit } from '@angular/core';
`);

await runMigration();

const content = tree.readContent('/index.ts');
expect(content).toContain(
`import { NgOnInit, makeStateKey, StateKey, TransferState } from '@angular/core'`);
});

it('should change imports but keep others ', async () => {
writeFile(
'/index.ts',
`import { TransferState, StateKey, makeStateKey, bootstrapApplication } from '@angular/platform-browser';`);

await runMigration();

const content = tree.readContent('/index.ts');
expect(content).toContain(
`import { makeStateKey, StateKey, TransferState } from '@angular/core'`);
expect(content).toContain(`import { bootstrapApplication } from '@angular/platform-browser'`);
});

it('should not change imports', async () => {
writeFile('/index.ts', `
import { TransferState } from '@not-angular/platform-browser'
`);

await runMigration();

const content = tree.readContent('/index.ts');
expect(content).toContain(`import { TransferState } from '@not-angular/platform-browser'`);
});
});
14 changes: 11 additions & 3 deletions packages/core/schematics/utils/change_tracker.ts
Expand Up @@ -99,8 +99,8 @@ export class ChangeTracker {
* @param moduleName Module from which the symbol is imported.
*/
addImport(
sourceFile: ts.SourceFile, symbolName: string, moduleName: string,
alias: string|null = null): ts.Expression {
sourceFile: ts.SourceFile, symbolName: string, moduleName: string, alias: string|null = null,
keepSymbolName = false): ts.Expression {
if (this._importRemapper) {
moduleName = this._importRemapper(moduleName, sourceFile.fileName);
}
Expand All @@ -110,7 +110,8 @@ export class ChangeTracker {
// paths will also cause TS to escape the forward slashes.
moduleName = normalizePath(moduleName);

return this._importManager.addImportToSourceFile(sourceFile, symbolName, moduleName, alias);
return this._importManager.addImportToSourceFile(
sourceFile, symbolName, moduleName, alias, false, keepSymbolName);
}

/**
Expand All @@ -122,6 +123,13 @@ export class ChangeTracker {
return this._changes;
}

/**
* Clear the tracked changes
*/
clearChanges(): void {
this._changes.clear();
}

/**
* Adds a change to a `ChangesByFile` map.
* @param file File that the change is associated with.
Expand Down

0 comments on commit 965ce5a

Please sign in to comment.