Skip to content

Commit 292a4b7

Browse files
committed
feat(@schematics/angular): update app-shell and ssr schematics to adopt new Server Rendering API
This commit revises the app-shell and ssr schematics to incorporate the new Server Rendering API, along with the integration of server-side routes. BREAKING CHANGE: The app-shell schematic is no longer compatible with Webpack-based builders.
1 parent 1bb68ba commit 292a4b7

File tree

46 files changed

+510
-696
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+510
-696
lines changed

packages/angular/build/src/builders/application/execute-post-bundle.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
} from '../../utils/server-rendering/models';
3030
import { prerenderPages } from '../../utils/server-rendering/prerender';
3131
import { augmentAppWithServiceWorkerEsbuild } from '../../utils/service-worker';
32-
import { INDEX_HTML_SERVER, NormalizedApplicationBuildOptions } from './options';
32+
import { INDEX_HTML_CSR, INDEX_HTML_SERVER, NormalizedApplicationBuildOptions } from './options';
3333
import { OutputMode } from './schema';
3434

3535
/**
@@ -154,7 +154,15 @@ export async function executePostBundleSteps(
154154
// Update the index contents with the app shell under these conditions:
155155
// - Replace 'index.html' with the app shell only if it hasn't been prerendered yet.
156156
// - Always replace 'index.csr.html' with the app shell.
157-
const filePath = appShellRoute && !indexHasBeenPrerendered ? indexHtmlOptions.output : path;
157+
let filePath = path;
158+
if (appShellRoute && !indexHasBeenPrerendered) {
159+
if (outputMode !== OutputMode.Server && indexHtmlOptions.output === INDEX_HTML_CSR) {
160+
filePath = 'index.html';
161+
} else {
162+
filePath = indexHtmlOptions.output;
163+
}
164+
}
165+
158166
additionalHtmlOutputFiles.set(
159167
filePath,
160168
createOutputFile(filePath, content, BuildOutputFileType.Browser),

packages/angular/build/src/utils/server-rendering/prerender.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ async function renderPages(
213213
outputFiles: outputFilesForWorker,
214214
assetFiles: assetFilesForWorker,
215215
outputMode,
216-
hasSsrEntry: !!outputFilesForWorker['/server.mjs'],
216+
hasSsrEntry: !!outputFilesForWorker['server.mjs'],
217217
} as RenderWorkerData,
218218
execArgv: workerExecArgv,
219219
});
@@ -319,7 +319,7 @@ async function getAllRoutes(
319319
outputFiles: outputFilesForWorker,
320320
assetFiles: assetFilesForWorker,
321321
outputMode,
322-
hasSsrEntry: !!outputFilesForWorker['/server.mjs'],
322+
hasSsrEntry: !!outputFilesForWorker['server.mjs'],
323323
} as RoutesExtractorWorkerData,
324324
execArgv: workerExecArgv,
325325
});

packages/angular/ssr/schematics/ng-add/index_spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ describe('@angular/ssr ng-add schematic', () => {
5252
});
5353

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

5757
expect(appTree.exists(filePath)).toBeFalse();
5858
const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree);

packages/angular_devkit/build_angular/src/builders/extract-i18n/application-extraction.ts

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export async function extractMessages(
5151
buildOptions.budgets = undefined;
5252
buildOptions.index = false;
5353
buildOptions.serviceWorker = false;
54+
buildOptions.server = undefined;
5455
buildOptions.ssr = false;
5556
buildOptions.appShell = false;
5657
buildOptions.prerender = false;

packages/schematics/angular/app-shell/index.ts

+62-114
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ import {
2929
import { applyToUpdateRecorder } from '../utility/change';
3030
import { getAppModulePath, isStandaloneApp } from '../utility/ng-ast-utils';
3131
import { findBootstrapApplicationCall, getMainFilePath } from '../utility/standalone/util';
32-
import { getWorkspace, updateWorkspace } from '../utility/workspace';
33-
import { Builders } from '../utility/workspace-models';
32+
import { getWorkspace } from '../utility/workspace';
3433
import { Schema as AppShellOptions } from './schema';
3534

3635
const APP_SHELL_ROUTE = 'shell';
@@ -140,77 +139,6 @@ function validateProject(mainPath: string): Rule {
140139
};
141140
}
142141

143-
function addAppShellConfigToWorkspace(options: AppShellOptions): Rule {
144-
return (host, context) => {
145-
return updateWorkspace((workspace) => {
146-
const project = workspace.projects.get(options.project);
147-
if (!project) {
148-
return;
149-
}
150-
151-
const buildTarget = project.targets.get('build');
152-
if (buildTarget?.builder === Builders.Application) {
153-
// Application builder configuration.
154-
const prodConfig = buildTarget.configurations?.production;
155-
if (!prodConfig) {
156-
throw new SchematicsException(
157-
`A "production" configuration is not defined for the "build" builder.`,
158-
);
159-
}
160-
161-
prodConfig.appShell = true;
162-
163-
return;
164-
}
165-
166-
// Webpack based builders configuration.
167-
// Validation of targets is handled already in the main function.
168-
// Duplicate keys means that we have configurations in both server and build builders.
169-
const serverConfigKeys = project.targets.get('server')?.configurations ?? {};
170-
const buildConfigKeys = project.targets.get('build')?.configurations ?? {};
171-
172-
const configurationNames = Object.keys({
173-
...serverConfigKeys,
174-
...buildConfigKeys,
175-
});
176-
177-
const configurations: Record<string, {}> = {};
178-
for (const key of configurationNames) {
179-
if (!serverConfigKeys[key]) {
180-
context.logger.warn(
181-
`Skipped adding "${key}" configuration to "app-shell" target as it's missing from "server" target.`,
182-
);
183-
184-
continue;
185-
}
186-
187-
if (!buildConfigKeys[key]) {
188-
context.logger.warn(
189-
`Skipped adding "${key}" configuration to "app-shell" target as it's missing from "build" target.`,
190-
);
191-
192-
continue;
193-
}
194-
195-
configurations[key] = {
196-
browserTarget: `${options.project}:build:${key}`,
197-
serverTarget: `${options.project}:server:${key}`,
198-
};
199-
}
200-
201-
project.targets.add({
202-
name: 'app-shell',
203-
builder: Builders.AppShell,
204-
defaultConfiguration: configurations['production'] ? 'production' : undefined,
205-
options: {
206-
route: APP_SHELL_ROUTE,
207-
},
208-
configurations,
209-
});
210-
});
211-
};
212-
}
213-
214142
function addRouterModule(mainPath: string): Rule {
215143
return (host: Tree) => {
216144
const modulePath = getAppModulePath(host, mainPath);
@@ -313,6 +241,7 @@ function addStandaloneServerRoute(options: AppShellOptions): Rule {
313241
throw new SchematicsException(`Cannot find "${configFilePath}".`);
314242
}
315243

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

325-
const recorder = host.beginUpdate(configFilePath);
326254
if (routesChange) {
327255
applyToUpdateRecorder(recorder, [routesChange]);
328-
host.commitUpdate(recorder);
329256
}
330257
}
331258

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

342269
// Add route to providers literal.
343-
const newProvidersLiteral = ts.factory.updateArrayLiteralExpression(providersLiteral, [
344-
...providersLiteral.elements,
345-
ts.factory.createObjectLiteralExpression(
346-
[
347-
ts.factory.createPropertyAssignment('provide', ts.factory.createIdentifier('ROUTES')),
348-
ts.factory.createPropertyAssignment('multi', ts.factory.createIdentifier('true')),
349-
ts.factory.createPropertyAssignment(
350-
'useValue',
351-
ts.factory.createArrayLiteralExpression(
352-
[
353-
ts.factory.createObjectLiteralExpression(
354-
[
355-
ts.factory.createPropertyAssignment(
356-
'path',
357-
ts.factory.createIdentifier(`'${APP_SHELL_ROUTE}'`),
358-
),
359-
ts.factory.createPropertyAssignment(
360-
'component',
361-
ts.factory.createIdentifier('AppShellComponent'),
362-
),
363-
],
364-
true,
365-
),
366-
],
367-
true,
368-
),
369-
),
370-
],
371-
true,
372-
),
373-
]);
374-
375-
const recorder = host.beginUpdate(configFilePath);
376270
recorder.remove(providersLiteral.getStart(), providersLiteral.getWidth());
377-
const printer = ts.createPrinter();
378-
recorder.insertRight(
379-
providersLiteral.getStart(),
380-
printer.printNode(ts.EmitHint.Unspecified, newProvidersLiteral, configSourceFile),
381-
);
271+
const updatedProvidersString = [
272+
...providersLiteral.elements.map((element) => ' ' + element.getText()),
273+
` {
274+
provide: ROUTES,
275+
multi: true,
276+
useValue: [{
277+
path: '${APP_SHELL_ROUTE}',
278+
component: AppShellComponent
279+
}]
280+
}\n `,
281+
];
282+
283+
recorder.insertRight(providersLiteral.getStart(), `[\n${updatedProvidersString.join(',\n')}]`);
382284

383285
// Add AppShellComponent import
384286
const appShellImportChange = insertImport(
@@ -393,6 +295,52 @@ function addStandaloneServerRoute(options: AppShellOptions): Rule {
393295
};
394296
}
395297

298+
function addServerRoutingConfig(options: AppShellOptions): Rule {
299+
return async (host: Tree) => {
300+
const workspace = await getWorkspace(host);
301+
const project = workspace.projects.get(options.project);
302+
if (!project) {
303+
throw new SchematicsException(`Project name "${options.project}" doesn't not exist.`);
304+
}
305+
306+
const configFilePath = join(project.sourceRoot ?? 'src', 'app/app.routes.server.ts');
307+
if (!host.exists(configFilePath)) {
308+
throw new SchematicsException(`Cannot find "${configFilePath}".`);
309+
}
310+
311+
const sourceFile = getSourceFile(host, configFilePath);
312+
const nodes = getSourceNodes(sourceFile);
313+
314+
// Find the serverRoutes variable declaration
315+
const serverRoutesNode = nodes.find(
316+
(node) =>
317+
ts.isVariableDeclaration(node) &&
318+
node.initializer &&
319+
ts.isArrayLiteralExpression(node.initializer) &&
320+
node.type &&
321+
ts.isArrayTypeNode(node.type) &&
322+
node.type.getText().includes('ServerRoute'),
323+
) as ts.VariableDeclaration | undefined;
324+
325+
if (!serverRoutesNode) {
326+
throw new SchematicsException(
327+
`Cannot find the "ServerRoute" configuration in "${configFilePath}".`,
328+
);
329+
}
330+
const recorder = host.beginUpdate(configFilePath);
331+
const arrayLiteral = serverRoutesNode.initializer as ts.ArrayLiteralExpression;
332+
const firstElementPosition =
333+
arrayLiteral.elements[0]?.getStart() ?? arrayLiteral.getStart() + 1;
334+
const newRouteString = `{
335+
path: '${APP_SHELL_ROUTE}',
336+
renderMode: RenderMode.AppShell
337+
},\n`;
338+
recorder.insertLeft(firstElementPosition, newRouteString);
339+
340+
host.commitUpdate(recorder);
341+
};
342+
}
343+
396344
export default function (options: AppShellOptions): Rule {
397345
return async (tree) => {
398346
const browserEntryPoint = await getMainFilePath(tree, options.project);
@@ -401,9 +349,9 @@ export default function (options: AppShellOptions): Rule {
401349
return chain([
402350
validateProject(browserEntryPoint),
403351
schematic('server', options),
404-
addAppShellConfigToWorkspace(options),
405352
isStandalone ? noop() : addRouterModule(browserEntryPoint),
406353
isStandalone ? addStandaloneServerRoute(options) : addServerRoutes(options),
354+
addServerRoutingConfig(options),
407355
schematic('component', {
408356
name: 'app-shell',
409357
module: 'app.module.server.ts',

0 commit comments

Comments
 (0)