Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
24 changes: 14 additions & 10 deletions packages/core/schematics/ng-generate/standalone-migration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,18 +61,22 @@ export default function(options: Options): Rule {

function standaloneMigration(
tree: Tree, tsconfigPath: string, basePath: string, pathToMigrate: string,
options: Options): number {
if (options.path.startsWith('..')) {
schematicOptions: Options): number {
if (schematicOptions.path.startsWith('..')) {
throw new SchematicsException(
'Cannot run standalone migration outside of the current project.');
}

const {host, rootNames} = createProgramOptions(tree, tsconfigPath, basePath);
const program = createProgram({
rootNames,
host,
options: {_enableTemplateTypeChecker: true, compileNonExportedClasses: true}
}) as NgtscProgram;
const {host, options, rootNames} = createProgramOptions(
tree, tsconfigPath, basePath, undefined, undefined,
{
_enableTemplateTypeChecker: true, // Required for the template type checker to work.
compileNonExportedClasses: true, // We want to migrate non-exported classes too.
// Avoid checking libraries to speed up the migration.
skipLibCheck: true,
skipDefaultLibCheck: true,
});
const program = createProgram({rootNames, host, options}) as NgtscProgram;
const printer = ts.createPrinter();

if (existsSync(pathToMigrate) && !statSync(pathToMigrate).isDirectory()) {
Expand All @@ -92,11 +96,11 @@ function standaloneMigration(
let pendingChanges: ChangesByFile;
let filesToRemove: Set<ts.SourceFile>|null = null;

if (options.mode === MigrationMode.pruneModules) {
if (schematicOptions.mode === MigrationMode.pruneModules) {
const result = pruneNgModules(program, host, basePath, rootNames, sourceFiles, printer);
pendingChanges = result.pendingChanges;
filesToRemove = result.filesToRemove;
} else if (options.mode === MigrationMode.standaloneBootstrap) {
} else if (schematicOptions.mode === MigrationMode.standaloneBootstrap) {
pendingChanges =
toStandaloneBootstrap(program, host, basePath, rootNames, sourceFiles, printer);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import ts from 'typescript';
import {getAngularDecorators} from '../../utils/ng_decorators';
import {closestNode} from '../../utils/typescript/nodes';

import {convertNgModuleDeclarationToStandalone} from './to-standalone';
import {convertNgModuleDeclarationToStandalone, extractDeclarationsFromModule, findTestObjectsToMigrate, migrateTestDeclarations} from './to-standalone';
import {ChangeTracker, createLanguageService, findClassDeclaration, findLiteralProperty, getNodeLookup, getRelativeImportPath, ImportRemapper, NamedClassDeclaration, NodeLookup, offsetsToNodes, UniqueItemTracker} from './util';

/** Information extracted from a `bootstrapModule` call necessary to migrate it. */
Expand All @@ -28,6 +28,8 @@ interface BootstrapCallAnalysis {
metadata: ts.ObjectLiteralExpression;
/** Component that the module is bootstrapping. */
component: NamedClassDeclaration;
/** Classes declared by the bootstrapped module. */
declarations: Reference<ts.ClassDeclaration>[];
}

export function toStandaloneBootstrap(
Expand All @@ -38,24 +40,39 @@ export function toStandaloneBootstrap(
const templateTypeChecker = program.compiler.getTemplateTypeChecker();
const languageService = createLanguageService(program, host, rootFileNames, basePath);
const bootstrapCalls: BootstrapCallAnalysis[] = [];

sourceFiles.forEach(function walk(node: ts.Node) {
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) &&
node.expression.name.text === 'bootstrapModule' &&
isClassReferenceInAngularModule(node.expression, 'PlatformRef', 'core', typeChecker)) {
const call = analyzeBootstrapCall(node, typeChecker);

if (call) {
bootstrapCalls.push(call);
const testObjects: ts.ObjectLiteralExpression[] = [];
const allDeclarations: Reference<ts.ClassDeclaration>[] = [];

for (const sourceFile of sourceFiles) {
sourceFile.forEachChild(function walk(node) {
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) &&
node.expression.name.text === 'bootstrapModule' &&
isClassReferenceInAngularModule(node.expression, 'PlatformRef', 'core', typeChecker)) {
const call = analyzeBootstrapCall(node, typeChecker, templateTypeChecker);

if (call) {
bootstrapCalls.push(call);
}
}
}
node.forEachChild(walk);
});
node.forEachChild(walk);
});

testObjects.push(...findTestObjectsToMigrate(sourceFile, typeChecker));
}

for (const call of bootstrapCalls) {
migrateBootstrapCall(call, tracker, languageService, typeChecker, templateTypeChecker, printer);
allDeclarations.push(...call.declarations);
migrateBootstrapCall(call, tracker, languageService, typeChecker, printer);
}

// The previous migrations explicitly skip over bootstrapped
// declarations so we have to migrate them now.
for (const declaration of allDeclarations) {
convertNgModuleDeclarationToStandalone(
declaration, allDeclarations, tracker, templateTypeChecker);
}

migrateTestDeclarations(testObjects, allDeclarations, tracker, templateTypeChecker, typeChecker);
return tracker.recordChanges();
}

Expand All @@ -64,9 +81,11 @@ export function toStandaloneBootstrap(
* necessary to convert it to `bootstrapApplication`.
* @param call Call to be analyzed.
* @param typeChecker
* @param templateTypeChecker
*/
function analyzeBootstrapCall(
call: ts.CallExpression, typeChecker: ts.TypeChecker): BootstrapCallAnalysis|null {
call: ts.CallExpression, typeChecker: ts.TypeChecker,
templateTypeChecker: TemplateTypeChecker): BootstrapCallAnalysis|null {
if (call.arguments.length === 0 || !ts.isIdentifier(call.arguments[0])) {
return null;
}
Expand Down Expand Up @@ -98,7 +117,13 @@ function analyzeBootstrapCall(
const component = findClassDeclaration(bootstrapProp.initializer.elements[0], typeChecker);

if (component && component.name && ts.isIdentifier(component.name)) {
return {module: declaration, metadata, component: component as NamedClassDeclaration, call};
return {
module: declaration,
metadata,
component: component as NamedClassDeclaration,
call,
declarations: extractDeclarationsFromModule(declaration, templateTypeChecker)
};
}

return null;
Expand All @@ -110,12 +135,11 @@ function analyzeBootstrapCall(
* @param tracker Tracker in which to register the changes.
* @param languageService
* @param typeChecker
* @param templateTypeChecker
* @param printer
*/
function migrateBootstrapCall(
analysis: BootstrapCallAnalysis, tracker: ChangeTracker, languageService: ts.LanguageService,
typeChecker: ts.TypeChecker, templateTypeChecker: TemplateTypeChecker, printer: ts.Printer) {
typeChecker: ts.TypeChecker, printer: ts.Printer) {
const sourceFile = analysis.call.getSourceFile();
const moduleSourceFile = analysis.metadata.getSourceFile();
const providers = findLiteralProperty(analysis.metadata, 'providers');
Expand All @@ -125,10 +149,6 @@ function migrateBootstrapCall(
const moduleImportsInNewCall: ts.Expression[] = [];
let nodeLookup: NodeLookup|null = null;

// The previous migrations explicitly skip over modules that bootstrap a
// component so we have to convert it as a part of this migration instead.
convertBootstrappedModuleToStandalone(analysis, tracker, templateTypeChecker);

// We can't reuse the module pruning logic, because we would have to recreate the entire program.
// Instead we comment out the module's metadata so that the user doesn't get compilation errors
// for the classes that are left in the `declarations` array. This should allow the app to
Expand Down Expand Up @@ -223,40 +243,6 @@ function replaceBootstrapCallExpression(
undefined, analysis.metadata.getSourceFile());
}

/**
* Converts the declarations of a bootstrapped module to standalone. These declarations are
* skipped in the `convert-to-standalone` phase so they need to be migrated when converting
* to `bootstrapApplication`.
* @param analysis Result of the analysis of the NgModule.
* @param tracker
* @param templateTypeChecker
*/
function convertBootstrappedModuleToStandalone(
analysis: BootstrapCallAnalysis, tracker: ChangeTracker,
templateTypeChecker: TemplateTypeChecker) {
const metadata = templateTypeChecker.getNgModuleMetadata(analysis.module);

if (!metadata) {
throw new Error(`Cannot resolve NgModule metadata for class ${
analysis.module.name?.getText()}. Cannot switch to standalone bootstrap API.`);
}

const classDeclarations =
metadata.declarations.filter(decl => ts.isClassDeclaration(decl.node)) as
Reference<ts.ClassDeclaration>[];

if (!classDeclarations.some(decl => decl.node === analysis.component)) {
throw new Error(`Bootstrapped component is not in the declarations array of NgModule ${
analysis.module.name?.getText()}. Cannot switch to standalone bootstrap API.`);
}

for (const decl of classDeclarations) {
if (ts.isClassDeclaration(decl.node)) {
convertNgModuleDeclarationToStandalone(decl, classDeclarations, tracker, templateTypeChecker);
}
}
}

/**
* Processes the `imports` of an NgModule so that they can be used in the `bootstrapApplication`
* call inside of a different file.
Expand Down
Loading