Skip to content

Commit 8e97df3

Browse files
clydinKeen Yee Liau
authored and
Keen Yee Liau
committed
feat(@angular-devkit/build-angular): allow control of index output path
This allows the output path of an application's index HTML file to be controlled independently of the input file. The output path for the file will be considered relative to the application's configured output path. This allows an application to contain multiple input index files for different configurations and allow the output file to remain constant. This also enables the placement of the index file in a subdirectory within the output path or change the name of the output index file neither of which was previously possible.
1 parent 2c71af1 commit 8e97df3

File tree

7 files changed

+189
-17
lines changed

7 files changed

+189
-17
lines changed

packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ export interface BuildOptions {
6363
es5BrowserSupport?: boolean;
6464

6565
main: string;
66-
index: string;
6766
polyfills?: string;
6867
budgets: Budget[];
6968
assets: AssetPatternClass[];

packages/angular_devkit/build_angular/src/angular-cli-files/utilities/index-file/write-index-html.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import { EmittedFiles } from '@angular-devkit/build-webpack';
10-
import { Path, basename, getSystemPath, join, virtualFs } from '@angular-devkit/core';
10+
import { Path, dirname, getSystemPath, join, virtualFs } from '@angular-devkit/core';
1111
import { Observable, of } from 'rxjs';
1212
import { map, switchMap } from 'rxjs/operators';
1313
import { ExtraEntryPoint } from '../../../browser/schema';
@@ -66,15 +66,15 @@ export function writeIndexHtml({
6666
moduleFiles: filterAndMapBuildFiles(moduleFiles, '.js'),
6767
loadOutputFile: async filePath => {
6868
return host
69-
.read(join(outputPath, filePath))
69+
.read(join(dirname(outputPath), filePath))
7070
.pipe(map(data => virtualFs.fileBufferToString(data)))
7171
.toPromise();
7272
},
7373
}),
7474
),
7575
switchMap(content => (postTransform ? postTransform(content) : of(content))),
7676
map(content => virtualFs.stringToFileBuffer(content)),
77-
switchMap(content => host.write(join(outputPath, basename(indexPath)), content)),
77+
switchMap(content => host.write(outputPath, content)),
7878
);
7979
}
8080

packages/angular_devkit/build_angular/src/browser/index.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@ import {
5555
import { ExecutionTransformer } from '../transforms';
5656
import { BuildBrowserFeatures, deleteOutputDir } from '../utils';
5757
import { assertCompatibleAngularVersion } from '../utils/version';
58-
import { generateBrowserWebpackConfigFromContext } from '../utils/webpack-browser-config';
58+
import {
59+
generateBrowserWebpackConfigFromContext,
60+
getIndexInputFile,
61+
getIndexOutputFile,
62+
} from '../utils/webpack-browser-config';
5963
import { Schema as BrowserBuilderSchema } from './schema';
6064

6165
export type BrowserBuilderOutput = json.JsonObject &
@@ -252,8 +256,8 @@ export function buildWebpackBrowser(
252256

253257
return writeIndexHtml({
254258
host,
255-
outputPath: resolve(root, normalize(options.outputPath)),
256-
indexPath: join(root, options.index),
259+
outputPath: resolve(root, join(normalize(options.outputPath), getIndexOutputFile(options))),
260+
indexPath: join(root, getIndexInputFile(options)),
257261
files,
258262
noModuleFiles,
259263
moduleFiles,

packages/angular_devkit/build_angular/src/browser/schema.json

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,8 +270,31 @@
270270
"x-deprecated": true
271271
},
272272
"index": {
273-
"type": "string",
274-
"description": "The name of the index HTML file."
273+
"description": "Configures the generation of the application's HTML index.",
274+
"oneOf": [
275+
{
276+
"type": "string",
277+
"description": "The path of a file to use for the application's HTML index. The filename of the specified path will be used for the generated file and will be created in the root of the application's configured output path."
278+
},
279+
{
280+
"type": "object",
281+
"description": "",
282+
"properties": {
283+
"input": {
284+
"type": "string",
285+
"minLength": 1,
286+
"description": "The path of a file to use for the application's generated HTML index."
287+
},
288+
"output": {
289+
"type": "string",
290+
"minLength": 1,
291+
"default": "index.html",
292+
"description": "The output path of the application's generated HTML index file. The full provided path will be used and will be considered relative to the application's configured output path."
293+
}
294+
},
295+
"required": ["input"]
296+
}
297+
]
275298
},
276299
"statsJson": {
277300
"type": "boolean",

packages/angular_devkit/build_angular/src/dev-server/index.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { Schema as BrowserBuilderSchema } from '../browser/schema';
4040
import { ExecutionTransformer } from '../transforms';
4141
import { BuildBrowserFeatures, normalizeOptimization } from '../utils';
4242
import { assertCompatibleAngularVersion } from '../utils/version';
43+
import { getIndexInputFile, getIndexOutputFile } from '../utils/webpack-browser-config';
4344
import { Schema } from './schema';
4445
const open = require('open');
4546

@@ -190,7 +191,7 @@ export function serveWebpackBrowser(
190191
}
191192

192193
if (browserOptions.index) {
193-
const { scripts = [], styles = [], index, baseHref, tsConfig } = browserOptions;
194+
const { scripts = [], styles = [], baseHref, tsConfig } = browserOptions;
194195
const projectName = context.target
195196
? context.target.project
196197
: workspace.getDefaultProjectName();
@@ -214,8 +215,8 @@ export function serveWebpackBrowser(
214215

215216
webpackConfig.plugins.push(
216217
new IndexHtmlWebpackPlugin({
217-
input: path.resolve(root, index),
218-
output: path.basename(index),
218+
input: path.resolve(root, getIndexInputFile(browserOptions)),
219+
output: getIndexOutputFile(browserOptions),
219220
baseHref,
220221
moduleEntrypoints,
221222
entrypoints,
@@ -317,7 +318,7 @@ export function buildServerConfig(
317318
historyApiFallback:
318319
!!browserOptions.index &&
319320
({
320-
index: `${servePath}/${path.basename(browserOptions.index)}`,
321+
index: `${servePath}/${getIndexOutputFile(browserOptions)}`,
321322
disableDotRule: true,
322323
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
323324
rewrites: [

packages/angular_devkit/build_angular/src/utils/webpack-browser-config.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,19 @@ export async function generateBrowserWebpackConfigFromContext(
208208

209209
return { workspace, config };
210210
}
211+
212+
export function getIndexOutputFile(options: BrowserBuilderSchema): string {
213+
if (typeof options.index === 'string') {
214+
return path.basename(options.index);
215+
} else {
216+
return options.index.output || 'index.html';
217+
}
218+
}
219+
220+
export function getIndexInputFile(options: BrowserBuilderSchema): string {
221+
if (typeof options.index === 'string') {
222+
return options.index;
223+
} else {
224+
return options.index.input;
225+
}
226+
}

packages/angular_devkit/build_angular/test/browser/index_spec_large.ts

Lines changed: 133 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8-
98
import { Architect } from '@angular-devkit/architect';
10-
import { join, normalize, tags, virtualFs } from '@angular-devkit/core';
9+
import { join, normalize, tags, virtualFs, workspaces } from '@angular-devkit/core';
1110
import { BrowserBuilderOutput } from '../../src/browser';
1211
import { createArchitect, host } from '../utils';
1312

14-
15-
describe('Browser Builder works with BOM index.html', () => {
13+
// tslint:disable-next-line:no-big-function
14+
describe('Browser Builder index HTML processing', () => {
1615
const targetSpec = { project: 'app', target: 'build' };
1716
let architect: Architect;
1817

@@ -108,4 +107,134 @@ describe('Browser Builder works with BOM index.html', () => {
108107
);
109108
await run.stop();
110109
});
110+
111+
it('uses the input value from the index option longform', async () => {
112+
const { workspace } = await workspaces.readWorkspace(host.root(), workspaces.createWorkspaceHost(host));
113+
const app = workspace.projects.get('app');
114+
if (!app) {
115+
fail('Test application "app" not found.');
116+
117+
return;
118+
}
119+
const target = app.targets.get('build');
120+
if (!target) {
121+
fail('Test application "app" target "build" not found.');
122+
123+
return;
124+
}
125+
if (!target.options) {
126+
target.options = {};
127+
}
128+
target.options.index = { input: 'src/index-2.html' };
129+
await workspaces.writeWorkspace(workspace, workspaces.createWorkspaceHost(host));
130+
131+
host.writeMultipleFiles({
132+
'src/index-2.html': tags.oneLine`
133+
<html><head><base href="/"><%= csrf_meta_tags %></head>
134+
<body><app-root></app-root></body></html>
135+
`,
136+
});
137+
await host.delete(join(host.root(), normalize('src/index.html'))).toPromise();
138+
139+
// Recreate architect to use update angular.json
140+
architect = (await createArchitect(host.root())).architect;
141+
142+
const run = await architect.scheduleTarget(targetSpec);
143+
await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: true }));
144+
145+
const outputIndexPath = join(host.root(), 'dist', 'index.html');
146+
const content = await host.read(normalize(outputIndexPath)).toPromise();
147+
expect(virtualFs.fileBufferToString(content)).toBe(
148+
`<html><head><base href="/"><%= csrf_meta_tags %></head> `
149+
+ `<body><app-root></app-root><script src="runtime.js"></script>`
150+
+ `<script src="polyfills.js"></script><script src="styles.js"></script>`
151+
+ `<script src="vendor.js"></script><script src="main.js"></script></body></html>`,
152+
);
153+
});
154+
155+
it('uses the output value from the index option longform', async () => {
156+
const { workspace } = await workspaces.readWorkspace(host.root(), workspaces.createWorkspaceHost(host));
157+
const app = workspace.projects.get('app');
158+
if (!app) {
159+
fail('Test application "app" not found.');
160+
161+
return;
162+
}
163+
const target = app.targets.get('build');
164+
if (!target) {
165+
fail('Test application "app" target "build" not found.');
166+
167+
return;
168+
}
169+
if (!target.options) {
170+
target.options = {};
171+
}
172+
target.options.index = { input: 'src/index.html', output: 'main.html' };
173+
await workspaces.writeWorkspace(workspace, workspaces.createWorkspaceHost(host));
174+
175+
host.writeMultipleFiles({
176+
'src/index.html': tags.oneLine`
177+
<html><head><base href="/"></head>
178+
<body><app-root></app-root></body></html>
179+
`,
180+
});
181+
182+
// Recreate architect to use update angular.json
183+
architect = (await createArchitect(host.root())).architect;
184+
185+
const run = await architect.scheduleTarget(targetSpec);
186+
await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: true }));
187+
188+
const outputIndexPath = join(host.root(), 'dist', 'main.html');
189+
const content = await host.read(normalize(outputIndexPath)).toPromise();
190+
expect(virtualFs.fileBufferToString(content)).toBe(
191+
`<html><head><base href="/"></head> `
192+
+ `<body><app-root></app-root><script src="runtime.js"></script>`
193+
+ `<script src="polyfills.js"></script><script src="styles.js"></script>`
194+
+ `<script src="vendor.js"></script><script src="main.js"></script></body></html>`,
195+
);
196+
});
197+
198+
it('creates subdirectories for output value from the index option longform', async () => {
199+
const { workspace } = await workspaces.readWorkspace(host.root(), workspaces.createWorkspaceHost(host));
200+
const app = workspace.projects.get('app');
201+
if (!app) {
202+
fail('Test application "app" not found.');
203+
204+
return;
205+
}
206+
const target = app.targets.get('build');
207+
if (!target) {
208+
fail('Test application "app" target "build" not found.');
209+
210+
return;
211+
}
212+
if (!target.options) {
213+
target.options = {};
214+
}
215+
target.options.index = { input: 'src/index.html', output: 'extra/main.html' };
216+
await workspaces.writeWorkspace(workspace, workspaces.createWorkspaceHost(host));
217+
218+
host.writeMultipleFiles({
219+
'src/index.html': tags.oneLine`
220+
<html><head><base href="/"></head>
221+
<body><app-root></app-root></body></html>
222+
`,
223+
});
224+
225+
// Recreate architect to use update angular.json
226+
architect = (await createArchitect(host.root())).architect;
227+
228+
const run = await architect.scheduleTarget(targetSpec);
229+
await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: true }));
230+
231+
const outputIndexPath = join(host.root(), 'dist', 'extra', 'main.html');
232+
const content = await host.read(normalize(outputIndexPath)).toPromise();
233+
expect(virtualFs.fileBufferToString(content)).toBe(
234+
`<html><head><base href="/"></head> `
235+
+ `<body><app-root></app-root><script src="runtime.js"></script>`
236+
+ `<script src="polyfills.js"></script><script src="styles.js"></script>`
237+
+ `<script src="vendor.js"></script><script src="main.js"></script></body></html>`,
238+
);
239+
});
111240
});

0 commit comments

Comments
 (0)