diff --git a/packages/schematics/angular/app-shell/index.ts b/packages/schematics/angular/app-shell/index.ts index bc472c3dbf45..c7e85c13a000 100644 --- a/packages/schematics/angular/app-shell/index.ts +++ b/packages/schematics/angular/app-shell/index.ts @@ -15,7 +15,6 @@ import { noop, schematic, } from '@angular-devkit/schematics'; -import { findBootstrapApplicationCall } from '../private/standalone'; import * as ts from '../third_party/github.com/Microsoft/TypeScript/lib/typescript'; import { addImportToModule, @@ -29,7 +28,8 @@ import { } from '../utility/ast-utils'; import { applyToUpdateRecorder } from '../utility/change'; import { getAppModulePath, isStandaloneApp } from '../utility/ng-ast-utils'; -import { getMainFilePath } from '../utility/standalone/util'; +import { findAppConfig } from '../utility/standalone/app_config'; +import { findBootstrapApplicationCall, getMainFilePath } from '../utility/standalone/util'; import { getWorkspace, updateWorkspace } from '../utility/workspace'; import { Builders } from '../utility/workspace-models'; import { Schema as AppShellOptions } from './schema'; @@ -89,18 +89,16 @@ function getComponentTemplate(host: Tree, compPath: string, tmplInfo: TemplateIn } function getBootstrapComponentPath(host: Tree, mainPath: string): string { - const mainSource = getSourceFile(host, mainPath); - const bootstrapAppCall = findBootstrapApplicationCall(mainSource); - let bootstrappingFilePath: string; let bootstrappingSource: ts.SourceFile; let componentName: string; - if (bootstrapAppCall) { + if (isStandaloneApp(host, mainPath)) { // Standalone Application - componentName = bootstrapAppCall.arguments[0].getText(); + const bootstrapCall = findBootstrapApplicationCall(host, mainPath); + componentName = bootstrapCall.arguments[0].getText(); bootstrappingFilePath = mainPath; - bootstrappingSource = mainSource; + bootstrappingSource = getSourceFile(host, mainPath); } else { // NgModule Application const modulePath = getAppModulePath(host, mainPath); diff --git a/packages/schematics/angular/package.json b/packages/schematics/angular/package.json index d1f498f98e1b..a929afdcd285 100644 --- a/packages/schematics/angular/package.json +++ b/packages/schematics/angular/package.json @@ -13,8 +13,7 @@ "./utility": "./utility/index.js", "./utility/*": "./utility/*.js", "./migrations/migration-collection.json": "./migrations/migration-collection.json", - "./*": "./*.js", - "./private/components": "./private/components.js" + "./*": "./*.js" }, "schematics": "./collection.json", "dependencies": { diff --git a/packages/schematics/angular/private/components.ts b/packages/schematics/angular/private/components.ts deleted file mode 100644 index d679f1cc35da..000000000000 --- a/packages/schematics/angular/private/components.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @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 - */ - -export { - addModuleImportToStandaloneBootstrap, - addFunctionalProvidersToStandaloneBootstrap, - callsProvidersFunction, - findBootstrapApplicationCall, - importsProvidersFrom, -} from './standalone'; diff --git a/packages/schematics/angular/private/standalone.ts b/packages/schematics/angular/private/standalone.ts deleted file mode 100644 index 33ea5e152f11..000000000000 --- a/packages/schematics/angular/private/standalone.ts +++ /dev/null @@ -1,570 +0,0 @@ -/** - * @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 { 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. - * @param filePath Path of the file in which to check. - * @param className Class name of the module to search for. - * @deprecated Private utility that will be removed. Use `addRootImport` or `addRootProvider` from - * `@schematics/angular/utility` instead. - */ -export function importsProvidersFrom(tree: Tree, filePath: string, className: string): boolean { - 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. - * @deprecated Private utility that will be removed. Use `addRootImport` or `addRootProvider` from - * `@schematics/angular/utility` instead. - */ -export function callsProvidersFunction( - tree: Tree, - filePath: string, - functionName: string, -): boolean { - const sourceFile = createSourceFile(tree, filePath); - const bootstrapCall = findBootstrapApplicationCall(sourceFile); - const appConfig = bootstrapCall ? findAppConfig(bootstrapCall, tree, filePath) : null; - const providersLiteral = appConfig ? findProvidersLiteral(appConfig.node) : null; - - return !!providersLiteral?.elements.some( - (el) => - ts.isCallExpression(el) && - ts.isIdentifier(el.expression) && - el.expression.text === functionName, - ); -} - -/** - * Adds an `importProvidersFrom` call to the `bootstrapApplication` call. - * @param tree File tree of the project. - * @param filePath Path to the file that should be updated. - * @param moduleName Name of the module that should be imported. - * @param modulePath Path from which to import the module. - * @deprecated Private utility that will be removed. Use `addRootImport` or `addRootProvider` from - * `@schematics/angular/utility` instead. - */ -export function addModuleImportToStandaloneBootstrap( - tree: Tree, - filePath: string, - moduleName: string, - modulePath: string, -) { - 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 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 (importCall) { - // If there's an `importProvidersFrom` call already, add the module to it. - recorder.insertRight( - importCall.arguments[importCall.arguments.length - 1].getEnd(), - `, ${moduleName}`, - ); - } else { - const providersLiteral = findProvidersLiteral(config); - - if (providersLiteral) { - // If there's a `providers` array, add the import to it. - addElementToArray(providersLiteral, importProvidersCall, recorder); - } else { - // Otherwise add a `providers` array to the existing object literal. - 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. - * @return The file path that the provider was added to. - * @deprecated Private utility that will be removed. Use `addRootImport` or `addRootProvider` from - * `@schematics/angular/utility` instead. - */ -export function addFunctionalProvidersToStandaloneBootstrap( - tree: Tree, - filePath: string, - functionName: string, - importPath: string, - args: ts.Expression[] = [], -): string { - 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 filePath; - } - - // 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 filePath; - } - - // 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); - - return configFilePath; -} - -/** - * Finds the call to `bootstrapApplication` within a file. - * @deprecated Private utility that will be removed. Use `addRootImport` or `addRootProvider` from - * `@schematics/angular/utility` instead. - */ -export function findBootstrapApplicationCall(sourceFile: ts.SourceFile): ts.CallExpression | null { - const localName = findImportLocalName( - sourceFile, - 'bootstrapApplication', - '@angular/platform-browser', - ); - - 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 an application config. */ -function findImportProvidersFromCall(config: ts.ObjectLiteralExpression): ts.CallExpression | null { - const importProvidersName = findImportLocalName( - config.getSourceFile(), - 'importProvidersFrom', - '@angular/core', - ); - const providersLiteral = findProvidersLiteral(config); - - if (providersLiteral && importProvidersName) { - for (const element of providersLiteral.elements) { - // Look for an array element that calls the `importProvidersFrom` function. - if ( - ts.isCallExpression(element) && - ts.isIdentifier(element.expression) && - element.expression.text === importProvidersName - ) { - return element; - } - } - } - - return null; -} - -/** 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; - } - } - } - } - - return null; -} - -/** - * Finds the local name of an imported symbol. Could be the symbol name itself or its alias. - * @param sourceFile File within which to search for the import. - * @param name Actual name of the import, not its local alias. - * @param moduleName Name of the module from which the symbol is imported. - */ -function findImportLocalName( - sourceFile: ts.SourceFile, - name: string, - moduleName: string, -): string | null { - for (const node of sourceFile.statements) { - // Only look for top-level imports. - if ( - !ts.isImportDeclaration(node) || - !ts.isStringLiteral(node.moduleSpecifier) || - node.moduleSpecifier.text !== moduleName - ) { - continue; - } - - // Filter out imports that don't have the right shape. - if ( - !node.importClause || - !node.importClause.namedBindings || - !ts.isNamedImports(node.importClause.namedBindings) - ) { - continue; - } - - // Look through the elements of the declaration for the specific import. - for (const element of node.importClause.namedBindings.elements) { - if ((element.propertyName || element.name).text === name) { - // The local name is always in `name`. - return element.name.text; - } - } - } - - 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); -} - -/** - * 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 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, - ), - ]); - - recorder.remove(call.getStart(), call.getWidth()); - recorder.insertRight( - call.getStart(), - ts.createPrinter().printNode(ts.EmitHint.Unspecified, newCall, call.getSourceFile()), - ); -} - -/** - * 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()), - ); -} - -/** - * 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()), - ); -} - -/** 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 deleted file mode 100644 index 9d0b6c9b2c92..000000000000 --- a/packages/schematics/angular/private/standalone_spec.ts +++ /dev/null @@ -1,658 +0,0 @@ -/** - * @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 { EmptyTree } from '@angular-devkit/schematics'; -import ts from '../third_party/github.com/Microsoft/TypeScript/lib/typescript'; -import { - addFunctionalProvidersToStandaloneBootstrap, - addModuleImportToStandaloneBootstrap, - callsProvidersFunction, - findBootstrapApplicationCall, - importsProvidersFrom, -} from './standalone'; - -describe('standalone utilities', () => { - let host: EmptyTree; - - beforeEach(() => { - host = new EmptyTree(); - }); - - function getSourceFileFrom(path: string) { - return ts.createSourceFile(path, host.readText(path), ts.ScriptTarget.Latest, true); - } - - function stripWhitespace(str: string) { - return str.replace(/\s/g, ''); - } - - function assertContains(source: string, targetString: string) { - expect(stripWhitespace(source)).toContain(stripWhitespace(targetString)); - } - - describe('findBootstrapApplicationCall', () => { - it('should find a call to `bootstrapApplication`', () => { - host.create( - '/test.ts', - ` - import { BrowserModule, bootstrapApplication } from '@angular/platform-browser'; - import { AppComponent } from './app.component'; - - bootstrapApplication(AppComponent, { - providers: [importProvidersFrom(BrowserModule)] - }); - `, - ); - - expect(findBootstrapApplicationCall(getSourceFileFrom('/test.ts'))).toBeTruthy(); - }); - - it('should find an aliased call to `bootstrapApplication`', () => { - host.create( - '/test.ts', - ` - import { BrowserModule, bootstrapApplication as boot } from '@angular/platform-browser'; - import { AppComponent } from './app.component'; - - boot(AppComponent, { - providers: [importProvidersFrom(BrowserModule)] - }); - `, - ); - - expect(findBootstrapApplicationCall(getSourceFileFrom('/test.ts'))).toBeTruthy(); - }); - - it('should return null if there are no bootstrapApplication calls', () => { - host.create( - '/test.ts', - ` - import { AppComponent } from './app.component'; - - console.log(AppComponent); - `, - ); - - expect(findBootstrapApplicationCall(getSourceFileFrom('/test.ts'))).toBeNull(); - }); - }); - - describe('importsProvidersFrom', () => { - it('should find that a bootstrapApplication call imports providers from a module', () => { - host.create( - '/test.ts', - ` - import { importProvidersFrom } from '@angular/core'; - import { BrowserModule, bootstrapApplication } from '@angular/platform-browser'; - import { AppComponent } from './app.component'; - - bootstrapApplication(AppComponent, { - providers: [ - {provide: foo, useValue: 10}, - importProvidersFrom(BrowserModule) - ] - }); - `, - ); - - expect(importsProvidersFrom(host, '/test.ts', 'BrowserModule')).toBe(true); - expect(importsProvidersFrom(host, '/test.ts', 'FooModule')).toBe(false); - }); - - it('should find that a bootstrapApplication call imports providers from a module if importProvidersFrom is aliased', () => { - host.create( - '/test.ts', - ` - import { importProvidersFrom as imp } from '@angular/core'; - import { BrowserModule, bootstrapApplication } from '@angular/platform-browser'; - import { AppComponent } from './app.component'; - - bootstrapApplication(AppComponent, { - providers: [imp(BrowserModule)] - }); - `, - ); - - expect(importsProvidersFrom(host, '/test.ts', 'BrowserModule')).toBe(true); - expect(importsProvidersFrom(host, '/test.ts', 'FooModule')).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(importsProvidersFrom(host, '/test.ts', 'FooModule')).toBe(false); - }); - }); - - 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( - '/test.ts', - ` - import { bootstrapApplication } from '@angular/platform-browser'; - import { AppComponent } from './app.component'; - - bootstrapApplication(AppComponent); - `, - ); - - 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, - `bootstrapApplication(AppComponent, {providers: [importProvidersFrom(FooModule)]});`, - ); - }); - - it('should be able to add a module import 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, {}); - `, - ); - - 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, - `bootstrapApplication(AppComponent, {providers: [importProvidersFrom(FooModule)]});`, - ); - }); - - it('should be able to add a module import 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'}] - }); - `, - ); - - addModuleImportToStandaloneBootstrap(host, '/test.ts', 'FooModule', '@foo/bar'); - - const content = stripWhitespace(host.readText('/test.ts')); - - assertContains(content, `import {enableProdMode, importProvidersFrom} from '@angular/core';`); - assertContains(content, `import {FooModule} from '@foo/bar';`); - assertContains( - content, - `bootstrapApplication(AppComponent, { - providers: [ - {provide: 'foo', useValue: 'bar'}, - importProvidersFrom(FooModule) - ] - });`, - ); - }); - - it('should be able to add a module import to a `bootstrapApplication` call with a pre-existing `importProvidersFrom` call', () => { - host.create( - '/test.ts', - ` - import { importProvidersFrom } from '@angular/core'; - import { bootstrapApplication, BrowserModule } from '@angular/platform-browser'; - import { AppComponent } from './app.component'; - - bootstrapApplication(AppComponent, { - providers: [{provide: 'foo', useValue: 'bar'}, importProvidersFrom(BrowserModule)] - }); - `, - ); - - 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, - `bootstrapApplication(AppComponent, { - providers: [ - {provide: 'foo', useValue: 'bar'}, - importProvidersFrom(BrowserModule, FooModule) - ] - });`, - ); - }); - - it('should throw if there is no `bootstrapModule` call', () => { - host.create( - '/test.ts', - ` - import { AppComponent } from './app.component'; - - console.log(AppComponent); - `, - ); - - expect(() => { - 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()] - }));`, - ); - }); - }); -}); diff --git a/packages/schematics/angular/service-worker/index.ts b/packages/schematics/angular/service-worker/index.ts index bb7d9f48d32f..f415c2bce76d 100644 --- a/packages/schematics/angular/service-worker/index.ts +++ b/packages/schematics/angular/service-worker/index.ts @@ -19,15 +19,16 @@ import { move, url, } from '@angular-devkit/schematics'; -import { addFunctionalProvidersToStandaloneBootstrap } from '../private/standalone'; import * as ts from '../third_party/github.com/Microsoft/TypeScript/lib/typescript'; -import { addDependency, readWorkspace, writeWorkspace } from '../utility'; +import { addDependency, addRootProvider, readWorkspace, writeWorkspace } from '../utility'; import { addSymbolToNgModuleMetadata, insertImport } from '../utility/ast-utils'; import { applyToUpdateRecorder } from '../utility/change'; import { getPackageJsonDependency } from '../utility/dependencies'; import { getAppModulePath, isStandaloneApp } from '../utility/ng-ast-utils'; import { relativePathToWorkspaceRoot } from '../utility/paths'; import { targetBuildNotFoundError } from '../utility/project-targets'; +import { findAppConfig } from '../utility/standalone/app_config'; +import { findBootstrapApplicationCall } from '../utility/standalone/util'; import { Builders } from '../utility/workspace-models'; import { Schema as ServiceWorkerOptions } from './schema'; @@ -78,41 +79,20 @@ function updateAppModule(mainPath: string): Rule { }; } -function addProvideServiceWorker(mainPath: string): Rule { +function addProvideServiceWorker(projectName: string, mainPath: string): Rule { return (host: Tree) => { - const updatedFilePath = addFunctionalProvidersToStandaloneBootstrap( - host, - mainPath, - 'provideServiceWorker', - '@angular/service-worker', - [ - ts.factory.createStringLiteral('ngsw-worker.js', true), - ts.factory.createObjectLiteralExpression( - [ - ts.factory.createPropertyAssignment( - ts.factory.createIdentifier('enabled'), - ts.factory.createPrefixUnaryExpression( - ts.SyntaxKind.ExclamationToken, - ts.factory.createCallExpression( - ts.factory.createIdentifier('isDevMode'), - undefined, - [], - ), - ), - ), - ts.factory.createPropertyAssignment( - ts.factory.createIdentifier('registrationStrategy'), - ts.factory.createStringLiteral('registerWhenStable:30000', true), - ), - ], - true, - ), - ], + const bootstrapCall = findBootstrapApplicationCall(host, mainPath); + const appConfig = findAppConfig(bootstrapCall, host, mainPath)?.filePath || mainPath; + addImport(host, appConfig, 'isDevMode', '@angular/core'); + + return addRootProvider( + projectName, + ({ code, external }) => + code`${external('provideServiceWorker', '@angular/service-worker')}('ngsw-worker.js', { + enabled: !isDevMode(), + registrationStrategy: 'registerWhenStable:30000' + })`, ); - - addImport(host, updatedFilePath, 'isDevMode', '@angular/core'); - - return host; }; } @@ -174,7 +154,7 @@ export default function (options: ServiceWorkerOptions): Rule { ]), ), isStandaloneApp(host, browserEntryPoint) - ? addProvideServiceWorker(browserEntryPoint) + ? addProvideServiceWorker(options.project, browserEntryPoint) : updateAppModule(browserEntryPoint), ]); }; diff --git a/packages/schematics/angular/utility/ng-ast-utils.ts b/packages/schematics/angular/utility/ng-ast-utils.ts index cddcef183bb0..2d1fa9cc1223 100644 --- a/packages/schematics/angular/utility/ng-ast-utils.ts +++ b/packages/schematics/angular/utility/ng-ast-utils.ts @@ -9,9 +9,9 @@ import { normalize } from '@angular-devkit/core'; import { SchematicsException, Tree } from '@angular-devkit/schematics'; import { dirname } from 'path'; -import { findBootstrapApplicationCall } from '../private/standalone'; import * as ts from '../third_party/github.com/Microsoft/TypeScript/lib/typescript'; import { findNode, getSourceNodes } from '../utility/ast-utils'; +import { findBootstrapApplicationCall } from './standalone/util'; export function findBootstrapModuleCall(host: Tree, mainPath: string): ts.CallExpression | null { const mainText = host.readText(mainPath); @@ -81,13 +81,15 @@ export function getAppModulePath(host: Tree, mainPath: string): string { } export function isStandaloneApp(host: Tree, mainPath: string): boolean { - const source = ts.createSourceFile( - mainPath, - host.readText(mainPath), - ts.ScriptTarget.Latest, - true, - ); - const bootstrapCall = findBootstrapApplicationCall(source); - - return bootstrapCall !== null; + try { + findBootstrapApplicationCall(host, mainPath); + + return true; + } catch (error) { + if (error instanceof SchematicsException) { + return false; + } + + throw error; + } }