Skip to content

Commit

Permalink
fix(@angular-devkit/build-angular): do not generate an index.html f…
Browse files Browse the repository at this point in the history
…ile 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"
      }
    }
  }
}
```
  • Loading branch information
alan-agius4 committed Apr 10, 2024
1 parent 733fba2 commit 2acf95a
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -194,35 +194,6 @@ export async function normalizeOptions(
? undefined
: await getTailwindConfig(searchDirectories, workspaceRoot, context);

const globalStyles = normalizeGlobalEntries(options.styles, 'styles');
const globalScripts = normalizeGlobalEntries(options.scripts, 'scripts');

let indexHtmlOptions;
// index can never have a value of `true` but in the schema it's of type `boolean`.
if (typeof options.index !== 'boolean') {
indexHtmlOptions = {
input: path.join(
workspaceRoot,
typeof options.index === 'string' ? options.index : options.index.input,
),
// The output file will be created within the configured output path
output:
typeof options.index === 'string'
? path.basename(options.index)
: options.index.output || 'index.html',
insertionOrder: [
['polyfills', true],
...globalStyles.filter((s) => s.initial).map((s) => [s.name, false]),
...globalScripts.filter((s) => s.initial).map((s) => [s.name, false]),
['main', true],
// [name, esm]
] as [string, boolean][],
transformer: extensions?.indexHtmlTransformer,
// Preload initial defaults to true
preloadInitial: typeof options.index !== 'object' || (options.index.preloadInitial ?? true),
};
}

let serverEntryPoint: string | undefined;
if (options.server) {
serverEntryPoint = path.join(workspaceRoot, options.server);
Expand Down Expand Up @@ -259,10 +230,57 @@ export async function normalizeOptions(
};
}

if ((appShellOptions || ssrOptions || prerenderOptions) && !serverEntryPoint) {
throw new Error(
'The "server" option is required when enabling "ssr", "prerender" or "app-shell".',
);
const globalStyles = normalizeGlobalEntries(options.styles, 'styles');
const globalScripts = normalizeGlobalEntries(options.scripts, 'scripts');
let indexHtmlOptions;
// index can never have a value of `true` but in the schema it's of type `boolean`.
if (typeof options.index !== 'boolean') {
let indexOutput: string;
// The output file will be created within the configured output path
if (typeof options.index === 'string') {
/**
* If SSR is activated, create a distinct entry file for the `index.html`.
* This is necessary because numerous server/cloud providers automatically serve the `index.html` as a static file
* if it exists (handling SSG).
* 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.csr.html' : indexBaseName;
} else {
indexOutput = options.index.output || 'index.html';
}

indexHtmlOptions = {
input: path.join(
workspaceRoot,
typeof options.index === 'string' ? options.index : options.index.input,
),
output: indexOutput,
insertionOrder: [
['polyfills', true],
...globalStyles.filter((s) => s.initial).map((s) => [s.name, false]),
...globalScripts.filter((s) => s.initial).map((s) => [s.name, false]),
['main', true],
// [name, esm]
] as [string, boolean][],
transformer: extensions?.indexHtmlTransformer,
// Preload initial defaults to true
preloadInitial: typeof options.index !== 'object' || (options.index.preloadInitial ?? true),
};
}

if (appShellOptions || ssrOptions || prerenderOptions) {
if (!serverEntryPoint) {
throw new Error(
'The "server" option is required when enabling "ssr", "prerender" or "app-shell".',
);
}

if (!indexHtmlOptions) {
throw new Error(
'The "index" option cannot be set to false when enabling "ssr", "prerender" or "app-shell".',
);
}
}

// Initial options to keep
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,11 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
await harness.modifyFile('src/tsconfig.app.json', (content) => {
const tsConfig = JSON.parse(content);
tsConfig.files ??= [];
tsConfig.files.push('main.server.ts', 'server.ts');
tsConfig.files.push('main.server.ts');

return JSON.stringify(tsConfig);
});

await harness.writeFile('src/server.ts', `console.log('Hello!');`);

harness.useTarget('build', {
...BASE_OPTIONS,
server: 'src/main.server.ts',
Expand All @@ -57,7 +55,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
harness.expectFile('dist/server/main.server.mjs').toExist();

harness
.expectFile('dist/browser/index.html')
.expectFile('dist/browser/index.csr.html')
.content.not.toMatch(/<link rel="modulepreload" href="chunk-\.+\.mjs">/);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
});

const buildCount = await harness
.execute({ outputLogsOnFailure: true })
.execute({ outputLogsOnFailure: false })
.pipe(
timeout(BUILD_TIMEOUT),
concatMap(async ({ result, logs }, index) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,5 +205,27 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
harness.expectFile('dist/browser/index.html').content.not.toContain('modulepreload');
harness.expectFile('dist/browser/index.html').content.not.toContain('chunk-');
});

it(`should generate 'index.csr.html' instead of 'index.html' by default when ssr is enabled.`, async () => {
await harness.modifyFile('src/tsconfig.app.json', (content) => {
const tsConfig = JSON.parse(content);
tsConfig.files ??= [];
tsConfig.files.push('main.server.ts');

return JSON.stringify(tsConfig);
});

harness.useTarget('build', {
...BASE_OPTIONS,
server: 'src/main.server.ts',
ssr: true,
});

const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();
harness.expectDirectory('dist/server').toExist();
harness.expectFile('dist/browser/index.csr.html').toExist();
harness.expectFile('dist/browser/index.html').toNotExist();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ export function app(): express.Express {
// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /<%= browserDistDirectory %>
server.get('*.*', express.static(browserDistFolder, {
maxAge: '1y'
server.get('**', express.static(browserDistFolder, {
maxAge: '1y',
index: 'index.html',
}));

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

commonEngine
Expand Down

0 comments on commit 2acf95a

Please sign in to comment.