diff --git a/packages/schematics/angular/private/components.ts b/packages/schematics/angular/private/components.ts index 3365c0bb353d..d679f1cc35da 100644 --- a/packages/schematics/angular/private/components.ts +++ b/packages/schematics/angular/private/components.ts @@ -8,6 +8,8 @@ export { addModuleImportToStandaloneBootstrap, + addFunctionalProvidersToStandaloneBootstrap, + callsProvidersFunction, findBootstrapApplicationCall, importsProvidersFrom, } from './standalone'; diff --git a/packages/schematics/angular/private/standalone.ts b/packages/schematics/angular/private/standalone.ts index b171b8ae7597..e2df6ae8884b 100644 --- a/packages/schematics/angular/private/standalone.ts +++ b/packages/schematics/angular/private/standalone.ts @@ -6,11 +6,21 @@ * found in the LICENSE file at https://angular.io/license */ -import { SchematicsException, Tree } from '@angular-devkit/schematics'; +import { SchematicsException, Tree, UpdateRecorder } from '@angular-devkit/schematics'; +import { dirname, join } from 'path'; import ts from '../third_party/github.com/Microsoft/TypeScript/lib/typescript'; import { insertImport } from '../utility/ast-utils'; import { InsertChange } from '../utility/change'; +/** App config that was resolved to its source node. */ +interface ResolvedAppConfig { + /** Tree-relative path of the file containing the app config. */ + filePath: string; + + /** Node defining the app config. */ + node: ts.ObjectLiteralExpression; +} + /** * Checks whether the providers from a module are being imported in a `bootstrapApplication` call. * @param tree File tree of the project. @@ -18,19 +28,37 @@ import { InsertChange } from '../utility/change'; * @param className Class name of the module to search for. */ export function importsProvidersFrom(tree: Tree, filePath: string, className: string): boolean { - const sourceFile = ts.createSourceFile( - filePath, - tree.readText(filePath), - ts.ScriptTarget.Latest, - true, + const sourceFile = createSourceFile(tree, filePath); + const bootstrapCall = findBootstrapApplicationCall(sourceFile); + const appConfig = bootstrapCall ? findAppConfig(bootstrapCall, tree, filePath) : null; + const importProvidersFromCall = appConfig ? findImportProvidersFromCall(appConfig.node) : null; + + return !!importProvidersFromCall?.arguments.some( + (arg) => ts.isIdentifier(arg) && arg.text === className, ); +} +/** + * Checks whether a providers function is being called in a `bootstrapApplication` call. + * @param tree File tree of the project. + * @param filePath Path of the file in which to check. + * @param functionName Name of the function to search for. + */ +export function callsProvidersFunction( + tree: Tree, + filePath: string, + functionName: string, +): boolean { + const sourceFile = createSourceFile(tree, filePath); const bootstrapCall = findBootstrapApplicationCall(sourceFile); - const importProvidersFromCall = bootstrapCall ? findImportProvidersFromCall(bootstrapCall) : null; + const appConfig = bootstrapCall ? findAppConfig(bootstrapCall, tree, filePath) : null; + const providersLiteral = appConfig ? findProvidersLiteral(appConfig.node) : null; - return ( - !!importProvidersFromCall && - importProvidersFromCall.arguments.some((arg) => ts.isIdentifier(arg) && arg.text === className) + return !!providersLiteral?.elements.some( + (el) => + ts.isCallExpression(el) && + ts.isIdentifier(el.expression) && + el.expression.text === functionName, ); } @@ -47,86 +75,163 @@ export function addModuleImportToStandaloneBootstrap( moduleName: string, modulePath: string, ) { - const sourceFile = ts.createSourceFile( - filePath, - tree.readText(filePath), - ts.ScriptTarget.Latest, - true, - ); - + const sourceFile = createSourceFile(tree, filePath); const bootstrapCall = findBootstrapApplicationCall(sourceFile); + const addImports = (file: ts.SourceFile, recorder: UpdateRecorder) => { + const sourceText = file.getText(); + + [ + insertImport(file, sourceText, moduleName, modulePath), + insertImport(file, sourceText, 'importProvidersFrom', '@angular/core'), + ].forEach((change) => { + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } + }); + }; if (!bootstrapCall) { throw new SchematicsException(`Could not find bootstrapApplication call in ${filePath}`); } - const recorder = tree.beginUpdate(filePath); - const importCall = findImportProvidersFromCall(bootstrapCall); - const printer = ts.createPrinter(); - const sourceText = sourceFile.getText(); - - // Add imports to the module being added and `importProvidersFrom`. We don't - // have to worry about duplicates, because `insertImport` handles them. - [ - insertImport(sourceFile, sourceText, moduleName, modulePath), - insertImport(sourceFile, sourceText, 'importProvidersFrom', '@angular/core'), - ].forEach((change) => { - if (change instanceof InsertChange) { - recorder.insertLeft(change.pos, change.toAdd); - } - }); + const importProvidersCall = ts.factory.createCallExpression( + ts.factory.createIdentifier('importProvidersFrom'), + [], + [ts.factory.createIdentifier(moduleName)], + ); + + // If there's only one argument, we have to create a new object literal. + if (bootstrapCall.arguments.length === 1) { + const recorder = tree.beginUpdate(filePath); + addNewAppConfigToCall(bootstrapCall, importProvidersCall, recorder); + addImports(sourceFile, recorder); + tree.commitUpdate(recorder); + + return; + } + + // If the config is a `mergeApplicationProviders` call, add another config to it. + if (isMergeAppConfigCall(bootstrapCall.arguments[1])) { + const recorder = tree.beginUpdate(filePath); + addNewAppConfigToCall(bootstrapCall.arguments[1], importProvidersCall, recorder); + addImports(sourceFile, recorder); + tree.commitUpdate(recorder); + + return; + } + + // Otherwise attempt to merge into the current config. + const appConfig = findAppConfig(bootstrapCall, tree, filePath); + + if (!appConfig) { + throw new SchematicsException( + `Could not statically analyze config in bootstrapApplication call in ${filePath}`, + ); + } + + const { filePath: configFilePath, node: config } = appConfig; + const recorder = tree.beginUpdate(configFilePath); + const importCall = findImportProvidersFromCall(config); + + addImports(config.getSourceFile(), recorder); - // If there is an `importProvidersFrom` call already, reuse it. if (importCall) { + // If there's an `importProvidersFrom` call already, add the module to it. recorder.insertRight( importCall.arguments[importCall.arguments.length - 1].getEnd(), `, ${moduleName}`, ); - } else if (bootstrapCall.arguments.length === 1) { - // Otherwise if there is no options parameter to `bootstrapApplication`, - // create an object literal with a `providers` array and the import. - const newCall = ts.factory.updateCallExpression( - bootstrapCall, - bootstrapCall.expression, - bootstrapCall.typeArguments, - [ - ...bootstrapCall.arguments, - ts.factory.createObjectLiteralExpression([createProvidersAssignment(moduleName)], true), - ], - ); - - recorder.remove(bootstrapCall.getStart(), bootstrapCall.getWidth()); - recorder.insertRight( - bootstrapCall.getStart(), - printer.printNode(ts.EmitHint.Unspecified, newCall, sourceFile), - ); } else { - const providersLiteral = findProvidersLiteral(bootstrapCall); + const providersLiteral = findProvidersLiteral(config); if (providersLiteral) { // If there's a `providers` array, add the import to it. - const newProvidersLiteral = ts.factory.updateArrayLiteralExpression(providersLiteral, [ - ...providersLiteral.elements, - createImportProvidersFromCall(moduleName), - ]); - recorder.remove(providersLiteral.getStart(), providersLiteral.getWidth()); - recorder.insertRight( - providersLiteral.getStart(), - printer.printNode(ts.EmitHint.Unspecified, newProvidersLiteral, sourceFile), - ); + addElementToArray(providersLiteral, importProvidersCall, recorder); } else { // Otherwise add a `providers` array to the existing object literal. - const optionsLiteral = bootstrapCall.arguments[1] as ts.ObjectLiteralExpression; - const newOptionsLiteral = ts.factory.updateObjectLiteralExpression(optionsLiteral, [ - ...optionsLiteral.properties, - createProvidersAssignment(moduleName), - ]); - recorder.remove(optionsLiteral.getStart(), optionsLiteral.getWidth()); - recorder.insertRight( - optionsLiteral.getStart(), - printer.printNode(ts.EmitHint.Unspecified, newOptionsLiteral, sourceFile), - ); + addProvidersToObjectLiteral(config, importProvidersCall, recorder); + } + } + + tree.commitUpdate(recorder); +} + +/** + * Adds a providers function call to the `bootstrapApplication` call. + * @param tree File tree of the project. + * @param filePath Path to the file that should be updated. + * @param functionName Name of the function that should be called. + * @param importPath Path from which to import the function. + * @param args Arguments to use when calling the function. + */ +export function addFunctionalProvidersToStandaloneBootstrap( + tree: Tree, + filePath: string, + functionName: string, + importPath: string, + args: ts.Expression[] = [], +) { + const sourceFile = createSourceFile(tree, filePath); + const bootstrapCall = findBootstrapApplicationCall(sourceFile); + const addImports = (file: ts.SourceFile, recorder: UpdateRecorder) => { + const change = insertImport(file, file.getText(), functionName, importPath); + + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); } + }; + + if (!bootstrapCall) { + throw new SchematicsException(`Could not find bootstrapApplication call in ${filePath}`); + } + + const providersCall = ts.factory.createCallExpression( + ts.factory.createIdentifier(functionName), + undefined, + args, + ); + + // If there's only one argument, we have to create a new object literal. + if (bootstrapCall.arguments.length === 1) { + const recorder = tree.beginUpdate(filePath); + addNewAppConfigToCall(bootstrapCall, providersCall, recorder); + addImports(sourceFile, recorder); + tree.commitUpdate(recorder); + + return; + } + + // If the config is a `mergeApplicationProviders` call, add another config to it. + if (isMergeAppConfigCall(bootstrapCall.arguments[1])) { + const recorder = tree.beginUpdate(filePath); + addNewAppConfigToCall(bootstrapCall.arguments[1], providersCall, recorder); + addImports(sourceFile, recorder); + tree.commitUpdate(recorder); + + return; + } + + // Otherwise attempt to merge into the current config. + const appConfig = findAppConfig(bootstrapCall, tree, filePath); + + if (!appConfig) { + throw new SchematicsException( + `Could not statically analyze config in bootstrapApplication call in ${filePath}`, + ); + } + + const { filePath: configFilePath, node: config } = appConfig; + const recorder = tree.beginUpdate(configFilePath); + const providersLiteral = findProvidersLiteral(config); + + addImports(config.getSourceFile(), recorder); + + if (providersLiteral) { + // If there's a `providers` array, add the import to it. + addElementToArray(providersLiteral, providersCall, recorder); + } else { + // Otherwise add a `providers` array to the existing object literal. + addProvidersToObjectLiteral(config, providersCall, recorder); } tree.commitUpdate(recorder); @@ -140,17 +245,37 @@ export function findBootstrapApplicationCall(sourceFile: ts.SourceFile): ts.Call '@angular/platform-browser', ); - return localName ? findCall(sourceFile, localName) : null; + if (!localName) { + return null; + } + + let result: ts.CallExpression | null = null; + + sourceFile.forEachChild(function walk(node) { + if ( + ts.isCallExpression(node) && + ts.isIdentifier(node.expression) && + node.expression.text === localName + ) { + result = node; + } + + if (!result) { + node.forEachChild(walk); + } + }); + + return result; } -/** Find a call to `importProvidersFrom` within a `bootstrapApplication` call. */ -function findImportProvidersFromCall(bootstrapCall: ts.CallExpression): ts.CallExpression | null { - const providersLiteral = findProvidersLiteral(bootstrapCall); +/** Find a call to `importProvidersFrom` within an application config. */ +function findImportProvidersFromCall(config: ts.ObjectLiteralExpression): ts.CallExpression | null { const importProvidersName = findImportLocalName( - bootstrapCall.getSourceFile(), + config.getSourceFile(), 'importProvidersFrom', '@angular/core', ); + const providersLiteral = findProvidersLiteral(config); if (providersLiteral && importProvidersName) { for (const element of providersLiteral.elements) { @@ -168,22 +293,123 @@ function findImportProvidersFromCall(bootstrapCall: ts.CallExpression): ts.CallE return null; } -/** Finds the `providers` array literal within a `bootstrapApplication` call. */ -function findProvidersLiteral(bootstrapCall: ts.CallExpression): ts.ArrayLiteralExpression | null { - // The imports have to be in the second argument of - // the function which has to be an object literal. - if ( - bootstrapCall.arguments.length > 1 && - ts.isObjectLiteralExpression(bootstrapCall.arguments[1]) - ) { - for (const prop of bootstrapCall.arguments[1].properties) { - if ( - ts.isPropertyAssignment(prop) && - ts.isIdentifier(prop.name) && - prop.name.text === 'providers' && - ts.isArrayLiteralExpression(prop.initializer) - ) { - return prop.initializer; +/** Finds the `providers` array literal within an application config. */ +function findProvidersLiteral( + config: ts.ObjectLiteralExpression, +): ts.ArrayLiteralExpression | null { + for (const prop of config.properties) { + if ( + ts.isPropertyAssignment(prop) && + ts.isIdentifier(prop.name) && + prop.name.text === 'providers' && + ts.isArrayLiteralExpression(prop.initializer) + ) { + return prop.initializer; + } + } + + return null; +} + +/** + * Resolves the node that defines the app config from a bootstrap call. + * @param bootstrapCall Call for which to resolve the config. + * @param tree File tree of the project. + * @param filePath File path of the bootstrap call. + */ +function findAppConfig( + bootstrapCall: ts.CallExpression, + tree: Tree, + filePath: string, +): ResolvedAppConfig | null { + if (bootstrapCall.arguments.length > 1) { + const config = bootstrapCall.arguments[1]; + + if (ts.isObjectLiteralExpression(config)) { + return { filePath, node: config }; + } + + if (ts.isIdentifier(config)) { + return resolveAppConfigFromIdentifier(config, tree, filePath); + } + } + + return null; +} + +/** + * Resolves the app config from an identifier referring to it. + * @param identifier Identifier referring to the app config. + * @param tree File tree of the project. + * @param bootstapFilePath Path of the bootstrap call. + */ +function resolveAppConfigFromIdentifier( + identifier: ts.Identifier, + tree: Tree, + bootstapFilePath: string, +): ResolvedAppConfig | null { + const sourceFile = identifier.getSourceFile(); + + for (const node of sourceFile.statements) { + // Only look at relative imports. This will break if the app uses a path + // mapping to refer to the import, but in order to resolve those, we would + // need knowledge about the entire program. + if ( + !ts.isImportDeclaration(node) || + !node.importClause?.namedBindings || + !ts.isNamedImports(node.importClause.namedBindings) || + !ts.isStringLiteralLike(node.moduleSpecifier) || + !node.moduleSpecifier.text.startsWith('.') + ) { + continue; + } + + for (const specifier of node.importClause.namedBindings.elements) { + if (specifier.name.text !== identifier.text) { + continue; + } + + // Look for a variable with the imported name in the file. Note that ideally we would use + // the type checker to resolve this, but we can't because these utilities are set up to + // operate on individual files, not the entire program. + const filePath = join(dirname(bootstapFilePath), node.moduleSpecifier.text + '.ts'); + const importedSourceFile = createSourceFile(tree, filePath); + const resolvedVariable = findAppConfigFromVariableName( + importedSourceFile, + (specifier.propertyName || specifier.name).text, + ); + + if (resolvedVariable) { + return { filePath, node: resolvedVariable }; + } + } + } + + const variableInSameFile = findAppConfigFromVariableName(sourceFile, identifier.text); + + return variableInSameFile ? { filePath: bootstapFilePath, node: variableInSameFile } : null; +} + +/** + * Finds an app config within the top-level variables of a file. + * @param sourceFile File in which to search for the config. + * @param variableName Name of the variable containing the config. + */ +function findAppConfigFromVariableName( + sourceFile: ts.SourceFile, + variableName: string, +): ts.ObjectLiteralExpression | null { + for (const node of sourceFile.statements) { + if (ts.isVariableStatement(node)) { + for (const decl of node.declarationList.declarations) { + if ( + ts.isIdentifier(decl.name) && + decl.name.text === variableName && + decl.initializer && + ts.isObjectLiteralExpression(decl.initializer) + ) { + return decl.initializer; + } } } } @@ -233,45 +459,97 @@ function findImportLocalName( return null; } +/** Creates a source file from a file path within a project. */ +function createSourceFile(tree: Tree, filePath: string): ts.SourceFile { + return ts.createSourceFile(filePath, tree.readText(filePath), ts.ScriptTarget.Latest, true); +} + /** - * Finds a call to a function with a specific name. - * @param rootNode Node from which to start searching. - * @param name Name of the function to search for. + * Creates a new app config object literal and adds it to a call expression as an argument. + * @param call Call to which to add the config. + * @param expression Expression that should inserted into the new config. + * @param recorder Recorder to which to log the change. */ -function findCall(rootNode: ts.Node, name: string): ts.CallExpression | null { - let result: ts.CallExpression | null = null; - - rootNode.forEachChild(function walk(node) { - if ( - ts.isCallExpression(node) && - ts.isIdentifier(node.expression) && - node.expression.text === name - ) { - result = node; - } +function addNewAppConfigToCall( + call: ts.CallExpression, + expression: ts.Expression, + recorder: UpdateRecorder, +): void { + const newCall = ts.factory.updateCallExpression(call, call.expression, call.typeArguments, [ + ...call.arguments, + ts.factory.createObjectLiteralExpression( + [ + ts.factory.createPropertyAssignment( + 'providers', + ts.factory.createArrayLiteralExpression([expression]), + ), + ], + true, + ), + ]); - if (!result) { - node.forEachChild(walk); - } - }); + recorder.remove(call.getStart(), call.getWidth()); + recorder.insertRight( + call.getStart(), + ts.createPrinter().printNode(ts.EmitHint.Unspecified, newCall, call.getSourceFile()), + ); +} - return result; +/** + * Adds an element to an array literal expression. + * @param node Array to which to add the element. + * @param element Element to be added. + * @param recorder Recorder to which to log the change. + */ +function addElementToArray( + node: ts.ArrayLiteralExpression, + element: ts.Expression, + recorder: UpdateRecorder, +): void { + const newLiteral = ts.factory.updateArrayLiteralExpression(node, [...node.elements, element]); + recorder.remove(node.getStart(), node.getWidth()); + recorder.insertRight( + node.getStart(), + ts.createPrinter().printNode(ts.EmitHint.Unspecified, newLiteral, node.getSourceFile()), + ); } -/** Creates an `importProvidersFrom({{moduleName}})` call. */ -function createImportProvidersFromCall(moduleName: string): ts.CallExpression { - return ts.factory.createCallChain( - ts.factory.createIdentifier('importProvidersFrom'), - undefined, - undefined, - [ts.factory.createIdentifier(moduleName)], +/** + * Adds a `providers` property to an object literal. + * @param node Literal to which to add the `providers`. + * @param expression Provider that should be part of the generated `providers` array. + * @param recorder Recorder to which to log the change. + */ +function addProvidersToObjectLiteral( + node: ts.ObjectLiteralExpression, + expression: ts.Expression, + recorder: UpdateRecorder, +) { + const newOptionsLiteral = ts.factory.updateObjectLiteralExpression(node, [ + ...node.properties, + ts.factory.createPropertyAssignment( + 'providers', + ts.factory.createArrayLiteralExpression([expression]), + ), + ]); + recorder.remove(node.getStart(), node.getWidth()); + recorder.insertRight( + node.getStart(), + ts.createPrinter().printNode(ts.EmitHint.Unspecified, newOptionsLiteral, node.getSourceFile()), ); } -/** Creates a `providers: [importProvidersFrom({{moduleName}})]` property assignment. */ -function createProvidersAssignment(moduleName: string): ts.PropertyAssignment { - return ts.factory.createPropertyAssignment( - 'providers', - ts.factory.createArrayLiteralExpression([createImportProvidersFromCall(moduleName)]), +/** Checks whether a node is a call to `mergeApplicationConfig`. */ +function isMergeAppConfigCall(node: ts.Node): node is ts.CallExpression { + if (!ts.isCallExpression(node)) { + return false; + } + + const localName = findImportLocalName( + node.getSourceFile(), + 'mergeApplicationConfig', + '@angular/core', ); + + return !!localName && ts.isIdentifier(node.expression) && node.expression.text === localName; } diff --git a/packages/schematics/angular/private/standalone_spec.ts b/packages/schematics/angular/private/standalone_spec.ts index e268b47b36b7..9d0b6c9b2c92 100644 --- a/packages/schematics/angular/private/standalone_spec.ts +++ b/packages/schematics/angular/private/standalone_spec.ts @@ -9,7 +9,9 @@ import { EmptyTree } from '@angular-devkit/schematics'; import ts from '../third_party/github.com/Microsoft/TypeScript/lib/typescript'; import { + addFunctionalProvidersToStandaloneBootstrap, addModuleImportToStandaloneBootstrap, + callsProvidersFunction, findBootstrapApplicationCall, importsProvidersFrom, } from './standalone'; @@ -134,6 +136,41 @@ describe('standalone utilities', () => { }); }); + describe('callsProvidersFunction', () => { + it('should find that a bootstrapApplication call invokes a specific providers function', () => { + host.create( + '/test.ts', + ` + import { provideAnimations, bootstrapApplication } from '@angular/platform-browser'; + import { AppComponent } from './app.component'; + + bootstrapApplication(AppComponent, { + providers: [ + {provide: foo, useValue: 10}, + provideAnimations() + ] + }); + `, + ); + + expect(callsProvidersFunction(host, '/test.ts', 'provideAnimations')).toBe(true); + expect(callsProvidersFunction(host, '/test.ts', 'noopAnimations')).toBe(false); + }); + + it('should return false if there is no bootstrapApplication calls', () => { + host.create( + '/test.ts', + ` + import { AppComponent } from './app.component'; + + console.log(AppComponent); + `, + ); + + expect(callsProvidersFunction(host, '/test.ts', 'foo')).toBe(false); + }); + }); + describe('addModuleImportToStandaloneBootstrap', () => { it('should be able to add a module import to a simple `bootstrapApplication` call', () => { host.create( @@ -259,5 +296,363 @@ describe('standalone utilities', () => { addModuleImportToStandaloneBootstrap(host, '/test.ts', 'FooModule', '@foo/bar'); }).toThrowError(/Could not find bootstrapApplication call in \/test\.ts/); }); + + it('should add providers to an imported app config', () => { + host.create( + '/test.ts', + ` + import { bootstrapApplication } from '@angular/platform-browser'; + import { AppComponent } from './app.component'; + import { appConfig } from './app/app.config'; + + bootstrapApplication(AppComponent, appConfig); + `, + ); + + host.create( + '/app/app.config.ts', + ` + export const appConfig = { + providers: [{provide: 'foo', useValue: 'bar'}] + }; + `, + ); + + addModuleImportToStandaloneBootstrap(host, '/test.ts', 'FooModule', '@foo/bar'); + + const content = stripWhitespace(host.readText('/app/app.config.ts')); + + assertContains(content, `import {importProvidersFrom} from '@angular/core';`); + assertContains(content, `import {FooModule} from '@foo/bar';`); + assertContains( + content, + `export const appConfig = { + providers: [{provide: 'foo', useValue: 'bar'}, importProvidersFrom(FooModule)] + };`, + ); + }); + + it('should add providers to an app config imported through an alias', () => { + host.create( + '/test.ts', + ` + import { bootstrapApplication } from '@angular/platform-browser'; + import { AppComponent } from './app.component'; + import { appConfig as config } from './app/app.config'; + + bootstrapApplication(AppComponent, config); + `, + ); + + host.create( + '/app/app.config.ts', + ` + export const appConfig = { + providers: [{provide: 'foo', useValue: 'bar'}] + }; + `, + ); + + addModuleImportToStandaloneBootstrap(host, '/test.ts', 'FooModule', '@foo/bar'); + + const content = stripWhitespace(host.readText('/app/app.config.ts')); + + assertContains(content, `import {importProvidersFrom} from '@angular/core';`); + assertContains(content, `import {FooModule} from '@foo/bar';`); + assertContains( + content, + `export const appConfig = { + providers: [{provide: 'foo', useValue: 'bar'}, importProvidersFrom(FooModule)] + };`, + ); + }); + + it('should add providers to an app config coming from a variable in the same file', () => { + host.create( + '/test.ts', + ` + import { bootstrapApplication } from '@angular/platform-browser'; + import { AppComponent } from './app.component'; + + const appConfig = { + providers: [{provide: 'foo', useValue: 'bar'}] + }; + + bootstrapApplication(AppComponent, appConfig); + `, + ); + + addModuleImportToStandaloneBootstrap(host, '/test.ts', 'FooModule', '@foo/bar'); + + const content = stripWhitespace(host.readText('/test.ts')); + + assertContains(content, `import {importProvidersFrom} from '@angular/core';`); + assertContains(content, `import {FooModule} from '@foo/bar';`); + assertContains( + content, + `const appConfig = { + providers: [{provide: 'foo', useValue: 'bar'}, importProvidersFrom(FooModule)] + };`, + ); + }); + + it('should add a module import to a config using mergeApplicationConfig', () => { + host.create( + '/test.ts', + ` + import { mergeApplicationConfig } from '@angular/core'; + import { bootstrapApplication } from '@angular/platform-browser'; + import { AppComponent } from './app.component'; + + bootstrapApplication(AppComponent, mergeApplicationConfig(a, b)); + `, + ); + + addModuleImportToStandaloneBootstrap(host, '/test.ts', 'FooModule', '@foo/bar'); + + const content = stripWhitespace(host.readText('/test.ts')); + + assertContains( + content, + `import {mergeApplicationConfig, importProvidersFrom} from '@angular/core';`, + ); + assertContains(content, `import {FooModule} from '@foo/bar';`); + assertContains( + content, + `bootstrapApplication(AppComponent, mergeApplicationConfig(a, b, { + providers: [importProvidersFrom(FooModule)] + }));`, + ); + }); + }); + + describe('addFunctionalProvidersToStandaloneBootstrap', () => { + it('should be able to add a providers function to a simple `bootstrapApplication` call', () => { + host.create( + '/test.ts', + ` + import { bootstrapApplication } from '@angular/platform-browser'; + import { AppComponent } from './app.component'; + + bootstrapApplication(AppComponent); + `, + ); + + addFunctionalProvidersToStandaloneBootstrap(host, '/test.ts', 'provideFoo', '@foo/bar'); + + const content = stripWhitespace(host.readText('/test.ts')); + + assertContains(content, `import {provideFoo} from '@foo/bar';`); + assertContains(content, `bootstrapApplication(AppComponent, {providers: [provideFoo()]});`); + }); + + it('should be able to add a providers function to a `bootstrapApplication` call with an empty options object', () => { + host.create( + '/test.ts', + ` + import { bootstrapApplication } from '@angular/platform-browser'; + import { AppComponent } from './app.component'; + + bootstrapApplication(AppComponent, {}); + `, + ); + + addFunctionalProvidersToStandaloneBootstrap(host, '/test.ts', 'provideFoo', '@foo/bar'); + + const content = stripWhitespace(host.readText('/test.ts')); + + assertContains(content, `import {provideFoo} from '@foo/bar';`); + assertContains(content, `bootstrapApplication(AppComponent, {providers: [provideFoo()]});`); + }); + + it('should be able to add a providers function to a `bootstrapApplication` call with a pre-existing `providers` array', () => { + host.create( + '/test.ts', + ` + import { enableProdMode } from '@angular/core'; + import { bootstrapApplication } from '@angular/platform-browser'; + import { AppComponent } from './app.component'; + + enableProdMode(); + + bootstrapApplication(AppComponent, { + providers: [{provide: 'foo', useValue: 'bar'}] + }); + `, + ); + + addFunctionalProvidersToStandaloneBootstrap(host, '/test.ts', 'provideFoo', '@foo/bar'); + + const content = stripWhitespace(host.readText('/test.ts')); + + assertContains(content, `import {provideFoo} from '@foo/bar';`); + assertContains( + content, + `bootstrapApplication(AppComponent, { + providers: [{provide: 'foo', useValue: 'bar'}, provideFoo()] + });`, + ); + }); + + it('should throw if there is no `bootstrapModule` call', () => { + host.create( + '/test.ts', + ` + import { AppComponent } from './app.component'; + + console.log(AppComponent); + `, + ); + + expect(() => { + addFunctionalProvidersToStandaloneBootstrap(host, '/test.ts', 'provideFoo', '@foo/bar'); + }).toThrowError(/Could not find bootstrapApplication call in \/test\.ts/); + }); + + it('should add providers to an imported app config', () => { + host.create( + '/test.ts', + ` + import { bootstrapApplication } from '@angular/platform-browser'; + import { AppComponent } from './app.component'; + import { appConfig } from './app/app.config'; + + bootstrapApplication(AppComponent, appConfig); + `, + ); + + host.create( + '/app/app.config.ts', + ` + export const appConfig = { + providers: [{provide: 'foo', useValue: 'bar'}] + }; + `, + ); + + addFunctionalProvidersToStandaloneBootstrap(host, '/test.ts', 'provideFoo', '@foo/bar'); + + const content = stripWhitespace(host.readText('/app/app.config.ts')); + + assertContains(content, `import {provideFoo} from '@foo/bar';`); + assertContains( + content, + `export const appConfig = { + providers: [{provide: 'foo', useValue: 'bar'}, provideFoo()] + };`, + ); + }); + + it('should add providers to an app config imported through an alias', () => { + host.create( + '/test.ts', + ` + import { bootstrapApplication } from '@angular/platform-browser'; + import { AppComponent } from './app.component'; + import { appConfig as config } from './app/app.config'; + + bootstrapApplication(AppComponent, config); + `, + ); + + host.create( + '/app/app.config.ts', + ` + export const appConfig = { + providers: [{provide: 'foo', useValue: 'bar'}] + }; + `, + ); + + addFunctionalProvidersToStandaloneBootstrap(host, '/test.ts', 'provideFoo', '@foo/bar'); + + const content = stripWhitespace(host.readText('/app/app.config.ts')); + + assertContains(content, `import {provideFoo} from '@foo/bar';`); + assertContains( + content, + `export const appConfig = { + providers: [{provide: 'foo', useValue: 'bar'}, provideFoo()] + };`, + ); + }); + + it('should add providers to an app config from a variable in the same file', () => { + host.create( + '/test.ts', + ` + import { bootstrapApplication } from '@angular/platform-browser'; + import { AppComponent } from './app.component'; + + const appConfig = { + providers: [{provide: 'foo', useValue: 'bar'}] + }; + + bootstrapApplication(AppComponent, appConfig); + `, + ); + + addFunctionalProvidersToStandaloneBootstrap(host, '/test.ts', 'provideFoo', '@foo/bar'); + + const content = stripWhitespace(host.readText('/test.ts')); + + assertContains(content, `import {provideFoo} from '@foo/bar';`); + assertContains( + content, + `const appConfig = { + providers: [{provide: 'foo', useValue: 'bar'}, provideFoo()] + };`, + ); + }); + + it('should be able to add a providers function with specific arguments', () => { + host.create( + '/test.ts', + ` + import { bootstrapApplication } from '@angular/platform-browser'; + import { AppComponent } from './app.component'; + + bootstrapApplication(AppComponent); + `, + ); + + addFunctionalProvidersToStandaloneBootstrap(host, '/test.ts', 'provideFoo', '@foo/bar', [ + ts.factory.createNumericLiteral(1), + ts.factory.createStringLiteral('hello', true), + ]); + + const content = stripWhitespace(host.readText('/test.ts')); + + assertContains(content, `import {provideFoo} from '@foo/bar';`); + assertContains( + content, + `bootstrapApplication(AppComponent, {providers: [provideFoo(1, 'hello')]});`, + ); + }); + + it('should add a providers call to a config using mergeApplicationConfig', () => { + host.create( + '/test.ts', + ` + import { mergeApplicationConfig } from '@angular/core'; + import { bootstrapApplication } from '@angular/platform-browser'; + import { AppComponent } from './app.component'; + + bootstrapApplication(AppComponent, mergeApplicationConfig(a, b)); + `, + ); + + addFunctionalProvidersToStandaloneBootstrap(host, '/test.ts', 'provideFoo', '@foo/bar'); + + const content = stripWhitespace(host.readText('/test.ts')); + + assertContains(content, `import {provideFoo} from '@foo/bar';`); + assertContains( + content, + `bootstrapApplication(AppComponent, mergeApplicationConfig(a, b, { + providers: [provideFoo()] + }));`, + ); + }); }); });