Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/angular/build/src/builders/application/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,10 @@ export async function normalizeOptions(
* For instance, accessing `foo.com/` would lead to `foo.com/index.html` being served instead of hitting the server.
*/
const indexBaseName = path.basename(options.index);
indexOutput = ssrOptions && indexBaseName === 'index.html' ? INDEX_HTML_CSR : indexBaseName;
indexOutput =
ssrOptions && options.outputMode !== OutputMode.Static && indexBaseName === 'index.html'
? INDEX_HTML_CSR
: indexBaseName;
} else {
indexOutput = options.index.output || 'index.html';
}
Expand Down
2 changes: 1 addition & 1 deletion packages/angular/ssr/schematics/ng-add/index_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe('@angular/ssr ng-add schematic', () => {
});

it('works', async () => {
const filePath = '/projects/test-app/server.ts';
const filePath = '/projects/test-app/src/server.ts';

expect(appTree.exists(filePath)).toBeFalse();
const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export async function extractMessages(
buildOptions.budgets = undefined;
buildOptions.index = false;
buildOptions.serviceWorker = false;
buildOptions.server = undefined;
buildOptions.ssr = false;
buildOptions.appShell = false;
buildOptions.prerender = false;
Expand Down
176 changes: 62 additions & 114 deletions packages/schematics/angular/app-shell/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ import {
import { applyToUpdateRecorder } from '../utility/change';
import { getAppModulePath, isStandaloneApp } from '../utility/ng-ast-utils';
import { findBootstrapApplicationCall, getMainFilePath } from '../utility/standalone/util';
import { getWorkspace, updateWorkspace } from '../utility/workspace';
import { Builders } from '../utility/workspace-models';
import { getWorkspace } from '../utility/workspace';
import { Schema as AppShellOptions } from './schema';

const APP_SHELL_ROUTE = 'shell';
Expand Down Expand Up @@ -140,77 +139,6 @@ function validateProject(mainPath: string): Rule {
};
}

function addAppShellConfigToWorkspace(options: AppShellOptions): Rule {
return (host, context) => {
return updateWorkspace((workspace) => {
const project = workspace.projects.get(options.project);
if (!project) {
return;
}

const buildTarget = project.targets.get('build');
if (buildTarget?.builder === Builders.Application) {
// Application builder configuration.
const prodConfig = buildTarget.configurations?.production;
if (!prodConfig) {
throw new SchematicsException(
`A "production" configuration is not defined for the "build" builder.`,
);
}

prodConfig.appShell = true;

return;
}

// Webpack based builders configuration.
// Validation of targets is handled already in the main function.
// Duplicate keys means that we have configurations in both server and build builders.
const serverConfigKeys = project.targets.get('server')?.configurations ?? {};
const buildConfigKeys = project.targets.get('build')?.configurations ?? {};

const configurationNames = Object.keys({
...serverConfigKeys,
...buildConfigKeys,
});

const configurations: Record<string, {}> = {};
for (const key of configurationNames) {
if (!serverConfigKeys[key]) {
context.logger.warn(
`Skipped adding "${key}" configuration to "app-shell" target as it's missing from "server" target.`,
);

continue;
}

if (!buildConfigKeys[key]) {
context.logger.warn(
`Skipped adding "${key}" configuration to "app-shell" target as it's missing from "build" target.`,
);

continue;
}

configurations[key] = {
browserTarget: `${options.project}:build:${key}`,
serverTarget: `${options.project}:server:${key}`,
};
}

project.targets.add({
name: 'app-shell',
builder: Builders.AppShell,
defaultConfiguration: configurations['production'] ? 'production' : undefined,
options: {
route: APP_SHELL_ROUTE,
},
configurations,
});
});
};
}

function addRouterModule(mainPath: string): Rule {
return (host: Tree) => {
const modulePath = getAppModulePath(host, mainPath);
Expand Down Expand Up @@ -313,6 +241,7 @@ function addStandaloneServerRoute(options: AppShellOptions): Rule {
throw new SchematicsException(`Cannot find "${configFilePath}".`);
}

const recorder = host.beginUpdate(configFilePath);
let configSourceFile = getSourceFile(host, configFilePath);
if (!isImported(configSourceFile, 'ROUTES', '@angular/router')) {
const routesChange = insertImport(
Expand All @@ -322,10 +251,8 @@ function addStandaloneServerRoute(options: AppShellOptions): Rule {
'@angular/router',
);

const recorder = host.beginUpdate(configFilePath);
if (routesChange) {
applyToUpdateRecorder(recorder, [routesChange]);
host.commitUpdate(recorder);
}
}

Expand All @@ -340,45 +267,20 @@ function addStandaloneServerRoute(options: AppShellOptions): Rule {
}

// Add route to providers literal.
const newProvidersLiteral = ts.factory.updateArrayLiteralExpression(providersLiteral, [
...providersLiteral.elements,
ts.factory.createObjectLiteralExpression(
[
ts.factory.createPropertyAssignment('provide', ts.factory.createIdentifier('ROUTES')),
ts.factory.createPropertyAssignment('multi', ts.factory.createIdentifier('true')),
ts.factory.createPropertyAssignment(
'useValue',
ts.factory.createArrayLiteralExpression(
[
ts.factory.createObjectLiteralExpression(
[
ts.factory.createPropertyAssignment(
'path',
ts.factory.createIdentifier(`'${APP_SHELL_ROUTE}'`),
),
ts.factory.createPropertyAssignment(
'component',
ts.factory.createIdentifier('AppShellComponent'),
),
],
true,
),
],
true,
),
),
],
true,
),
]);

const recorder = host.beginUpdate(configFilePath);
recorder.remove(providersLiteral.getStart(), providersLiteral.getWidth());
const printer = ts.createPrinter();
recorder.insertRight(
providersLiteral.getStart(),
printer.printNode(ts.EmitHint.Unspecified, newProvidersLiteral, configSourceFile),
);
const updatedProvidersString = [
...providersLiteral.elements.map((element) => ' ' + element.getText()),
` {
provide: ROUTES,
multi: true,
useValue: [{
path: '${APP_SHELL_ROUTE}',
component: AppShellComponent
}]
}\n `,
];

recorder.insertRight(providersLiteral.getStart(), `[\n${updatedProvidersString.join(',\n')}]`);

// Add AppShellComponent import
const appShellImportChange = insertImport(
Expand All @@ -393,6 +295,52 @@ function addStandaloneServerRoute(options: AppShellOptions): Rule {
};
}

function addServerRoutingConfig(options: AppShellOptions): Rule {
return async (host: Tree) => {
const workspace = await getWorkspace(host);
const project = workspace.projects.get(options.project);
if (!project) {
throw new SchematicsException(`Project name "${options.project}" doesn't not exist.`);
}

const configFilePath = join(project.sourceRoot ?? 'src', 'app/app.routes.server.ts');
if (!host.exists(configFilePath)) {
throw new SchematicsException(`Cannot find "${configFilePath}".`);
}

const sourceFile = getSourceFile(host, configFilePath);
const nodes = getSourceNodes(sourceFile);

// Find the serverRoutes variable declaration
const serverRoutesNode = nodes.find(
(node) =>
ts.isVariableDeclaration(node) &&
node.initializer &&
ts.isArrayLiteralExpression(node.initializer) &&
node.type &&
ts.isArrayTypeNode(node.type) &&
node.type.getText().includes('ServerRoute'),
) as ts.VariableDeclaration | undefined;

if (!serverRoutesNode) {
throw new SchematicsException(
`Cannot find the "ServerRoute" configuration in "${configFilePath}".`,
);
}
const recorder = host.beginUpdate(configFilePath);
const arrayLiteral = serverRoutesNode.initializer as ts.ArrayLiteralExpression;
const firstElementPosition =
arrayLiteral.elements[0]?.getStart() ?? arrayLiteral.getStart() + 1;
const newRouteString = `{
path: '${APP_SHELL_ROUTE}',
renderMode: RenderMode.AppShell
},\n`;
recorder.insertLeft(firstElementPosition, newRouteString);

host.commitUpdate(recorder);
};
}

export default function (options: AppShellOptions): Rule {
return async (tree) => {
const browserEntryPoint = await getMainFilePath(tree, options.project);
Expand All @@ -401,9 +349,9 @@ export default function (options: AppShellOptions): Rule {
return chain([
validateProject(browserEntryPoint),
schematic('server', options),
addAppShellConfigToWorkspace(options),
isStandalone ? noop() : addRouterModule(browserEntryPoint),
isStandalone ? addStandaloneServerRoute(options) : addServerRoutes(options),
addServerRoutingConfig(options),
schematic('component', {
name: 'app-shell',
module: 'app.module.server.ts',
Expand Down
76 changes: 19 additions & 57 deletions packages/schematics/angular/app-shell/index_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import { tags } from '@angular-devkit/core';
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
import { Schema as ApplicationOptions } from '../application/schema';
import { Builders } from '../utility/workspace-models';
import { Schema as WorkspaceOptions } from '../workspace/schema';
import { Schema as AppShellOptions } from './schema';

Expand Down Expand Up @@ -51,15 +50,6 @@ describe('App Shell Schematic', () => {
);
});

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 ensure the client app has a router-outlet', async () => {
appTree = await schematicRunner.runSchematic('workspace', workspaceOptions);
appTree = await schematicRunner.runSchematic(
Expand Down Expand Up @@ -168,7 +158,7 @@ describe('App Shell Schematic', () => {
expect(content).toMatch(
/const routes: Routes = \[ { path: 'shell', component: AppShellComponent }\];/,
);
expect(content).toMatch(/ServerModule,\r?\n\s*RouterModule\.forRoot\(routes\),/);
expect(content).toContain(`ServerModule, RouterModule.forRoot(routes)]`);
});

it('should create the shell component', async () => {
Expand Down Expand Up @@ -205,22 +195,34 @@ describe('App Shell Schematic', () => {
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 update the server routing configuration', async () => {
const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
const content = tree.readContent('/projects/bar/src/app/app.routes.server.ts');
expect(tags.oneLine`${content}`).toContain(tags.oneLine`{
path: 'shell',
renderMode: RenderMode.AppShell
},
{
path: '**',
renderMode: RenderMode.Prerender
}`);
});

it('should define a server route', async () => {
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,
multi: true,
useValue: [
{
path: 'shell',
component: AppShellComponent
}
]
useValue: [{
path: 'shell',
component: AppShellComponent
}]
}`);
});

Expand All @@ -240,44 +242,4 @@ describe('App Shell Schematic', () => {
);
});
});

describe('Legacy browser builder', () => {
function convertBuilderToLegacyBrowser(): void {
const config = JSON.parse(appTree.readContent('/angular.json'));
const build = config.projects.bar.architect.build;

build.builder = Builders.Browser;
build.options = {
...build.options,
main: build.options.browser,
browser: undefined,
};

build.configurations.development = {
...build.configurations.development,
vendorChunk: true,
namedChunks: true,
buildOptimizer: false,
};

appTree.overwrite('/angular.json', JSON.stringify(config, undefined, 2));
}

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

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['app-shell'];
expect(target.configurations.development.browserTarget).toEqual('bar:build:development');
expect(target.configurations.development.serverTarget).toEqual('bar:server:development');
expect(target.configurations.production.browserTarget).toEqual('bar:build:production');
expect(target.configurations.production.serverTarget).toEqual('bar:server:production');
});
});
});
4 changes: 2 additions & 2 deletions packages/schematics/angular/application/index_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ describe('Application Schematic', () => {

it(`should create an application with SSR features when 'ssr=true'`, async () => {
const options = { ...defaultOptions, ssr: true };
const filePath = '/projects/foo/server.ts';
const filePath = '/projects/foo/src/server.ts';
expect(workspaceTree.exists(filePath)).toBeFalse();
const tree = await schematicRunner.runSchematic('application', options, workspaceTree);
expect(tree.exists(filePath)).toBeTrue();
Expand All @@ -200,7 +200,7 @@ describe('Application Schematic', () => {
it(`should not create an application with SSR features when 'ssr=false'`, async () => {
const options = { ...defaultOptions, ssr: false };
const tree = await schematicRunner.runSchematic('application', options, workspaceTree);
expect(tree.exists('/projects/foo/server.ts')).toBeFalse();
expect(tree.exists('/projects/foo/src/server.ts')).toBeFalse();
});

describe(`update package.json`, () => {
Expand Down
Loading