Skip to content

Commit 1dd399c

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 473b4c4 commit 1dd399c

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
@@ -18,8 +18,8 @@ import {
1818
debug,
1919
} from 'webpack';
2020
import { RawSource } from 'webpack-sources';
21-
import { AssetPatternClass } from '../../../browser/schema';
22-
import { isEs5SupportNeeded } from '../../../utils/differential-loading';
21+
import { AssetPatternClass, ExtraEntryPoint } from '../../../browser/schema';
22+
import { BuildBrowserFeatures } from '../../../utils/build-browser-features';
2323
import { BundleBudgetPlugin } from '../../plugins/bundle-budget';
2424
import { CleanCssWebpackPlugin } from '../../plugins/cleancss-webpack-plugin';
2525
import { NamedLazyChunksPlugin } from '../../plugins/named-chunks-plugin';
@@ -40,7 +40,7 @@ export const buildOptimizerLoader: string = g['_DevKitIsLocal']
4040

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

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

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
@@ -57,7 +57,7 @@ import {
5757
statsWarningsToString,
5858
} from '../angular-cli-files/utilities/stats';
5959
import { ExecutionTransformer } from '../transforms';
60-
import { deleteOutputDir, isEs5SupportNeeded } from '../utils';
60+
import { BuildBrowserFeatures, deleteOutputDir } from '../utils';
6161
import { Version } from '../utils/version';
6262
import { generateBrowserWebpackConfigFromContext } from '../utils/webpack-browser-config';
6363
import { Schema as BrowserBuilderSchema } from './schema';
@@ -202,9 +202,13 @@ export function buildWebpackBrowser(
202202
const tsConfigPath = path.resolve(getSystemPath(workspace.root), options.tsConfig);
203203
const tsConfig = readTsconfig(tsConfigPath);
204204

205-
if (isEs5SupportNeeded(projectRoot) &&
206-
tsConfig.options.target !== ScriptTarget.ES5 &&
207-
tsConfig.options.target !== ScriptTarget.ES2015) {
205+
const target = tsConfig.options.target || ScriptTarget.ES5;
206+
const buildBrowserFeatures = new BuildBrowserFeatures(
207+
getSystemPath(projectRoot),
208+
target,
209+
);
210+
211+
if (target > ScriptTarget.ES2015 && buildBrowserFeatures.isDifferentialLoadingNeeded()) {
208212
context.logger.warn(tags.stripIndent`
209213
WARNING: Using differential loading with targets ES5 and ES2016 or higher may
210214
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)