Kristiyan Kostadinov bff634fe09 fix(@schematics/angular): update private Components utilities to work with standalone project structure
When the utilities for adding providers to the `bootstrapApplication` call were written, the structure for a standalone project handn't been finalized yet which meant that the code made some assumptions around how the app config is being passed in. This code breaks with the final project structure, because there the app config is in a separate file instead of an object literal.

These changes update the utilities to account for the new structure. Note the updates are meant to cover the majority of apps and there are likely some edge cases that aren't covered. Covering all cases is tricky with the current setup, because to do so correctly would require knowledge of the entire app whereas the utilities operate on a file-by-file basis. For example, now the code is able to update a config in a separate file, but it wouldn't be able to handle a config that is re-exported. For such a case to be supported, we would need access to the type checker. These edge cases will be addressed later on with a larger refactor of the schematics utilities.

I've added a couple of extra utilities for inserting function calls into the `providers` array and checking if a function is called in the `providers` array. We'll need them for the `ng generate` schematics on Components.
2023-04-03 09:05:26 +00:00

659 lines
21 KiB
TypeScript

/**
* @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()]
}));`,
);
});
});
});