Skip to content

DL fixes for safari and false negatives for ES6 module support #14716

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
"@bazel/typescript": "~0.26.0",
"@ngtools/json-schema": "^1.1.0",
"@types/browserslist": "^4.4.0",
"@types/caniuse-api": "^3.0.0",
"@types/caniuse-lite": "^1.0.0",
"@types/copy-webpack-plugin": "^4.4.1",
"@types/express": "^4.16.0",
"@types/glob": "^7.0.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/angular_devkit/build_angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"ajv": "6.10.0",
"autoprefixer": "9.6.0",
"browserslist": "4.6.2",
"caniuse-api": "3.0.0",
"caniuse-lite": "1.0.30000974",
"circular-dependency-plugin": "5.0.2",
"clean-css": "4.2.1",
"copy-webpack-plugin": "5.0.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,8 @@
/**
* Safari 10.1 supports modules, but does not support the `nomodule` attribute - it will
* load <script nomodule> anyway. This snippet solve this problem, but only for script
* tags that load external code, e.g.: <script nomodule src="nomodule.js"></script>
*
* Again: this will **not** prevent inline script, e.g.:
* <script nomodule>alert('no modules');</script>.
*
* This workaround is possible because Safari supports the non-standard 'beforeload' event.
* This allows us to trap the module and nomodule load.
*
* Note also that `nomodule` is supported in later versions of Safari - it's just 10.1 that
* omits this attribute.
*/
(function() {
(function () {
var check = document.createElement('script');
if (!('noModule' in check) && 'onbeforeload' in check) {
var support = false;
document.addEventListener('beforeload', function(e) {
document.addEventListener('beforeload', function (e) {
if (e.target === check) {
support = true;
} else if (!e.target.hasAttribute('nomodule') || !support) {
Expand All @@ -30,4 +16,4 @@
document.head.appendChild(check);
check.remove();
}
}());
}());
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import {
debug,
} from 'webpack';
import { RawSource } from 'webpack-sources';
import { AssetPatternClass } from '../../../browser/schema';
import { isEs5SupportNeeded } from '../../../utils/differential-loading';
import { AssetPatternClass, ExtraEntryPoint } from '../../../browser/schema';
import { BuildBrowserFeatures } from '../../../utils/build-browser-features';
import { BundleBudgetPlugin } from '../../plugins/bundle-budget';
import { CleanCssWebpackPlugin } from '../../plugins/cleancss-webpack-plugin';
import { NamedLazyChunksPlugin } from '../../plugins/named-chunks-plugin';
Expand All @@ -40,7 +40,7 @@ export const buildOptimizerLoader: string = g['_DevKitIsLocal']

// tslint:disable-next-line:no-big-function
export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
const { root, projectRoot, buildOptions } = wco;
const { root, projectRoot, buildOptions, tsConfig } = wco;
const { styles: stylesOptimization, scripts: scriptsOptimization } = buildOptions.optimization;
const {
styles: stylesSourceMap,
Expand All @@ -67,25 +67,39 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
}

if (wco.buildOptions.platform !== 'server') {
const es5Polyfills = path.join(__dirname, '..', 'es5-polyfills.js');
const es5JitPolyfills = path.join(__dirname, '..', 'es5-jit-polyfills.js');
if (targetInFileName) {
// For differential loading we don't need to have 2 polyfill bundles
if (buildOptions.scriptTargetOverride === ScriptTarget.ES2015) {
entryPoints['polyfills'] = [path.join(__dirname, '..', 'safari-nomodule.js')];
} else {
entryPoints['polyfills'] = [es5Polyfills];
if (!buildOptions.aot) {
entryPoints['polyfills'].push(es5JitPolyfills);
const buildBrowserFeatures = new BuildBrowserFeatures(
projectRoot,
tsConfig.options.target || ScriptTarget.ES5,
);
if ((buildOptions.scriptTargetOverride || tsConfig.options.target) === ScriptTarget.ES5) {
if (buildOptions.es5BrowserSupport ||
(
buildOptions.es5BrowserSupport === undefined &&
buildBrowserFeatures.isEs5SupportNeeded()
)
) {
// The nomodule polyfill needs to be inject 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 this to fail.
if (buildBrowserFeatures.isNoModulePolyfillNeeded()) {
const noModuleScript: ExtraEntryPoint = {
bundleName: 'polyfills-nomodule-es5',
input: path.join(__dirname, '..', 'safari-nomodule.js'),
};
buildOptions.scripts = buildOptions.scripts
? [...buildOptions.scripts, noModuleScript]
: [noModuleScript];
}
}
} else {
// For NON differential loading we want to have 2 polyfill bundles
if (buildOptions.es5BrowserSupport
|| (buildOptions.es5BrowserSupport === undefined && isEs5SupportNeeded(projectRoot))) {
entryPoints['polyfills-es5'] = [es5Polyfills];

// For differential loading we don't need to generate a seperate polyfill file
// because they will be loaded exclusivly based on module and nomodule
const polyfillsChunkName = buildBrowserFeatures.isDifferentialLoadingNeeded()
? 'polyfills'
: 'polyfills-es5';

entryPoints[polyfillsChunkName] = [path.join(__dirname, '..', 'es5-polyfills.js')];
if (!buildOptions.aot) {
entryPoints['polyfills-es5'].push(es5JitPolyfills);
entryPoints[polyfillsChunkName].push(path.join(__dirname, '..', 'es5-jit-polyfills.js'));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,11 @@
// tslint:disable
// TODO: cleanup this file, it's copied as is from Angular CLI.

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

export const ngAppResolve = (resolvePath: string): string => {
return path.resolve(process.cwd(), resolvePath);
};

export interface HashFormat {
chunk: string;
extract: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export function generateEntryPoints(
};

const entryPoints = [
'polyfills-nomodule-es5',
'polyfills-es5',
'polyfills',
'sw-register',
Expand Down
12 changes: 8 additions & 4 deletions packages/angular_devkit/build_angular/src/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ import {
statsWarningsToString,
} from '../angular-cli-files/utilities/stats';
import { ExecutionTransformer } from '../transforms';
import { deleteOutputDir, isEs5SupportNeeded } from '../utils';
import { BuildBrowserFeatures, deleteOutputDir } from '../utils';
import { Version } from '../utils/version';
import { generateBrowserWebpackConfigFromContext } from '../utils/webpack-browser-config';
import { Schema as BrowserBuilderSchema } from './schema';
Expand Down Expand Up @@ -202,9 +202,13 @@ export function buildWebpackBrowser(
const tsConfigPath = path.resolve(getSystemPath(workspace.root), options.tsConfig);
const tsConfig = readTsconfig(tsConfigPath);

if (isEs5SupportNeeded(projectRoot) &&
tsConfig.options.target !== ScriptTarget.ES5 &&
tsConfig.options.target !== ScriptTarget.ES2015) {
const target = tsConfig.options.target || ScriptTarget.ES5;
const buildBrowserFeatures = new BuildBrowserFeatures(
getSystemPath(projectRoot),
target,
);

if (target > ScriptTarget.ES2015 && buildBrowserFeatures.isDifferentialLoadingNeeded()) {
context.logger.warn(tags.stripIndent`
WARNING: Using differential loading with targets ES5 and ES2016 or higher may
cause problems. Browsers with support for ES2015 will load the ES2016+ scripts
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 browserslist from 'browserslist';
import { feature, features } from 'caniuse-lite';
import * as ts from 'typescript';

export class BuildBrowserFeatures {
private readonly _supportedBrowsers: string[];
private readonly _es6TargetOrLater: boolean;

constructor(
private projectRoot: string,
private scriptTarget: ts.ScriptTarget,
) {
this._supportedBrowsers = browserslist(undefined, { path: this.projectRoot });
this._es6TargetOrLater = this.scriptTarget > ts.ScriptTarget.ES5;
}

/**
* True, when one or more browsers requires ES5
* support and the scirpt target is ES2015 or greater.
*/
isDifferentialLoadingNeeded(): boolean {
return this._es6TargetOrLater && this.isEs5SupportNeeded();
}

/**
* True, when one or more browsers requires ES5 support
*/
isEs5SupportNeeded(): boolean {
return !this.isFeatureSupported('es6-module');
}

/**
* Safari 10.1 and iOS Safari 10.3 supports modules,
* but does not support the `nomodule` attribute.
* While return `true`, when support for Safari 10.1 and iOS Safari 10.3
* is required and in differential loading is enabled.
*/
isNoModulePolyfillNeeded(): boolean {
if (!this.isDifferentialLoadingNeeded()) {
return false;
}

const safariBrowsers = [
'safari 10.1',
'ios_saf 10.3',
];

return this._supportedBrowsers.some(browser => safariBrowsers.includes(browser));
}

/**
* True, when a browser feature is supported partially or fully.
*/
isFeatureSupported(featureId: string): boolean {
// y: feature is fully available
// n: feature is unavailable
// a: feature is partially supported
// x: feature is prefixed
const criteria = [
'y',
'a',
];

const data = feature(features[featureId]);

return !this._supportedBrowsers
.some(browser => {
const [agentId, version] = browser.split(' ');

const browserData = data.stats[agentId];
const featureStatus = (browserData && browserData[version]) as string | undefined;

// We are only interested in the first character
// Ex: when 'a #4 #5', we only need to check for 'a'
// as for such cases we should polyfill these features as needed
return !featureStatus || !criteria.includes(featureStatus.charAt(0));
});
}
}
Loading