Skip to content

Commit

Permalink
feat(@angular-devkit/build-angular): improve handling of nomodules an…
Browse files Browse the repository at this point in the history
…d modules in index generation

Since when having differential loading we already know which files originated from which build. We shouldn't need to merge and transform this data.

With this change, the index generator accepts a couple of new inputs.
1. `files` - used for Js and CSS files which require nomodule nor module attributes
2. `moduleFiles` - Js files that need to have a `module` attribute
3. `noModuleFiles`  - Js files that need to have a `nomodule` attribute
4. `entrypoints` - used to sort the insertion of files in the HTML file
  • Loading branch information
Alan Agius authored and alexeagle committed Apr 10, 2019
1 parent 55863a5 commit 0baa9c8
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 112 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,37 +16,39 @@ import {
const parse5 = require('parse5');


export type LoadOutputFileFunctionType = (file: string) => string;
export type LoadOutputFileFunctionType = (file: string) => Promise<string>;

export interface GenerateIndexHtmlParams {
// input file name (e. g. index.html)
export interface GenerateIndexHtmlOptions {
/* Input file name (e. g. index.html) */
input: string;
// contents of input
/* Input contents */
inputContent: string;
baseHref?: string;
deployUrl?: string;
sri: boolean;
// the files emitted by the build
unfilteredSortedFiles: CompiledFileInfo[];
// additional files that should be added using nomodule
noModuleFiles: Set<string>;
// function that loads a file
// This allows us to use different routines within the IndexHtmlWebpackPlugin and
// when used without this plugin.
/*
* Files emitted by the build.
* Js files will be added without 'nomodule' nor 'module'.
*/
files: FileInfo[];
/** Files that should be added using 'nomodule'. */
noModuleFiles?: FileInfo[];
/** Files that should be added using 'module'. */
moduleFiles?: FileInfo[];
/*
* Function that loads a file used.
* This allows us to use different routines within the IndexHtmlWebpackPlugin and
* when used without this plugin.
*/
loadOutputFile: LoadOutputFileFunctionType;
/** Used to sort the inseration of files in the HTML file */
entrypoints: string[];
}

/*
* Defines the type of script tag that is generated for the script reference
* nomodule: <script src="..." nomodule></script>
* module: <script src="..." type="module"></script>
* none: <script src="..."></script>
*/
export type CompiledFileType = 'nomodule' | 'module' | 'none';

export interface CompiledFileInfo {
file: string;
type: CompiledFileType;
export interface FileInfo {
fileName: string;
name: string;
extension: string;
}

/*
Expand All @@ -55,41 +57,32 @@ export interface CompiledFileInfo {
* after processing several configurations in order to build different sets of
* bundles for differential serving.
*/
export function generateIndexHtml(params: GenerateIndexHtmlParams): Source {

const loadOutputFile = params.loadOutputFile;

// Filter files
const existingFiles = new Set<string>();
const stylesheets: string[] = [];
const scripts: string[] = [];

const fileNames = params.unfilteredSortedFiles.map(f => f.file);
const moduleFilesArray = params.unfilteredSortedFiles
.filter(f => f.type === 'module')
.map(f => f.file);

const moduleFiles = new Set<string>(moduleFilesArray);

const noModuleFilesArray = params.unfilteredSortedFiles
.filter(f => f.type === 'nomodule')
.map(f => f.file);

noModuleFilesArray.push(...params.noModuleFiles);

const noModuleFiles = new Set<string>(noModuleFilesArray);

for (const file of fileNames) {

if (existingFiles.has(file)) {
continue;
}
existingFiles.add(file);

if (file.endsWith('.js')) {
scripts.push(file);
} else if (file.endsWith('.css')) {
stylesheets.push(file);
export async function generateIndexHtml(params: GenerateIndexHtmlOptions): Promise<Source> {
const {
loadOutputFile,
files,
noModuleFiles = [],
moduleFiles = [],
entrypoints,
} = params;

const stylesheets = new Set<string>();
const scripts = new Set<string>();

// Sort files in the order we want to insert them by entrypoint and dedupes duplicates
const mergedFiles = [...noModuleFiles, ...moduleFiles, ...files];
for (const entrypoint of entrypoints) {
for (const { extension, fileName, name } of mergedFiles) {
if (name !== entrypoint) { continue; }

switch (extension) {
case '.js':
scripts.add(fileName);
break;
case '.css':
stylesheets.add(fileName);
break;
}
}
}

Expand All @@ -103,8 +96,7 @@ export function generateIndexHtml(params: GenerateIndexHtmlParams): Source {
for (const htmlChild of docChild.childNodes) {
if (htmlChild.tagName === 'head') {
headElement = htmlChild;
}
if (htmlChild.tagName === 'body') {
} else if (htmlChild.tagName === 'body') {
bodyElement = htmlChild;
}
}
Expand Down Expand Up @@ -143,16 +135,19 @@ export function generateIndexHtml(params: GenerateIndexHtmlParams): Source {
{ name: 'src', value: (params.deployUrl || '') + script },
];

if (noModuleFiles.has(script)) {
attrs.push({ name: 'nomodule', value: null });
}

if (moduleFiles.has(script)) {
attrs.push({ name: 'type', value: 'module' });
// We want to include nomodule or module when a file is not common amongs all
// such as runtime.js
const scriptPredictor = ({ fileName }: FileInfo): boolean => fileName === script;
if (!files.some(scriptPredictor)) {
if (noModuleFiles.some(scriptPredictor)) {
attrs.push({ name: 'nomodule', value: null });
} else if (moduleFiles.some(scriptPredictor)) {
attrs.push({ name: 'type', value: 'module' });
}
}

if (params.sri) {
const content = loadOutputFile(script);
const content = await loadOutputFile(script);
attrs.push(..._generateSriAttributes(content));
}

Expand Down Expand Up @@ -222,7 +217,7 @@ export function generateIndexHtml(params: GenerateIndexHtmlParams): Source {
];

if (params.sri) {
const content = loadOutputFile(stylesheet);
const content = await loadOutputFile(stylesheet);
attrs.push(..._generateSriAttributes(content));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,92 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import { generateIndexHtml } from './generate-index-html';
import { tags } from '@angular-devkit/core';
import { FileInfo, GenerateIndexHtmlOptions, generateIndexHtml } from './generate-index-html';

describe('index-html-webpack-plugin', () => {
const indexGeneratorOptions: GenerateIndexHtmlOptions = {
input: 'index.html',
inputContent: '<html><head></head><body></body></html>',
baseHref: '/',
sri: false,
files: [],
loadOutputFile: async (_fileName: string) => '',
entrypoints: ['polyfills', 'main', 'styles'],
};

it('can generate index.html', () => {
const oneLineHtml = (html: TemplateStringsArray) =>
tags.stripIndents`${html}`.replace(/(\>\s+)/g, '>');

it('can generate index.html', async () => {
const source = generateIndexHtml({
input: 'index.html',
inputContent: '<html><head></head><body></body></html>',
baseHref: '/',
sri: false,
loadOutputFile: (fileName: string) => '',
unfilteredSortedFiles: [
{file: 'a.js', type: 'module'},
{file: 'b.js', type: 'nomodule'},
{file: 'c.js', type: 'none'},
...indexGeneratorOptions,
files: [
{ fileName: 'styles.css', extension: '.css', name: 'styles' },
{ fileName: 'runtime.js', extension: '.js', name: 'main' },
{ fileName: 'main.js', extension: '.js', name: 'main' },
{ fileName: 'runtime.js', extension: '.js', name: 'polyfills' },
{ fileName: 'polyfills.js', extension: '.js', name: 'polyfills' },
],
noModuleFiles: new Set<string>(),
});

const html = source.source();
const html = (await source).source();
expect(html).toEqual(oneLineHtml`
<html>
<head><base href="/">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<script src="runtime.js"></script>
<script src="polyfills.js"></script>
<script src="main.js"></script>
</body>
</html>
`);
});

it('should emit correct script tags when having module and non-module js', async () => {
const es2015JsFiles: FileInfo[] = [
{ fileName: 'runtime-es2015.js', extension: '.js', name: 'main' },
{ fileName: 'main-es2015.js', extension: '.js', name: 'main' },
{ fileName: 'runtime-es2015.js', extension: '.js', name: 'polyfills' },
{ fileName: 'polyfills-es2015.js', extension: '.js', name: 'polyfills' },
];

const es5JsFiles: FileInfo[] = [
{ fileName: 'runtime-es5.js', extension: '.js', name: 'main' },
{ fileName: 'main-es5.js', extension: '.js', name: 'main' },
{ fileName: 'runtime-es5.js', extension: '.js', name: 'polyfills' },
{ fileName: 'polyfills-es5.js', extension: '.js', name: 'polyfills' },
];

expect(html).toContain('<script src="a.js" type="module"></script>');
expect(html).toContain('<script src="b.js" nomodule></script>');
expect(html).toContain('<script src="c.js"></script>');
const source = generateIndexHtml({
...indexGeneratorOptions,
files: [
{ fileName: 'styles.css', extension: '.css', name: 'styles' },
{ fileName: 'styles.css', extension: '.css', name: 'styles' },
],
moduleFiles: es2015JsFiles,
noModuleFiles: es5JsFiles,
});

const html = (await source).source();
expect(html).toEqual(oneLineHtml`
<html>
<head>
<base href="/">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<script src="runtime-es5.js" nomodule></script>
<script src="polyfills-es5.js" nomodule></script>
<script src="runtime-es2015.js" type="module"></script>
<script src="polyfills-es2015.js" type="module"></script>
<script src="main-es5.js" nomodule></script>
<script src="main-es2015.js" type="module"></script>
</body>
</html>
`);
});

});
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as path from 'path';
import { Compiler, compilation } from 'webpack';
import { CompiledFileInfo, CompiledFileType, generateIndexHtml } from './generate-index-html';
import { FileInfo, generateIndexHtml } from './generate-index-html';

export interface IndexHtmlWebpackPluginOptions {
input: string;
Expand Down Expand Up @@ -65,45 +66,36 @@ export class IndexHtmlWebpackPlugin {
(compilation as compilation.Compilation & { fileDependencies: Set<string> })
.fileDependencies.add(this._options.input);

const loadOutputFile = (name: string) => compilation.assets[name].source();

// Get all files for selected entrypoints
const unfilteredSortedFiles: string[] = [];
const noModuleFiles = new Set<string>();
const otherFiles = new Set<string>();
for (const entryName of this._options.entrypoints) {
const entrypoint = compilation.entrypoints.get(entryName);
if (entrypoint && entrypoint.getFiles) {
const files: string[] = entrypoint.getFiles() || [];
unfilteredSortedFiles.push(...files);

if (this._options.noModuleEntrypoints.includes(entryName)) {
files.forEach(file => noModuleFiles.add(file));
} else {
files.forEach(file => otherFiles.add(file));
}
const files: FileInfo[] = [];
const noModuleFiles: FileInfo[] = [];

for (const [entryName, entrypoint] of compilation.entrypoints) {
const entryFiles: FileInfo[] = (entrypoint && entrypoint.getFiles() || [])
.map((f: string): FileInfo => ({
name: entryName,
fileName: f,
extension: path.extname(f),
}));

if (this._options.noModuleEntrypoints.includes(entryName)) {
noModuleFiles.push(...entryFiles);
} else {
files.push(...entryFiles);
}
}

// Clean out files that are used in all types of entrypoints
otherFiles.forEach(file => noModuleFiles.delete(file));

// If this plugin calls generateIndexHtml it always uses type: 'none' to align with
// its original behavior.
const compiledFiles: CompiledFileInfo[] = unfilteredSortedFiles.map(f => ({
file: f,
type: 'none' as CompiledFileType,
}));

const indexSource = generateIndexHtml({
const loadOutputFile = (name: string) => compilation.assets[name].source();
const indexSource = await generateIndexHtml({
input: this._options.input,
inputContent,
baseHref: this._options.baseHref,
deployUrl: this._options.deployUrl,
sri: this._options.sri,
unfilteredSortedFiles: compiledFiles,
files,
noModuleFiles,
loadOutputFile,
entrypoints: this._options.entrypoints,
});

// Add to compilation assets
Expand Down

0 comments on commit 0baa9c8

Please sign in to comment.