@@ -16,7 +16,7 @@ import {getAngularDecorators} from '../../utils/ng_decorators';
1616import { closestNode } from '../../utils/typescript/nodes' ;
1717
1818import { convertNgModuleDeclarationToStandalone } from './to-standalone' ;
19- import { ChangeTracker , createLanguageService , findClassDeclaration , findLiteralProperty , getNodeLookup , getRelativeImportPath , NamedClassDeclaration , NodeLookup , offsetsToNodes } from './util' ;
19+ import { ChangeTracker , createLanguageService , findClassDeclaration , findLiteralProperty , getNodeLookup , getRelativeImportPath , NamedClassDeclaration , NodeLookup , offsetsToNodes , UniqueItemTracker } from './util' ;
2020
2121/** Information extracted from a `bootstrapModule` call necessary to migrate it. */
2222interface BootstrapCallAnalysis {
@@ -281,19 +281,27 @@ function migrateImportsForBootstrapCall(
281281 }
282282
283283 for ( const element of imports . initializer . elements ) {
284- // If the reference is to a `RouterModule.forRoot` call with
285- // one argument, we can migrate to the new `provideRouter` API.
286- if ( ts . isCallExpression ( element ) && element . arguments . length === 1 &&
287- ts . isPropertyAccessExpression ( element . expression ) &&
288- element . expression . name . text === 'forRoot' &&
284+ // If the reference is to a `RouterModule.forRoot` call, we can try to migrate it.
285+ if ( ts . isCallExpression ( element ) && ts . isPropertyAccessExpression ( element . expression ) &&
286+ element . arguments . length > 0 && element . expression . name . text === 'forRoot' &&
289287 isClassReferenceInModule (
290288 element . expression . expression , 'RouterModule' , '@angular/router' , typeChecker ) ) {
291- providersInNewCall . push ( ts . factory . createCallExpression (
292- tracker . addImport ( sourceFile , 'provideRouter' , '@angular/router' ) , [ ] ,
293- element . arguments ) ) ;
294- addNodesToCopy (
295- sourceFile , element . arguments [ 0 ] , nodeLookup , tracker , nodesToCopy , languageService ) ;
296- continue ;
289+ const options = element . arguments [ 1 ] as ts . Expression | undefined ;
290+ const features = options ? getRouterModuleForRootFeatures ( sourceFile , options , tracker ) : [ ] ;
291+
292+ // If the features come back as null, it means that the router
293+ // has a configuration that can't be migrated automatically.
294+ if ( features !== null ) {
295+ providersInNewCall . push ( ts . factory . createCallExpression (
296+ tracker . addImport ( sourceFile , 'provideRouter' , '@angular/router' ) , [ ] ,
297+ [ element . arguments [ 0 ] , ...features ] ) ) ;
298+ addNodesToCopy (
299+ sourceFile , element . arguments [ 0 ] , nodeLookup , tracker , nodesToCopy , languageService ) ;
300+ if ( options ) {
301+ addNodesToCopy ( sourceFile , options , nodeLookup , tracker , nodesToCopy , languageService ) ;
302+ }
303+ continue ;
304+ }
297305 }
298306
299307 if ( ts . isIdentifier ( element ) ) {
@@ -335,6 +343,111 @@ function migrateImportsForBootstrapCall(
335343 }
336344}
337345
346+ /**
347+ * Generates the call expressions that can be used to replace the options
348+ * object that is passed into a `RouterModule.forRoot` call.
349+ * @param sourceFile File that the `forRoot` call is coming from.
350+ * @param options Node that is passed as the second argument to the `forRoot` call.
351+ * @param tracker Tracker in which to track imports that need to be inserted.
352+ * @returns Null if the options can't be migrated, otherwise an array of call expressions.
353+ */
354+ function getRouterModuleForRootFeatures (
355+ sourceFile : ts . SourceFile , options : ts . Expression , tracker : ChangeTracker ) : ts . CallExpression [ ] |
356+ null {
357+ // Options that aren't a static object literal can't be migrated.
358+ if ( ! ts . isObjectLiteralExpression ( options ) ) {
359+ return null ;
360+ }
361+
362+ const featureExpressions : ts . CallExpression [ ] = [ ] ;
363+ const configOptions : ts . PropertyAssignment [ ] = [ ] ;
364+ const inMemoryScrollingOptions : ts . PropertyAssignment [ ] = [ ] ;
365+ const features = new UniqueItemTracker < string , ts . Expression | null > ( ) ;
366+
367+ for ( const prop of options . properties ) {
368+ // We can't migrate options that we can't easily analyze.
369+ if ( ! ts . isPropertyAssignment ( prop ) ||
370+ ( ! ts . isIdentifier ( prop . name ) && ! ts . isStringLiteralLike ( prop . name ) ) ) {
371+ return null ;
372+ }
373+
374+ switch ( prop . name . text ) {
375+ // `preloadingStrategy` maps to the `withPreloading` function.
376+ case 'preloadingStrategy' :
377+ features . track ( 'withPreloading' , prop . initializer ) ;
378+ break ;
379+
380+ // `enableTracing: true` maps to the `withDebugTracing` feature.
381+ case 'enableTracing' :
382+ if ( prop . initializer . kind === ts . SyntaxKind . TrueKeyword ) {
383+ features . track ( 'withDebugTracing' , null ) ;
384+ }
385+ break ;
386+
387+ // `initialNavigation: 'enabled'` and `initialNavigation: 'enabledBlocking'` map to the
388+ // `withEnabledBlockingInitialNavigation` feature, while `initialNavigation: 'disabled'` maps
389+ // to the `withDisabledInitialNavigation` feature.
390+ case 'initialNavigation' :
391+ if ( ! ts . isStringLiteralLike ( prop . initializer ) ) {
392+ return null ;
393+ }
394+ if ( prop . initializer . text === 'enabledBlocking' || prop . initializer . text === 'enabled' ) {
395+ features . track ( 'withEnabledBlockingInitialNavigation' , null ) ;
396+ } else if ( prop . initializer . text === 'disabled' ) {
397+ features . track ( 'withDisabledInitialNavigation' , null ) ;
398+ }
399+ break ;
400+
401+ // `useHash: true` maps to the `withHashLocation` feature.
402+ case 'useHash' :
403+ if ( prop . initializer . kind === ts . SyntaxKind . TrueKeyword ) {
404+ features . track ( 'withHashLocation' , null ) ;
405+ }
406+ break ;
407+
408+ // `errorHandler` maps to the `withNavigationErrorHandler` feature.
409+ case 'errorHandler' :
410+ features . track ( 'withNavigationErrorHandler' , prop . initializer ) ;
411+ break ;
412+
413+ // `anchorScrolling` and `scrollPositionRestoration` arguments have to be combined into an
414+ // object literal that is passed into the `withInMemoryScrolling` feature.
415+ case 'anchorScrolling' :
416+ case 'scrollPositionRestoration' :
417+ inMemoryScrollingOptions . push ( prop ) ;
418+ break ;
419+
420+ // All remaining properties can be passed through the `withRouterConfig` feature.
421+ default :
422+ configOptions . push ( prop ) ;
423+ break ;
424+ }
425+ }
426+
427+ if ( inMemoryScrollingOptions . length > 0 ) {
428+ features . track (
429+ 'withInMemoryScrolling' ,
430+ ts . factory . createObjectLiteralExpression ( inMemoryScrollingOptions ) ) ;
431+ }
432+
433+ if ( configOptions . length > 0 ) {
434+ features . track ( 'withRouterConfig' , ts . factory . createObjectLiteralExpression ( configOptions ) ) ;
435+ }
436+
437+ for ( const [ feature , featureArgs ] of features . getEntries ( ) ) {
438+ const callArgs : ts . Expression [ ] = [ ] ;
439+ featureArgs . forEach ( arg => {
440+ if ( arg !== null ) {
441+ callArgs . push ( arg ) ;
442+ }
443+ } ) ;
444+ featureExpressions . push ( ts . factory . createCallExpression (
445+ tracker . addImport ( sourceFile , feature , '@angular/router' ) , [ ] , callArgs ) ) ;
446+ }
447+
448+ return featureExpressions ;
449+ }
450+
338451/**
339452 * Finds all the nodes that are referenced inside a root node and would need to be copied into a
340453 * new file in order for the node to compile, and tracks them.
0 commit comments