Skip to content

Commit e6b3774

Browse files
alan-agius4clydin
authored andcommitted
feat(@angular-devkit/build-angular): add ssr option in application builder
This commit adds an `ssr` option to the application builder, this can be either a `boolean` or an `object` with an `entryPoint` property. In the future, server bundles will only be emitted when the ssr option is truthy, as unlike SSR, SSG and AppShell do not require the server bundles to be written to disk.
1 parent a0a2c7a commit e6b3774

File tree

8 files changed

+148
-46
lines changed

8 files changed

+148
-46
lines changed

Diff for: packages/angular_devkit/build_angular/src/builders/application/execute-build.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export async function executeBuild(
5252
cacheOptions,
5353
prerenderOptions,
5454
appShellOptions,
55+
ssrOptions,
5556
} = options;
5657

5758
const browsers = getSupportedBrowsers(projectRoot, context.logger);
@@ -167,8 +168,7 @@ export async function executeBuild(
167168

168169
executionResult.addOutputFile(indexHtmlOptions.output, content);
169170

170-
if (serverEntryPoint) {
171-
// TODO only add the below file when SSR is enabled.
171+
if (ssrOptions) {
172172
executionResult.addOutputFile('index.server.html', contentWithoutCriticalCssInlined);
173173
}
174174
}

Diff for: packages/angular_devkit/build_angular/src/builders/application/options.ts

+12
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,17 @@ export async function normalizeOptions(
186186
};
187187
}
188188

189+
let ssrOptions;
190+
if (options.ssr === true) {
191+
ssrOptions = {};
192+
} else if (typeof options.ssr === 'object') {
193+
const { entry } = options.ssr;
194+
195+
ssrOptions = {
196+
entry: entry && path.join(workspaceRoot, entry),
197+
};
198+
}
199+
189200
let appShellOptions;
190201
if (options.appShell) {
191202
appShellOptions = {
@@ -241,6 +252,7 @@ export async function normalizeOptions(
241252
serverEntryPoint,
242253
prerenderOptions,
243254
appShellOptions,
255+
ssrOptions,
244256
verbose,
245257
watch,
246258
workspaceRoot,

Diff for: packages/angular_devkit/build_angular/src/builders/application/schema.json

+20
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,26 @@
452452
}
453453
]
454454
},
455+
"ssr": {
456+
"description": "Server side render (SSR) pages of your application during runtime.",
457+
"default": false,
458+
"oneOf": [
459+
{
460+
"type": "boolean",
461+
"description": "Enable the server bundles to be written to disk."
462+
},
463+
{
464+
"type": "object",
465+
"properties": {
466+
"entry": {
467+
"type": "string",
468+
"description": "The server entry-point that when executed will spawn the web server."
469+
}
470+
},
471+
"additionalProperties": false
472+
}
473+
]
474+
},
455475
"appShell": {
456476
"type": "boolean",
457477
"description": "Generates an application shell during build time.",

Diff for: packages/angular_devkit/build_angular/src/builders/application/tests/options/server_spec.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
3030
const { result } = await harness.executeOnce();
3131
expect(result?.success).toBeTrue();
3232

33-
harness.expectFile('dist/server.mjs').toExist();
33+
harness.expectFile('dist/main.server.mjs').toExist();
3434
harness.expectFile('dist/main.js').toExist();
3535
});
3636

@@ -45,7 +45,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
4545
const { result } = await harness.executeOnce();
4646
expect(result?.success).toBeTrue();
4747

48-
harness.expectFile('dist/server.mjs').toExist();
48+
harness.expectFile('dist/main.server.mjs').toExist();
4949
});
5050

5151
it('fails and shows an error when file does not exist', async () => {
@@ -62,7 +62,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
6262
);
6363

6464
harness.expectFile('dist/main.js').toNotExist();
65-
harness.expectFile('dist/server.mjs').toNotExist();
65+
harness.expectFile('dist/main.server.mjs').toNotExist();
6666
});
6767

6868
it('throws an error when given an empty string', async () => {
@@ -88,8 +88,8 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
8888
const { result } = await harness.executeOnce();
8989
expect(result?.success).toBeTrue();
9090

91-
// Always uses the name `server.mjs` for the `server` option.
92-
harness.expectFile('dist/server.mjs').toExist();
91+
// Always uses the name `main.server.mjs` for the `server` option.
92+
harness.expectFile('dist/main.server.mjs').toExist();
9393
});
9494
});
9595
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { buildApplication } from '../../index';
10+
import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup';
11+
12+
describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
13+
beforeEach(async () => {
14+
await harness.modifyFile('src/tsconfig.app.json', (content) => {
15+
const tsConfig = JSON.parse(content);
16+
tsConfig.files ??= [];
17+
tsConfig.files.push('main.server.ts', 'server.ts');
18+
19+
return JSON.stringify(tsConfig);
20+
});
21+
22+
await harness.writeFile('src/server.ts', `console.log('Hello!');`);
23+
});
24+
25+
describe('Option: "ssr"', () => {
26+
it('uses a provided TypeScript file', async () => {
27+
harness.useTarget('build', {
28+
...BASE_OPTIONS,
29+
server: 'src/main.server.ts',
30+
ssr: {
31+
entry: 'src/server.ts',
32+
},
33+
});
34+
35+
const { result } = await harness.executeOnce();
36+
expect(result?.success).toBeTrue();
37+
38+
harness.expectFile('dist/main.server.mjs').toExist();
39+
harness.expectFile('dist/server.mjs').toExist();
40+
});
41+
42+
it('resolves an absolute path as relative inside the workspace root', async () => {
43+
await harness.writeFile('file.mjs', `console.log('Hello!');`);
44+
45+
harness.useTarget('build', {
46+
...BASE_OPTIONS,
47+
server: 'src/main.server.ts',
48+
ssr: {
49+
entry: '/file.mjs',
50+
},
51+
});
52+
53+
const { result } = await harness.executeOnce();
54+
expect(result?.success).toBeTrue();
55+
harness.expectFile('dist/server.mjs').toExist();
56+
});
57+
});
58+
});

Diff for: packages/angular_devkit/build_angular/src/tools/esbuild/application-code-bundle.ts

+38-29
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export function createServerCodeBundleOptions(
9797
target: string[],
9898
sourceFileCache: SourceFileCache,
9999
): BuildOptions {
100-
const { jit, serverEntryPoint, workspaceRoot } = options;
100+
const { jit, serverEntryPoint, workspaceRoot, ssrOptions } = options;
101101

102102
assert(
103103
serverEntryPoint,
@@ -110,7 +110,15 @@ export function createServerCodeBundleOptions(
110110
sourceFileCache,
111111
);
112112

113-
const namespace = 'angular:server-entry';
113+
const namespace = 'angular:main-server';
114+
const entryPoints: Record<string, string> = {
115+
'main.server': namespace,
116+
};
117+
118+
const ssrEntryPoint = ssrOptions?.entry;
119+
if (ssrEntryPoint) {
120+
entryPoints['server'] = ssrEntryPoint;
121+
}
114122

115123
const buildOptions: BuildOptions = {
116124
...getEsBuildCommonOptions(options),
@@ -131,9 +139,7 @@ export function createServerCodeBundleOptions(
131139
`globalThis['require'] ??= createRequire(import.meta.url);`,
132140
].join('\n'),
133141
},
134-
entryPoints: {
135-
'server': namespace,
136-
},
142+
entryPoints,
137143
supported: getFeatureSupport(target),
138144
plugins: [
139145
createSourcemapIngorelistPlugin(),
@@ -143,30 +149,6 @@ export function createServerCodeBundleOptions(
143149
// Component stylesheet options
144150
styleOptions,
145151
),
146-
createVirtualModulePlugin({
147-
namespace,
148-
loadContent: () => {
149-
const mainServerEntryPoint = path
150-
.relative(workspaceRoot, serverEntryPoint)
151-
.replace(/\\/g, '/');
152-
const importAndExportDec: string[] = [
153-
`import '@angular/platform-server/init';`,
154-
`import moduleOrBootstrapFn from './${mainServerEntryPoint}';`,
155-
`export default moduleOrBootstrapFn;`,
156-
`export { renderApplication, renderModule, ɵSERVER_CONTEXT } from '@angular/platform-server';`,
157-
];
158-
159-
if (jit) {
160-
importAndExportDec.unshift(`import '@angular/compiler';`);
161-
}
162-
163-
return {
164-
contents: importAndExportDec.join('\n'),
165-
loader: 'js',
166-
resolveDir: workspaceRoot,
167-
};
168-
},
169-
}),
170152
],
171153
};
172154

@@ -177,6 +159,33 @@ export function createServerCodeBundleOptions(
177159
buildOptions.plugins.push(createRxjsEsmResolutionPlugin());
178160
}
179161

162+
buildOptions.plugins.push(
163+
createVirtualModulePlugin({
164+
namespace,
165+
loadContent: () => {
166+
const mainServerEntryPoint = path
167+
.relative(workspaceRoot, serverEntryPoint)
168+
.replace(/\\/g, '/');
169+
const importAndExportDec: string[] = [
170+
`import '@angular/platform-server/init';`,
171+
`import moduleOrBootstrapFn from './${mainServerEntryPoint}';`,
172+
`export default moduleOrBootstrapFn;`,
173+
`export { renderApplication, renderModule, ɵSERVER_CONTEXT } from '@angular/platform-server';`,
174+
];
175+
176+
if (jit) {
177+
importAndExportDec.unshift(`import '@angular/compiler';`);
178+
}
179+
180+
return {
181+
contents: importAndExportDec.join('\n'),
182+
loader: 'js',
183+
resolveDir: workspaceRoot,
184+
};
185+
},
186+
}),
187+
);
188+
180189
return buildOptions;
181190
}
182191

Diff for: packages/angular_devkit/build_angular/src/utils/ssg/render-worker.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ async function render({ route, serverContext }: RenderOptions): Promise<RenderRe
6363
ɵSERVER_CONTEXT,
6464
renderModule,
6565
renderApplication,
66-
} = await loadEsmModule<BundleExports>('./server.mjs');
66+
} = await loadEsmModule<BundleExports>('./main.server.mjs');
6767

6868
assert(ɵSERVER_CONTEXT, `ɵSERVER_CONTEXT was not exported.`);
6969

Diff for: tests/legacy-cli/e2e_runner.ts

+12-9
Original file line numberDiff line numberDiff line change
@@ -375,14 +375,17 @@ async function findPackageTars(): Promise<{ [pkg: string]: PkgInfo }> {
375375
}),
376376
);
377377

378-
return pkgs.reduce((all, pkg, i) => {
379-
const json = pkgJsons[i].toString('utf8');
380-
const { name, version } = JSON.parse(json);
381-
if (!name) {
382-
throw new Error(`Package ${pkg} - package.json name/version not found`);
383-
}
378+
return pkgs.reduce(
379+
(all, pkg, i) => {
380+
const json = pkgJsons[i].toString('utf8');
381+
const { name, version } = JSON.parse(json);
382+
if (!name) {
383+
throw new Error(`Package ${pkg} - package.json name/version not found`);
384+
}
384385

385-
all[name] = { path: realpathSync(pkg), name, version };
386-
return all;
387-
}, {} as { [pkg: string]: PkgInfo });
386+
all[name] = { path: realpathSync(pkg), name, version };
387+
return all;
388+
},
389+
{} as { [pkg: string]: PkgInfo },
390+
);
388391
}

0 commit comments

Comments
 (0)