Skip to content

Commit

Permalink
feat(@schematics/angular): enable standalone by default in new applic…
Browse files Browse the repository at this point in the history
…ations

This commit update the schematics to generate a standalone application by default.
  • Loading branch information
alan-agius4 committed Sep 22, 2023
1 parent 61f409c commit ac0db66
Show file tree
Hide file tree
Showing 19 changed files with 868 additions and 807 deletions.
263 changes: 126 additions & 137 deletions packages/schematics/angular/app-shell/index_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,63 +40,71 @@ describe('App Shell Schematic', () => {

beforeEach(async () => {
appTree = await schematicRunner.runSchematic('workspace', workspaceOptions);
appTree = await schematicRunner.runSchematic('application', appOptions, appTree);
});

it('should add app shell configuration', async () => {
const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
const filePath = '/angular.json';
const content = tree.readContent(filePath);
const workspace = JSON.parse(content);
const target = workspace.projects.bar.architect['build'];
expect(target.configurations.production.appShell).toBeTrue();
});
describe('non standalone application', () => {
beforeEach(async () => {
appTree = await schematicRunner.runSchematic(
'application',
{ ...appOptions, standalone: false },
appTree,
);
});

it('should ensure the client app has a router-outlet', async () => {
appTree = await schematicRunner.runSchematic('workspace', workspaceOptions);
appTree = await schematicRunner.runSchematic(
'application',
{ ...appOptions, routing: false },
appTree,
);
await expectAsync(
schematicRunner.runSchematic('app-shell', defaultOptions, appTree),
).toBeRejected();
});
it('should add app shell configuration', async () => {
const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
const filePath = '/angular.json';
const content = tree.readContent(filePath);
const workspace = JSON.parse(content);
const target = workspace.projects.bar.architect['build'];
expect(target.configurations.production.appShell).toBeTrue();
});

it('should add a server app', async () => {
const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
const filePath = '/projects/bar/src/app/app.module.server.ts';
expect(tree.exists(filePath)).toEqual(true);
});
it('should ensure the client app has a router-outlet', async () => {
appTree = await schematicRunner.runSchematic('workspace', workspaceOptions);
appTree = await schematicRunner.runSchematic(
'application',
{ ...appOptions, routing: false },
appTree,
);
await expectAsync(
schematicRunner.runSchematic('app-shell', defaultOptions, appTree),
).toBeRejected();
});

it('should add router module to client app module', async () => {
const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
const filePath = '/projects/bar/src/app/app.module.ts';
const content = tree.readContent(filePath);
expect(content).toMatch(/import { RouterModule } from '@angular\/router';/);
});
it('should add a server app', async () => {
const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
const filePath = '/projects/bar/src/app/app.module.server.ts';
expect(tree.exists(filePath)).toEqual(true);
});

it('should not fail when AppModule have imported RouterModule already', async () => {
const updateRecorder = appTree.beginUpdate('/projects/bar/src/app/app.module.ts');
updateRecorder.insertLeft(0, "import { RouterModule } from '@angular/router';");
appTree.commitUpdate(updateRecorder);
it('should add router module to client app module', async () => {
const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
const filePath = '/projects/bar/src/app/app.module.ts';
const content = tree.readContent(filePath);
expect(content).toMatch(/import { RouterModule } from '@angular\/router';/);
});

const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
const filePath = '/projects/bar/src/app/app.module.ts';
const content = tree.readContent(filePath);
expect(content).toMatch(/import { RouterModule } from '@angular\/router';/);
});
it('should not fail when AppModule have imported RouterModule already', async () => {
const updateRecorder = appTree.beginUpdate('/projects/bar/src/app/app.module.ts');
updateRecorder.insertLeft(0, "import { RouterModule } from '@angular/router';");
appTree.commitUpdate(updateRecorder);

describe('Add router-outlet', () => {
function makeInlineTemplate(tree: UnitTestTree, template?: string): void {
template =
template ||
`
const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
const filePath = '/projects/bar/src/app/app.module.ts';
const content = tree.readContent(filePath);
expect(content).toMatch(/import { RouterModule } from '@angular\/router';/);
});

describe('Add router-outlet', () => {
function makeInlineTemplate(tree: UnitTestTree, template?: string): void {
template =
template ||
`
<p>
App works!
</p>`;
const newText = `
const newText = `
import { Component } from '@angular/core';
@Component({
Expand All @@ -109,112 +117,100 @@ describe('App Shell Schematic', () => {
export class AppComponent { }
`;
tree.overwrite('/projects/bar/src/app/app.component.ts', newText);
tree.delete('/projects/bar/src/app/app.component.html');
}

it('should not re-add the router outlet (external template)', async () => {
const htmlPath = '/projects/bar/src/app/app.component.html';
appTree.overwrite(htmlPath, '<router-outlet></router-outlet>');
const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
const content = tree.readContent(htmlPath);
const matches = content.match(/<router-outlet><\/router-outlet>/g);
const numMatches = matches ? matches.length : 0;
expect(numMatches).toEqual(1);
tree.overwrite('/projects/bar/src/app/app.component.ts', newText);
tree.delete('/projects/bar/src/app/app.component.html');
}

it('should not re-add the router outlet (external template)', async () => {
const htmlPath = '/projects/bar/src/app/app.component.html';
appTree.overwrite(htmlPath, '<router-outlet></router-outlet>');
const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
const content = tree.readContent(htmlPath);
const matches = content.match(/<router-outlet><\/router-outlet>/g);
const numMatches = matches ? matches.length : 0;
expect(numMatches).toEqual(1);
});

it('should not re-add the router outlet (inline template)', async () => {
makeInlineTemplate(appTree, '<router-outlet></router-outlet>');
const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
const content = tree.readContent('/projects/bar/src/app/app.component.ts');
const matches = content.match(/<router-outlet><\/router-outlet>/g);
const numMatches = matches ? matches.length : 0;
expect(numMatches).toEqual(1);
});
});

it('should not re-add the router outlet (inline template)', async () => {
makeInlineTemplate(appTree, '<router-outlet></router-outlet>');
it('should add router imports to server module', async () => {
const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
const content = tree.readContent('/projects/bar/src/app/app.component.ts');
const matches = content.match(/<router-outlet><\/router-outlet>/g);
const numMatches = matches ? matches.length : 0;
expect(numMatches).toEqual(1);
const filePath = '/projects/bar/src/app/app.module.server.ts';
const content = tree.readContent(filePath);
expect(content).toMatch(/import { Routes, RouterModule } from '@angular\/router';/);
});
});

it('should add router imports to server module', async () => {
const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
const filePath = '/projects/bar/src/app/app.module.server.ts';
const content = tree.readContent(filePath);
expect(content).toMatch(/import { Routes, RouterModule } from '@angular\/router';/);
});

it('should work if server config was added prior to running the app-shell schematic', async () => {
let tree = await schematicRunner.runSchematic('server', defaultOptions, appTree);
tree = await schematicRunner.runSchematic('app-shell', defaultOptions, tree);
expect(tree.exists('/projects/bar/src/app/app-shell/app-shell.component.ts')).toBe(true);
});
it('should work if server config was added prior to running the app-shell schematic', async () => {
let tree = await schematicRunner.runSchematic('server', defaultOptions, appTree);
tree = await schematicRunner.runSchematic('app-shell', defaultOptions, tree);
expect(tree.exists('/projects/bar/src/app/app-shell/app-shell.component.ts')).toBe(true);
});

it('should define a server route', async () => {
const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
const filePath = '/projects/bar/src/app/app.module.server.ts';
const content = tree.readContent(filePath);
expect(content).toMatch(/const routes: Routes = \[/);
});
it('should define a server route', async () => {
const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
const filePath = '/projects/bar/src/app/app.module.server.ts';
const content = tree.readContent(filePath);
expect(content).toMatch(/const routes: Routes = \[/);
});

it('should import RouterModule with forRoot', async () => {
const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
const filePath = '/projects/bar/src/app/app.module.server.ts';
const content = tree.readContent(filePath);
expect(content).toMatch(
/const routes: Routes = \[ { path: 'shell', component: AppShellComponent }\];/,
);
expect(content).toMatch(/ServerModule,\r?\n\s*RouterModule\.forRoot\(routes\),/);
});
it('should import RouterModule with forRoot', async () => {
const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
const filePath = '/projects/bar/src/app/app.module.server.ts';
const content = tree.readContent(filePath);
expect(content).toMatch(
/const routes: Routes = \[ { path: 'shell', component: AppShellComponent }\];/,
);
expect(content).toMatch(/ServerModule,\r?\n\s*RouterModule\.forRoot\(routes\),/);
});

it('should create the shell component', async () => {
const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
expect(tree.exists('/projects/bar/src/app/app-shell/app-shell.component.ts')).toBe(true);
const content = tree.readContent('/projects/bar/src/app/app.module.server.ts');
expect(content).toMatch(/app-shell\.component/);
it('should create the shell component', async () => {
const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
expect(tree.exists('/projects/bar/src/app/app-shell/app-shell.component.ts')).toBe(true);
const content = tree.readContent('/projects/bar/src/app/app.module.server.ts');
expect(content).toMatch(/app-shell\.component/);
});
});

describe('standalone application', () => {
const standaloneAppName = 'baz';
const standaloneAppOptions: ApplicationOptions = {
...appOptions,
name: standaloneAppName,
standalone: true,
};
const defaultStandaloneOptions: AppShellOptions = {
project: standaloneAppName,
};

beforeEach(async () => {
appTree = await schematicRunner.runSchematic('application', standaloneAppOptions, appTree);
appTree = await schematicRunner.runSchematic('application', appOptions, appTree);
});

it('should ensure the client app has a router-outlet', async () => {
appTree = await schematicRunner.runSchematic('workspace', workspaceOptions);
const appName = 'baz';
appTree = await schematicRunner.runSchematic(
'application',
{ ...standaloneAppOptions, routing: false },
{
...appOptions,
name: appName,
routing: false,
},
appTree,
);

await expectAsync(
schematicRunner.runSchematic('app-shell', defaultStandaloneOptions, appTree),
schematicRunner.runSchematic('app-shell', { ...defaultOptions, project: appName }, appTree),
).toBeRejected();
});

it('should create the shell component', async () => {
const tree = await schematicRunner.runSchematic(
'app-shell',
defaultStandaloneOptions,
appTree,
);
expect(tree.exists('/projects/baz/src/app/app-shell/app-shell.component.ts')).toBe(true);
const content = tree.readContent('/projects/baz/src/app/app.config.server.ts');
const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
expect(tree.exists('/projects/bar/src/app/app-shell/app-shell.component.ts')).toBe(true);
const content = tree.readContent('/projects/bar/src/app/app.config.server.ts');
expect(content).toMatch(/app-shell\.component/);
});

it('should define a server route', async () => {
const tree = await schematicRunner.runSchematic(
'app-shell',
defaultStandaloneOptions,
appTree,
);
const filePath = '/projects/baz/src/app/app.config.server.ts';
const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
const filePath = '/projects/bar/src/app/app.config.server.ts';
const content = tree.readContent(filePath);
expect(tags.oneLine`${content}`).toContain(tags.oneLine`{
provide: ROUTES,
Expand All @@ -229,23 +225,15 @@ describe('App Shell Schematic', () => {
});

it(`should add import to 'ROUTES' token from '@angular/router'`, async () => {
const tree = await schematicRunner.runSchematic(
'app-shell',
defaultStandaloneOptions,
appTree,
);
const filePath = '/projects/baz/src/app/app.config.server.ts';
const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
const filePath = '/projects/bar/src/app/app.config.server.ts';
const content = tree.readContent(filePath);
expect(content).toContain(`import { ROUTES } from '@angular/router';`);
});

it(`should add import to 'AppShellComponent'`, async () => {
const tree = await schematicRunner.runSchematic(
'app-shell',
defaultStandaloneOptions,
appTree,
);
const filePath = '/projects/baz/src/app/app.config.server.ts';
const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
const filePath = '/projects/bar/src/app/app.config.server.ts';
const content = tree.readContent(filePath);
expect(content).toContain(
`import { AppShellComponent } from './app-shell/app-shell.component';`,
Expand Down Expand Up @@ -275,7 +263,8 @@ describe('App Shell Schematic', () => {
appTree.overwrite('/angular.json', JSON.stringify(config, undefined, 2));
}

beforeEach(() => {
beforeEach(async () => {
appTree = await schematicRunner.runSchematic('application', appOptions, appTree);
convertBuilderToLegacyBrowser();
});

Expand Down
19 changes: 3 additions & 16 deletions packages/schematics/angular/application/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,6 @@ export default function (options: ApplicationOptions): Rule {
const { appDir, appRootSelector, componentOptions, folderName, sourceDir } =
await getAppOptions(host, options);

if (options.standalone) {
context.logger.warn(
'Standalone application structure is new and not yet supported by many existing' +
` 'ng add' and 'ng update' integrations with community libraries.`,
);
}

return chain([
addAppToWorkspaceFile(options, appDir, folderName),
options.standalone
Expand Down Expand Up @@ -183,20 +176,14 @@ function addAppToWorkspaceFile(
];

schematicsWithTests.forEach((type) => {
if (!(`@schematics/angular:${type}` in schematics)) {
schematics[`@schematics/angular:${type}`] = {};
}
(schematics[`@schematics/angular:${type}`] as JsonObject).skipTests = true;
((schematics[`@schematics/angular:${type}`] ??= {}) as JsonObject).skipTests = true;
});
}

if (options.standalone) {
if (!options.standalone) {
const schematicsWithStandalone = ['component', 'directive', 'pipe'];
schematicsWithStandalone.forEach((type) => {
if (!(`@schematics/angular:${type}` in schematics)) {
schematics[`@schematics/angular:${type}`] = {};
}
(schematics[`@schematics/angular:${type}`] as JsonObject).standalone = true;
((schematics[`@schematics/angular:${type}`] ??= {}) as JsonObject).standalone = false;
});
}

Expand Down
Loading

0 comments on commit ac0db66

Please sign in to comment.