feat(angular): add standalone app generation (#11592)
This commit is contained in:
parent
3c1d39b7c5
commit
af93cfd597
@ -252,6 +252,11 @@
|
||||
"description": "Skip setting the project as the default project. When `false` (the default), the project is set as the default project only if there is no default project already set.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"standalone": {
|
||||
"description": "Generate an application that is setup to use standalone components.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -32,7 +32,7 @@ describe('Angular Projects', () => {
|
||||
`generate @nrwl/angular:app ${myapp} --directory=myDir --no-interactive`
|
||||
);
|
||||
runCLI(
|
||||
`generate @nrwl/angular:app ${myapp2} --directory=myDir --no-interactive`
|
||||
`generate @nrwl/angular:app ${myapp2} --standalone=true --directory=myDir --no-interactive`
|
||||
);
|
||||
runCLI(
|
||||
`generate @nrwl/angular:lib ${mylib} --directory=myDir --add-module-spec --no-interactive`
|
||||
|
||||
@ -24,6 +24,133 @@ const config = require('./module-federation.config');
|
||||
module.exports = withModuleFederation(config);"
|
||||
`;
|
||||
|
||||
exports[`app --standalone should generate a standalone app correctly with routing 1`] = `
|
||||
"import { enableProdMode, importProvidersFrom } from '@angular/core';
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { AppComponent } from './app/app.component';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
bootstrapApplication(AppComponent, {
|
||||
providers: [importProvidersFrom(RouterModule.forRoot([], {initialNavigation: 'enabledBlocking'}))],
|
||||
}).catch((err) => console.error(err));"
|
||||
`;
|
||||
|
||||
exports[`app --standalone should generate a standalone app correctly with routing 2`] = `
|
||||
"import { NxWelcomeComponent } from './nx-welcome.component';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [NxWelcomeComponent, RouterModule],
|
||||
selector: 'proj-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'standalone';
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`app --standalone should generate a standalone app correctly with routing 3`] = `
|
||||
"import { TestBed } from '@angular/core/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
import { NxWelcomeComponent } from './nx-welcome.component';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({imports: [AppComponent, NxWelcomeComponent, RouterTestingModule] }).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(\`should have as title 'standalone'\`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual('standalone');
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('Welcome standalone');
|
||||
});
|
||||
});
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`app --standalone should generate a standalone app correctly without routing 1`] = `
|
||||
"import { enableProdMode } from '@angular/core';
|
||||
import { bootstrapApplication } from '@angular/platform-browser';;
|
||||
import { AppComponent } from './app/app.component';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
bootstrapApplication(AppComponent).catch((err) => console.error(err));"
|
||||
`;
|
||||
|
||||
exports[`app --standalone should generate a standalone app correctly without routing 2`] = `
|
||||
"import { NxWelcomeComponent } from './nx-welcome.component';
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [NxWelcomeComponent],
|
||||
selector: 'proj-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'standalone';
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`app --standalone should generate a standalone app correctly without routing 3`] = `
|
||||
"import { TestBed } from '@angular/core/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
import { NxWelcomeComponent } from './nx-welcome.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({imports: [AppComponent, NxWelcomeComponent]}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(\`should have as title 'standalone'\`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual('standalone');
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('Welcome standalone');
|
||||
});
|
||||
});
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`app at the root should accept numbers in the path 1`] = `"src/9-websites/my-app"`;
|
||||
|
||||
exports[`app nested should update workspace.json 1`] = `
|
||||
|
||||
@ -1074,6 +1074,58 @@ describe('app', () => {
|
||||
expect(devDependencies['autoprefixer']).toBe(autoprefixerVersion);
|
||||
});
|
||||
});
|
||||
|
||||
describe('--standalone', () => {
|
||||
it('should generate a standalone app correctly with routing', async () => {
|
||||
// ACT
|
||||
await generateApp(appTree, 'standalone', {
|
||||
standalone: true,
|
||||
routing: true,
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(
|
||||
appTree.read('apps/standalone/src/main.ts', 'utf-8')
|
||||
).toMatchSnapshot();
|
||||
expect(
|
||||
appTree.read('apps/standalone/src/app/app.component.ts', 'utf-8')
|
||||
).toMatchSnapshot();
|
||||
expect(
|
||||
appTree.read('apps/standalone/src/app/app.component.spec.ts', 'utf-8')
|
||||
).toMatchSnapshot();
|
||||
expect(
|
||||
appTree.exists('apps/standalone/src/app/app.module.ts')
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
appTree.read('apps/standalone/src/app/nx-welcome.component.ts', 'utf-8')
|
||||
).toContain('standalone: true');
|
||||
});
|
||||
|
||||
it('should generate a standalone app correctly without routing', async () => {
|
||||
// ACT
|
||||
await generateApp(appTree, 'standalone', {
|
||||
standalone: true,
|
||||
routing: false,
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(
|
||||
appTree.read('apps/standalone/src/main.ts', 'utf-8')
|
||||
).toMatchSnapshot();
|
||||
expect(
|
||||
appTree.read('apps/standalone/src/app/app.component.ts', 'utf-8')
|
||||
).toMatchSnapshot();
|
||||
expect(
|
||||
appTree.read('apps/standalone/src/app/app.component.spec.ts', 'utf-8')
|
||||
).toMatchSnapshot();
|
||||
expect(
|
||||
appTree.exists('apps/standalone/src/app/app.module.ts')
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
appTree.read('apps/standalone/src/app/nx-welcome.component.ts', 'utf-8')
|
||||
).toContain('standalone: true');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function generateApp(
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
addProxyConfig,
|
||||
addRouterRootConfiguration,
|
||||
addUnitTestRunner,
|
||||
convertToStandaloneApp,
|
||||
createFiles,
|
||||
enableStrictTypeChecking,
|
||||
normalizeOptions,
|
||||
@ -84,6 +85,7 @@ export async function applicationGenerator(
|
||||
flat: true,
|
||||
viewEncapsulation: 'None',
|
||||
project: options.name,
|
||||
standalone: options.standalone,
|
||||
});
|
||||
updateNxComponentTemplate(host, options);
|
||||
|
||||
@ -133,6 +135,10 @@ export async function applicationGenerator(
|
||||
await addMf(host, options);
|
||||
}
|
||||
|
||||
if (options.standalone) {
|
||||
convertToStandaloneApp(host, options);
|
||||
}
|
||||
|
||||
if (!options.skipFormat) {
|
||||
await formatFiles(host);
|
||||
}
|
||||
|
||||
@ -0,0 +1,132 @@
|
||||
import type { Tree } from '@nrwl/devkit';
|
||||
import { joinPathFragments } from '@nrwl/devkit';
|
||||
import type { NormalizedSchema } from './normalized-schema';
|
||||
import { tsquery } from '@phenomnomnominal/tsquery';
|
||||
|
||||
export function convertToStandaloneApp(tree: Tree, options: NormalizedSchema) {
|
||||
const pathToAppModule = joinPathFragments(
|
||||
options.appProjectRoot,
|
||||
'src/app/app.module.ts'
|
||||
);
|
||||
updateMainEntrypoint(options, tree, pathToAppModule);
|
||||
updateAppComponent(tree, options);
|
||||
if (!options.skipTests) {
|
||||
updateAppComponentSpec(tree, options);
|
||||
}
|
||||
|
||||
tree.delete(pathToAppModule);
|
||||
}
|
||||
|
||||
function updateMainEntrypoint(
|
||||
options: NormalizedSchema,
|
||||
tree: Tree,
|
||||
pathToAppModule: string
|
||||
) {
|
||||
let routerModuleSetup: string;
|
||||
if (options.routing) {
|
||||
const appModuleContents = tree.read(pathToAppModule, 'utf-8');
|
||||
const ast = tsquery.ast(appModuleContents);
|
||||
|
||||
const ROUTER_MODULE_SELECTOR =
|
||||
'PropertyAssignment:has(Identifier[name=imports]) CallExpression:has(PropertyAccessExpression > Identifier[name=RouterModule])';
|
||||
const nodes = tsquery(ast, ROUTER_MODULE_SELECTOR, {
|
||||
visitAllChildren: true,
|
||||
});
|
||||
if (nodes.length > 0) {
|
||||
routerModuleSetup = nodes[0].getText();
|
||||
}
|
||||
}
|
||||
|
||||
tree.write(
|
||||
joinPathFragments(options.appProjectRoot, 'src/main.ts'),
|
||||
standaloneComponentMainContents(routerModuleSetup)
|
||||
);
|
||||
}
|
||||
|
||||
const standaloneComponentMainContents = (
|
||||
routerModuleSetup
|
||||
) => `import { enableProdMode${
|
||||
routerModuleSetup ? `, importProvidersFrom` : ``
|
||||
} } from '@angular/core';
|
||||
import { bootstrapApplication } from '@angular/platform-browser';${
|
||||
routerModuleSetup
|
||||
? `
|
||||
import { RouterModule } from '@angular/router'`
|
||||
: ``
|
||||
};
|
||||
import { AppComponent } from './app/app.component';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
bootstrapApplication(AppComponent${
|
||||
routerModuleSetup
|
||||
? `, {
|
||||
providers: [importProvidersFrom(${routerModuleSetup})],
|
||||
}`
|
||||
: ''
|
||||
}).catch((err) => console.error(err));`;
|
||||
|
||||
function updateAppComponent(tree: Tree, options: NormalizedSchema) {
|
||||
const pathToAppComponent = joinPathFragments(
|
||||
options.appProjectRoot,
|
||||
'src/app/app.component.ts'
|
||||
);
|
||||
const appComponentContents = tree.read(pathToAppComponent, 'utf-8');
|
||||
|
||||
const ast = tsquery.ast(appComponentContents);
|
||||
const COMPONENT_DECORATOR_SELECTOR =
|
||||
'Decorator > CallExpression:has(Identifier[name=Component]) ObjectLiteralExpression';
|
||||
const nodes = tsquery(ast, COMPONENT_DECORATOR_SELECTOR, {
|
||||
visitAllChildren: true,
|
||||
});
|
||||
|
||||
if (nodes.length === 0) {
|
||||
throw new Error(
|
||||
'Could not find Component decorator within app.component.ts for standalone app generation.'
|
||||
);
|
||||
}
|
||||
|
||||
const startPos = nodes[0].getStart() + 1;
|
||||
|
||||
const newAppComponentContents = `import { NxWelcomeComponent } from './nx-welcome.component';${
|
||||
options.routing
|
||||
? `
|
||||
import { RouterModule } from '@angular/router';`
|
||||
: ''
|
||||
}
|
||||
${appComponentContents.slice(0, startPos)}
|
||||
standalone: true,
|
||||
imports: [NxWelcomeComponent${
|
||||
options.routing ? ', RouterModule' : ''
|
||||
}],${appComponentContents.slice(startPos, -1)}`;
|
||||
|
||||
tree.write(pathToAppComponent, newAppComponentContents);
|
||||
}
|
||||
|
||||
function updateAppComponentSpec(tree: Tree, options: NormalizedSchema) {
|
||||
const pathToAppComponentSpec = joinPathFragments(
|
||||
options.appProjectRoot,
|
||||
'src/app/app.component.spec.ts'
|
||||
);
|
||||
const appComponentSpecContents = tree.read(pathToAppComponentSpec, 'utf-8');
|
||||
|
||||
let newAppComponentSpecContents: string;
|
||||
if (!options.routing) {
|
||||
newAppComponentSpecContents = appComponentSpecContents.replace(
|
||||
'declarations',
|
||||
'imports'
|
||||
);
|
||||
} else {
|
||||
newAppComponentSpecContents = appComponentSpecContents
|
||||
.replace(
|
||||
'imports: [RouterTestingModule],',
|
||||
'imports: [AppComponent, NxWelcomeComponent, RouterTestingModule]'
|
||||
)
|
||||
.replace('declarations: [AppComponent, NxWelcomeComponent]', '');
|
||||
}
|
||||
|
||||
tree.write(pathToAppComponentSpec, newAppComponentSpecContents);
|
||||
}
|
||||
@ -19,3 +19,4 @@ export * from './update-nx-component-template';
|
||||
export * from './update-config-files';
|
||||
export * from './update-e2e-project';
|
||||
export * from './update-editor-tsconfig';
|
||||
export * from './convert-to-standalone-app';
|
||||
|
||||
@ -33,4 +33,5 @@ export interface Schema {
|
||||
skipPostInstall?: boolean;
|
||||
federationType?: 'static' | 'dynamic';
|
||||
skipDefaultProject?: boolean;
|
||||
standalone?: boolean;
|
||||
}
|
||||
|
||||
@ -181,6 +181,11 @@
|
||||
"description": "Skip setting the project as the default project. When `false` (the default), the project is set as the default project only if there is no default project already set.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"standalone": {
|
||||
"description": "Generate an application that is setup to use standalone components.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user