Skip to content

Commit 56d9adc

Browse files
alan-agius4Keen Yee Liau
authored andcommitted
fix(@angular-devkit/build-angular): nomodule polyfill for Safari 10.1 and iOS Safari 10.3
The nomodule polyfill needs to be loaded prior to any script and be outside of webpack compilation because otherwise webpack will cause the script to be wrapped in `window["webpackJsonp"]` which causes it to fail. This polyfill will only be injected when the either Safari 10.1 or iOS Safari 10.3 support is required, which is based on the browsers defined in browserslist file. Fixes #14680
1 parent bb8bca1 commit 56d9adc

File tree

13 files changed

+348
-138
lines changed

13 files changed

+348
-138
lines changed
Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,8 @@
1-
/**
2-
* Safari 10.1 supports modules, but does not support the `nomodule` attribute - it will
3-
* load <script nomodule> anyway. This snippet solve this problem, but only for script
4-
* tags that load external code, e.g.: <script nomodule src="nomodule.js"></script>
5-
*
6-
* Again: this will **not** prevent inline script, e.g.:
7-
* <script nomodule>alert('no modules');</script>.
8-
*
9-
* This workaround is possible because Safari supports the non-standard 'beforeload' event.
10-
* This allows us to trap the module and nomodule load.
11-
*
12-
* Note also that `nomodule` is supported in later versions of Safari - it's just 10.1 that
13-
* omits this attribute.
14-
*/
15-
(function() {
1+
(function () {
162
var check = document.createElement('script');
173
if (!('noModule' in check) && 'onbeforeload' in check) {
184
var support = false;
19-
document.addEventListener('beforeload', function(e) {
5+
document.addEventListener('beforeload', function (e) {
206
if (e.target === check) {
217
support = true;
228
} else if (!e.target.hasAttribute('nomodule') || !support) {
@@ -30,4 +16,4 @@
3016
document.head.appendChild(check);
3117
check.remove();
3218
}
33-
}());
19+
}());

packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import {
1616
Output,
1717
debug,
1818
} from 'webpack';
19-
import { AssetPatternClass } from '../../../browser/schema';
20-
import { isEs5SupportNeeded } from '../../../utils/differential-loading';
19+
import { AssetPatternClass, ExtraEntryPoint } from '../../../browser/schema';
20+
import { BuildBrowserFeatures } from '../../../utils/build-browser-features';
2121
import { BundleBudgetPlugin } from '../../plugins/bundle-budget';
2222
import { CleanCssWebpackPlugin } from '../../plugins/cleancss-webpack-plugin';
2323
import { NamedLazyChunksPlugin } from '../../plugins/named-chunks-plugin';
@@ -41,7 +41,7 @@ export const buildOptimizerLoader: string = g['_DevKitIsLocal']
4141

4242
// tslint:disable-next-line:no-big-function
4343
export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
44-
const { root, projectRoot, buildOptions } = wco;
44+
const { root, projectRoot, buildOptions, tsConfig } = wco;
4545
const { styles: stylesOptimization, scripts: scriptsOptimization } = buildOptions.optimization;
4646
const {
4747
styles: stylesSourceMap,
@@ -68,25 +68,39 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
6868
}
6969

7070
if (wco.buildOptions.platform !== 'server') {
71-
const es5Polyfills = path.join(__dirname, '..', 'es5-polyfills.js');
72-
const es5JitPolyfills = path.join(__dirname, '..', 'es5-jit-polyfills.js');
73-
if (targetInFileName) {
74-
// For differential loading we don't need to have 2 polyfill bundles
75-
if (buildOptions.scriptTargetOverride === ScriptTarget.ES2015) {
76-
entryPoints['polyfills'] = [path.join(__dirname, '..', 'safari-nomodule.js')];
77-
} else {
78-
entryPoints['polyfills'] = [es5Polyfills];
79-
if (!buildOptions.aot) {
80-
entryPoints['polyfills'].push(es5JitPolyfills);
71+
const buildBrowserFeatures = new BuildBrowserFeatures(
72+
projectRoot,
73+
tsConfig.options.target || ScriptTarget.ES5,
74+
);
75+
if ((buildOptions.scriptTargetOverride || tsConfig.options.target) === ScriptTarget.ES5) {
76+
if (buildOptions.es5BrowserSupport ||
77+
(
78+
buildOptions.es5BrowserSupport === undefined &&
79+
buildBrowserFeatures.isEs5SupportNeeded()
80+
)
81+
) {
82+
// The nomodule polyfill needs to be inject prior to any script and be
83+
// outside of webpack compilation because otherwise webpack will cause the
84+
// script to be wrapped in window["webpackJsonp"] which causes this to fail.
85+
if (buildBrowserFeatures.isNoModulePolyfillNeeded()) {
86+
const noModuleScript: ExtraEntryPoint = {
87+
bundleName: 'polyfills-nomodule-es5',
88+
input: path.join(__dirname, '..', 'safari-nomodule.js'),
89+
};
90+
buildOptions.scripts = buildOptions.scripts
91+
? [...buildOptions.scripts, noModuleScript]
92+
: [noModuleScript];
8193
}
82-
}
83-
} else {
84-
// For NON differential loading we want to have 2 polyfill bundles
85-
if (buildOptions.es5BrowserSupport
86-
|| (buildOptions.es5BrowserSupport === undefined && isEs5SupportNeeded(projectRoot))) {
87-
entryPoints['polyfills-es5'] = [es5Polyfills];
94+
95+
// For differential loading we don't need to generate a seperate polyfill file
96+
// because they will be loaded exclusivly based on module and nomodule
97+
const polyfillsChunkName = buildBrowserFeatures.isDifferentialLoadingNeeded()
98+
? 'polyfills'
99+
: 'polyfills-es5';
100+
101+
entryPoints[polyfillsChunkName] = [path.join(__dirname, '..', 'es5-polyfills.js')];
88102
if (!buildOptions.aot) {
89-
entryPoints['polyfills-es5'].push(es5JitPolyfills);
103+
entryPoints[polyfillsChunkName].push(path.join(__dirname, '..', 'es5-jit-polyfills.js'));
90104
}
91105
}
92106
}

packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/utils.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,11 @@
88
// tslint:disable
99
// TODO: cleanup this file, it's copied as is from Angular CLI.
1010

11-
import * as path from 'path';
1211
import { basename, normalize } from '@angular-devkit/core';
1312
import { ExtraEntryPoint, ExtraEntryPointClass } from '../../../browser/schema';
1413
import { SourceMapDevToolPlugin } from 'webpack';
1514
import { ScriptTarget } from 'typescript';
1615

17-
export const ngAppResolve = (resolvePath: string): string => {
18-
return path.resolve(process.cwd(), resolvePath);
19-
};
20-
2116
export interface HashFormat {
2217
chunk: string;
2318
extract: string;

packages/angular_devkit/build_angular/src/angular-cli-files/utilities/package-chunk-sort.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export function generateEntryPoints(
2626
};
2727

2828
const entryPoints = [
29+
'polyfills-nomodule-es5',
2930
'polyfills-es5',
3031
'polyfills',
3132
'sw-register',

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import {
4949
statsWarningsToString,
5050
} from '../angular-cli-files/utilities/stats';
5151
import { ExecutionTransformer } from '../transforms';
52-
import { deleteOutputDir, isEs5SupportNeeded } from '../utils';
52+
import { BuildBrowserFeatures, deleteOutputDir } from '../utils';
5353
import { Version } from '../utils/version';
5454
import { generateBrowserWebpackConfigFromContext } from '../utils/webpack-browser-config';
5555
import { Schema as BrowserBuilderSchema } from './schema';
@@ -193,9 +193,13 @@ export function buildWebpackBrowser(
193193
const tsConfigPath = path.resolve(getSystemPath(workspace.root), options.tsConfig);
194194
const tsConfig = readTsconfig(tsConfigPath);
195195

196-
if (isEs5SupportNeeded(projectRoot) &&
197-
tsConfig.options.target !== ScriptTarget.ES5 &&
198-
tsConfig.options.target !== ScriptTarget.ES2015) {
196+
const target = tsConfig.options.target || ScriptTarget.ES5;
197+
const buildBrowserFeatures = new BuildBrowserFeatures(
198+
getSystemPath(projectRoot),
199+
target,
200+
);
201+
202+
if (target > ScriptTarget.ES2015 && buildBrowserFeatures.isDifferentialLoadingNeeded()) {
199203
context.logger.warn(tags.stripIndent`
200204
WARNING: Using differential loading with targets ES5 and ES2016 or higher may
201205
cause problems. Browsers with support for ES2015 will load the ES2016+ scripts
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. 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 * as browserslist from 'browserslist';
10+
import { feature, features } from 'caniuse-lite';
11+
import * as ts from 'typescript';
12+
13+
export class BuildBrowserFeatures {
14+
private readonly _supportedBrowsers: string[];
15+
private readonly _es6TargetOrLater: boolean;
16+
17+
constructor(
18+
private projectRoot: string,
19+
private scriptTarget: ts.ScriptTarget,
20+
) {
21+
this._supportedBrowsers = browserslist(undefined, { path: this.projectRoot });
22+
this._es6TargetOrLater = this.scriptTarget > ts.ScriptTarget.ES5;
23+
}
24+
25+
/**
26+
* True, when one or more browsers requires ES5
27+
* support and the scirpt target is ES2015 or greater.
28+
*/
29+
isDifferentialLoadingNeeded(): boolean {
30+
return this._es6TargetOrLater && this.isEs5SupportNeeded();
31+
}
32+
33+
/**
34+
* True, when one or more browsers requires ES5 support
35+
*/
36+
isEs5SupportNeeded(): boolean {
37+
return !caniuse.isSupported('es6-module', this._supportedBrowsers.join(', '));
38+
}
39+
40+
/**
41+
* Safari 10.1 and iOS Safari 10.3 supports modules,
42+
* but does not support the `nomodule` attribute.
43+
* While return `true`, when support for Safari 10.1 and iOS Safari 10.3
44+
* is required and in differential loading is enabled.
45+
*/
46+
isNoModulePolyfillNeeded(): boolean {
47+
if (!this.isDifferentialLoadingNeeded()) {
48+
return false;
49+
}
50+
51+
const safariBrowsers = [
52+
'safari 10.1',
53+
'ios_saf 10.3',
54+
];
55+
56+
return this._supportedBrowsers.some(browser => safariBrowsers.includes(browser));
57+
}
58+
}

0 commit comments

Comments
 (0)