Skip to content

Commit cd2250f

Browse files
alan-agius4dgp1130
authored andcommitted
fix(@angular-devkit/build-angular): downlevel libraries based on the browserslist configurations
There is no standard for library authors to ship their library in different ES versions, which can result in vendor libraries to ship ES features which are not supported by one or more browsers that the user's application supports. With this change, we will be downlevelling libraries based on the list of supported browsers which is configured in the browserslist configuration. Previously, we only downlevelled libraries when targeting ES5. The TypeScript target option will not effect how the libraries get downlevelled. Closes #23126
1 parent 790a171 commit cd2250f

File tree

6 files changed

+117
-34
lines changed

6 files changed

+117
-34
lines changed

packages/angular_devkit/build_angular/src/babel/presets/application.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export interface ApplicationPresetOptions {
4747
linkerPluginCreator: typeof import('@angular/compiler-cli/linker/babel').createEs2015LinkerPlugin;
4848
};
4949

50-
forceES5?: boolean;
50+
forcePresetEnv?: boolean;
5151
forceAsyncTransformation?: boolean;
5252
instrumentCode?: {
5353
includedBasePath: string;
@@ -59,6 +59,7 @@ export interface ApplicationPresetOptions {
5959
wrapDecorators: boolean;
6060
};
6161

62+
supportedBrowsers?: string[];
6263
diagnosticReporter?: DiagnosticReporter;
6364
}
6465

@@ -178,14 +179,13 @@ export default function (api: unknown, options: ApplicationPresetOptions) {
178179
);
179180
}
180181

181-
if (options.forceES5) {
182+
if (options.forcePresetEnv) {
182183
presets.push([
183184
require('@babel/preset-env').default,
184185
{
185186
bugfixes: true,
186187
modules: false,
187-
// Comparable behavior to tsconfig target of ES5
188-
targets: { ie: 9 },
188+
targets: options.supportedBrowsers,
189189
exclude: ['transform-typeof-symbol'],
190190
},
191191
]);

packages/angular_devkit/build_angular/src/babel/webpack-loader.ts

+38-15
Original file line numberDiff line numberDiff line change
@@ -72,18 +72,26 @@ export default custom<ApplicationPresetOptions>(() => {
7272

7373
return {
7474
async customOptions(options, { source, map }) {
75-
const { i18n, scriptTarget, aot, optimize, instrumentCode, ...rawOptions } =
76-
options as AngularBabelLoaderOptions;
75+
const {
76+
i18n,
77+
scriptTarget,
78+
aot,
79+
optimize,
80+
instrumentCode,
81+
supportedBrowsers,
82+
...rawOptions
83+
} = options as AngularBabelLoaderOptions;
7784

7885
// Must process file if plugins are added
7986
let shouldProcess = Array.isArray(rawOptions.plugins) && rawOptions.plugins.length > 0;
8087

8188
const customOptions: ApplicationPresetOptions = {
8289
forceAsyncTransformation: false,
83-
forceES5: false,
90+
forcePresetEnv: false,
8491
angularLinker: undefined,
8592
i18n: undefined,
8693
instrumentCode: undefined,
94+
supportedBrowsers,
8795
};
8896

8997
// Analyze file for linking
@@ -107,20 +115,35 @@ export default custom<ApplicationPresetOptions>(() => {
107115

108116
// Analyze for ES target processing
109117
const esTarget = scriptTarget as ScriptTarget | undefined;
110-
if (esTarget !== undefined) {
111-
if (esTarget < ScriptTarget.ES2015) {
112-
customOptions.forceES5 = true;
113-
} else if (esTarget >= ScriptTarget.ES2017 || /\.[cm]?js$/.test(this.resourcePath)) {
114-
// Application code (TS files) will only contain native async if target is ES2017+.
115-
// However, third-party libraries can regardless of the target option.
116-
// APF packages with code in [f]esm2015 directories is downlevelled to ES2015 and
117-
// will not have native async.
118-
customOptions.forceAsyncTransformation =
119-
!/[\\/][_f]?esm2015[\\/]/.test(this.resourcePath) && source.includes('async');
120-
}
121-
shouldProcess ||= customOptions.forceAsyncTransformation || customOptions.forceES5 || false;
118+
const isJsFile = /\.[cm]?js$/.test(this.resourcePath);
119+
120+
// The below should be dropped when we no longer support ES5 TypeScript output.
121+
if (esTarget === ScriptTarget.ES5) {
122+
// This is needed because when target is ES5 we change the TypeScript target to ES2015
123+
// because it simplifies build-optimization passes.
124+
// @see https://github.com/angular/angular-cli/blob/22af6520834171d01413d4c7e4a9f13fb752252e/packages/angular_devkit/build_angular/src/webpack/plugins/typescript.ts#L51-L56
125+
customOptions.forcePresetEnv = true;
126+
// Comparable behavior to tsconfig target of ES5
127+
customOptions.supportedBrowsers = ['IE 9'];
128+
} else if (isJsFile) {
129+
// Applications code ES version can be controlled using TypeScript's `target` option.
130+
// However, this doesn't effect libraries and hence we use preset-env to downlevel ES fetaures
131+
// based on the supported browsers in browserlist.
132+
customOptions.forcePresetEnv = true;
122133
}
123134

135+
if ((esTarget !== undefined && esTarget >= ScriptTarget.ES2017) || isJsFile) {
136+
// Application code (TS files) will only contain native async if target is ES2017+.
137+
// However, third-party libraries can regardless of the target option.
138+
// APF packages with code in [f]esm2015 directories is downlevelled to ES2015 and
139+
// will not have native async.
140+
customOptions.forceAsyncTransformation =
141+
!/[\\/][_f]?esm2015[\\/]/.test(this.resourcePath) && source.includes('async');
142+
}
143+
144+
shouldProcess ||=
145+
customOptions.forceAsyncTransformation || customOptions.forcePresetEnv || false;
146+
124147
// Analyze for i18n inlining
125148
if (
126149
i18n &&

packages/angular_devkit/build_angular/src/builders/browser/tests/behavior/typescript-target_spec.ts

+65-14
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import { logging } from '@angular-devkit/core';
910
import { buildWebpackBrowser } from '../../index';
1011
import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup';
1112

@@ -141,12 +142,12 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
141142
await harness.writeFile(
142143
'src/main.ts',
143144
`
144-
(async () => {
145-
for await (const o of [1, 2, 3]) {
146-
console.log("for await...of");
147-
}
148-
})();
149-
`,
145+
(async () => {
146+
for await (const o of [1, 2, 3]) {
147+
console.log("for await...of");
148+
}
149+
})();
150+
`,
150151
);
151152

152153
harness.useTarget('build', {
@@ -176,14 +177,14 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
176177
await harness.writeFile(
177178
'src/es2015-syntax.js',
178179
`
179-
class foo {
180-
bar() {
181-
console.log('baz');
182-
}
183-
}
184-
185-
(new foo()).bar();
186-
`,
180+
class foo {
181+
bar() {
182+
console.log('baz');
183+
}
184+
}
185+
186+
(new foo()).bar();
187+
`,
187188
);
188189

189190
harness.useTarget('build', {
@@ -198,5 +199,55 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
198199

199200
expect(result?.success).toBe(true);
200201
});
202+
203+
it('a deprecation warning should be issued when targetting ES5', async () => {
204+
await harness.modifyFile('src/tsconfig.app.json', (content) => {
205+
const tsconfig = JSON.parse(content);
206+
if (!tsconfig.compilerOptions) {
207+
tsconfig.compilerOptions = {};
208+
}
209+
tsconfig.compilerOptions.target = 'es5';
210+
211+
return JSON.stringify(tsconfig);
212+
});
213+
await harness.writeFiles({
214+
'src/tsconfig.worker.json': `{
215+
"extends": "../tsconfig.json",
216+
"compilerOptions": {
217+
"outDir": "../out-tsc/worker",
218+
"lib": [
219+
"es2018",
220+
"webworker"
221+
],
222+
"types": []
223+
},
224+
"include": [
225+
"**/*.worker.ts",
226+
]
227+
}`,
228+
'src/app/app.worker.ts': `
229+
/// <reference lib="webworker" />
230+
231+
const prefix: string = 'Data: ';
232+
addEventListener('message', ({ data }) => {
233+
postMessage(prefix + data);
234+
});
235+
`,
236+
});
237+
238+
harness.useTarget('build', {
239+
...BASE_OPTIONS,
240+
webWorkerTsConfig: 'src/tsconfig.worker.json',
241+
});
242+
243+
const { result, logs } = await harness.executeOnce();
244+
expect(result?.success).toBeTrue();
245+
246+
const deprecationMessages = logs.filter(({ message }) =>
247+
message.startsWith('DEPRECATED: ES5 output is deprecated'),
248+
);
249+
250+
expect(deprecationMessages).toHaveSize(1);
251+
});
201252
});
202253
});

packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/build_localize_replaced_watch_spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ describeBuilder(serveWebpackBrowser, DEV_SERVER_BUILDER_INFO, (harness) => {
5555
const buildCount = await harness
5656
.execute()
5757
.pipe(
58-
timeout(BUILD_TIMEOUT),
58+
timeout(BUILD_TIMEOUT * 2),
5959
concatMap(async ({ result }, index) => {
6060
expect(result?.success).toBe(true);
6161

packages/angular_devkit/build_angular/src/webpack/configs/common.ts

+1
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ export async function getCommonConfig(wco: WebpackConfigOptions): Promise<Config
378378
scriptTarget,
379379
aot: buildOptions.aot,
380380
optimize: buildOptions.buildOptimizer,
381+
supportedBrowsers: buildOptions.supportedBrowsers,
381382
instrumentCode: codeCoverage
382383
? {
383384
includedBasePath: sourceRoot,

packages/angular_devkit/build_angular/src/webpack/plugins/typescript.ts

+8
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ function ensureIvy(wco: WebpackConfigOptions): void {
2626
wco.tsConfig.options.enableIvy = true;
2727
}
2828

29+
let es5TargetWarningsShown = false;
2930
export function createIvyPlugin(
3031
wco: WebpackConfigOptions,
3132
aot: boolean,
@@ -53,6 +54,13 @@ export function createIvyPlugin(
5354
// as for third-party libraries. This greatly reduces the complexity of static analysis.
5455
if (wco.scriptTarget < ScriptTarget.ES2015) {
5556
compilerOptions.target = ScriptTarget.ES2015;
57+
if (!es5TargetWarningsShown) {
58+
wco.logger.warn(
59+
'DEPRECATED: ES5 output is deprecated. Please update TypeScript `target` compiler option to ES2015 or later.',
60+
);
61+
62+
es5TargetWarningsShown = true;
63+
}
5664
}
5765

5866
const fileReplacements: Record<string, string> = {};

0 commit comments

Comments
 (0)