Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/core/schematics/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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",
Expand Down
8 changes: 7 additions & 1 deletion packages/core/schematics/migrations.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
{
"schematics": {}
"schematics": {
"standalone-false": {
Comment thread
AndrewKushnir marked this conversation as resolved.
Outdated
"version": "19.0.0",
"description": "Updates non-standalone Directives, Component and Pipes to standalone:false",
"factory": "./bundles/standalone-false#migrate"
}
}
}
21 changes: 21 additions & 0 deletions packages/core/schematics/migrations/standalone-false/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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",
],
)
58 changes: 58 additions & 0 deletions packages/core/schematics/migrations/standalone-false/index.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
126 changes: 126 additions & 0 deletions packages/core/schematics/migrations/standalone-false/migration.ts
Original file line number Diff line number Diff line change
@@ -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)) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want/need this to work for import * as ng from '@angular/core'; @ng.Component() usages?

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<ts.ObjectLiteralElementLike>) {
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;
}
Loading