Skip to content

Commit 2acf95a

Browse files
committed
fix(@angular-devkit/build-angular): do not generate an index.html file in the browser directory when using SSR.
BREAKING CHANGE: By default, the index.html file is no longer emitted in the browser directory when using the application builder with SSR. Instead, an index.csr.html file is emitted. This change is implemented because in many cases server and cloud providers incorrectly treat the index.html file as a statically generated page. If you still require the old behavior, you can use the `index` option to specify the `output` file name. ```json "architect": { "build": { "builder": "@angular-devkit/build-angular:application", "options": { "outputPath": "dist/my-app", "index": { "input": "src/index.html", "output": "index.html" } } } } ```
1 parent 733fba2 commit 2acf95a

File tree

5 files changed

+80
-41
lines changed

5 files changed

+80
-41
lines changed

packages/angular_devkit/build_angular/src/builders/application/options.ts

+51-33
Original file line numberDiff line numberDiff line change
@@ -194,35 +194,6 @@ export async function normalizeOptions(
194194
? undefined
195195
: await getTailwindConfig(searchDirectories, workspaceRoot, context);
196196

197-
const globalStyles = normalizeGlobalEntries(options.styles, 'styles');
198-
const globalScripts = normalizeGlobalEntries(options.scripts, 'scripts');
199-
200-
let indexHtmlOptions;
201-
// index can never have a value of `true` but in the schema it's of type `boolean`.
202-
if (typeof options.index !== 'boolean') {
203-
indexHtmlOptions = {
204-
input: path.join(
205-
workspaceRoot,
206-
typeof options.index === 'string' ? options.index : options.index.input,
207-
),
208-
// The output file will be created within the configured output path
209-
output:
210-
typeof options.index === 'string'
211-
? path.basename(options.index)
212-
: options.index.output || 'index.html',
213-
insertionOrder: [
214-
['polyfills', true],
215-
...globalStyles.filter((s) => s.initial).map((s) => [s.name, false]),
216-
...globalScripts.filter((s) => s.initial).map((s) => [s.name, false]),
217-
['main', true],
218-
// [name, esm]
219-
] as [string, boolean][],
220-
transformer: extensions?.indexHtmlTransformer,
221-
// Preload initial defaults to true
222-
preloadInitial: typeof options.index !== 'object' || (options.index.preloadInitial ?? true),
223-
};
224-
}
225-
226197
let serverEntryPoint: string | undefined;
227198
if (options.server) {
228199
serverEntryPoint = path.join(workspaceRoot, options.server);
@@ -259,10 +230,57 @@ export async function normalizeOptions(
259230
};
260231
}
261232

262-
if ((appShellOptions || ssrOptions || prerenderOptions) && !serverEntryPoint) {
263-
throw new Error(
264-
'The "server" option is required when enabling "ssr", "prerender" or "app-shell".',
265-
);
233+
const globalStyles = normalizeGlobalEntries(options.styles, 'styles');
234+
const globalScripts = normalizeGlobalEntries(options.scripts, 'scripts');
235+
let indexHtmlOptions;
236+
// index can never have a value of `true` but in the schema it's of type `boolean`.
237+
if (typeof options.index !== 'boolean') {
238+
let indexOutput: string;
239+
// The output file will be created within the configured output path
240+
if (typeof options.index === 'string') {
241+
/**
242+
* If SSR is activated, create a distinct entry file for the `index.html`.
243+
* This is necessary because numerous server/cloud providers automatically serve the `index.html` as a static file
244+
* if it exists (handling SSG).
245+
* For instance, accessing `foo.com/` would lead to `foo.com/index.html` being served instead of hitting the server.
246+
*/
247+
const indexBaseName = path.basename(options.index);
248+
indexOutput = ssrOptions && indexBaseName === 'index.html' ? 'index.csr.html' : indexBaseName;
249+
} else {
250+
indexOutput = options.index.output || 'index.html';
251+
}
252+
253+
indexHtmlOptions = {
254+
input: path.join(
255+
workspaceRoot,
256+
typeof options.index === 'string' ? options.index : options.index.input,
257+
),
258+
output: indexOutput,
259+
insertionOrder: [
260+
['polyfills', true],
261+
...globalStyles.filter((s) => s.initial).map((s) => [s.name, false]),
262+
...globalScripts.filter((s) => s.initial).map((s) => [s.name, false]),
263+
['main', true],
264+
// [name, esm]
265+
] as [string, boolean][],
266+
transformer: extensions?.indexHtmlTransformer,
267+
// Preload initial defaults to true
268+
preloadInitial: typeof options.index !== 'object' || (options.index.preloadInitial ?? true),
269+
};
270+
}
271+
272+
if (appShellOptions || ssrOptions || prerenderOptions) {
273+
if (!serverEntryPoint) {
274+
throw new Error(
275+
'The "server" option is required when enabling "ssr", "prerender" or "app-shell".',
276+
);
277+
}
278+
279+
if (!indexHtmlOptions) {
280+
throw new Error(
281+
'The "index" option cannot be set to false when enabling "ssr", "prerender" or "app-shell".',
282+
);
283+
}
266284
}
267285

268286
// Initial options to keep

packages/angular_devkit/build_angular/src/builders/application/tests/behavior/index-preload-hints_spec.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,11 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
3838
await harness.modifyFile('src/tsconfig.app.json', (content) => {
3939
const tsConfig = JSON.parse(content);
4040
tsConfig.files ??= [];
41-
tsConfig.files.push('main.server.ts', 'server.ts');
41+
tsConfig.files.push('main.server.ts');
4242

4343
return JSON.stringify(tsConfig);
4444
});
4545

46-
await harness.writeFile('src/server.ts', `console.log('Hello!');`);
47-
4846
harness.useTarget('build', {
4947
...BASE_OPTIONS,
5048
server: 'src/main.server.ts',
@@ -57,7 +55,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
5755
harness.expectFile('dist/server/main.server.mjs').toExist();
5856

5957
harness
60-
.expectFile('dist/browser/index.html')
58+
.expectFile('dist/browser/index.csr.html')
6159
.content.not.toMatch(/<link rel="modulepreload" href="chunk-\.+\.mjs">/);
6260
});
6361
});

packages/angular_devkit/build_angular/src/builders/application/tests/behavior/rebuild-errors_spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
315315
});
316316

317317
const buildCount = await harness
318-
.execute({ outputLogsOnFailure: true })
318+
.execute({ outputLogsOnFailure: false })
319319
.pipe(
320320
timeout(BUILD_TIMEOUT),
321321
concatMap(async ({ result, logs }, index) => {

packages/angular_devkit/build_angular/src/builders/application/tests/options/index_spec.ts

+22
Original file line numberDiff line numberDiff line change
@@ -205,5 +205,27 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
205205
harness.expectFile('dist/browser/index.html').content.not.toContain('modulepreload');
206206
harness.expectFile('dist/browser/index.html').content.not.toContain('chunk-');
207207
});
208+
209+
it(`should generate 'index.csr.html' instead of 'index.html' by default when ssr is enabled.`, async () => {
210+
await harness.modifyFile('src/tsconfig.app.json', (content) => {
211+
const tsConfig = JSON.parse(content);
212+
tsConfig.files ??= [];
213+
tsConfig.files.push('main.server.ts');
214+
215+
return JSON.stringify(tsConfig);
216+
});
217+
218+
harness.useTarget('build', {
219+
...BASE_OPTIONS,
220+
server: 'src/main.server.ts',
221+
ssr: true,
222+
});
223+
224+
const { result } = await harness.executeOnce();
225+
expect(result?.success).toBeTrue();
226+
harness.expectDirectory('dist/server').toExist();
227+
harness.expectFile('dist/browser/index.csr.html').toExist();
228+
harness.expectFile('dist/browser/index.html').toNotExist();
229+
});
208230
});
209231
});

packages/schematics/angular/ssr/files/application-builder/server.ts.template

+4-3
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ export function app(): express.Express {
2020
// Example Express Rest API endpoints
2121
// server.get('/api/**', (req, res) => { });
2222
// Serve static files from /<%= browserDistDirectory %>
23-
server.get('*.*', express.static(browserDistFolder, {
24-
maxAge: '1y'
23+
server.get('**', express.static(browserDistFolder, {
24+
maxAge: '1y',
25+
index: 'index.html',
2526
}));
2627

2728
// All regular routes use the Angular engine
28-
server.get('*', (req, res, next) => {
29+
server.get('**', (req, res, next) => {
2930
const { protocol, originalUrl, baseUrl, headers } = req;
3031

3132
commonEngine

0 commit comments

Comments
 (0)