Skip to content

Commit aec56ac

Browse files
committed
feat(@angular-devkit/build-angular): use Browserslist to determine ECMA output
With this change we reduce the reliance on the TypeScript target compiler option to output a certain ECMA version. Instead we now use the browsers that are configured in the Browserslist configuration to determine which ECMA features and version are needed. This is done by passing the transpiled TypeScript to Babel preset-env. **Note about useDefineForClassFields**: while setting this to `false` will output JavaScript which is not spec compliant, this is needed because TypeScript introduced class fields many years before it was ratified in TC39. The latest version of the spec have a different runtime behavior to TypeScript’s implementation but the same syntax. Therefore, we opt-out from using upcoming ECMA runtime behavior to better support the ECO system and libraries that depend on the non spec compliant output. One of biggest case is usages of the deprected `@Effect` decorator by NGRX which otherwise would cause runtime failures. Dropping `useDefineForClassFields` will be considered in a future major releases. For more information see: microsoft/TypeScript#45995. BREAKING CHANGE: Internally the Angular CLI now always set the TypeScript `target` to `ES2022` and `useDefineForClassFields` to `false` unless the target is set to `ES2022` or later in the TypeScript configuration. To control ECMA version and features use the Browerslist configuration.
1 parent 113e2c0 commit aec56ac

File tree

32 files changed

+410
-199
lines changed

32 files changed

+410
-199
lines changed

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

+9-22
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
*/
88

99
import { custom } from 'babel-loader';
10-
import { ScriptTarget } from 'typescript';
1110
import { loadEsmModule } from '../utils/load-esm';
1211
import { VERSION } from '../utils/package-version';
1312
import { ApplicationPresetOptions, I18nPluginCreators } from './presets/application';
@@ -72,15 +71,8 @@ export default custom<ApplicationPresetOptions>(() => {
7271

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

8577
// Must process file if plugins are added
8678
let shouldProcess = Array.isArray(rawOptions.plugins) && rawOptions.plugins.length > 0;
@@ -114,24 +106,19 @@ export default custom<ApplicationPresetOptions>(() => {
114106
}
115107

116108
// Analyze for ES target processing
117-
const esTarget = scriptTarget as ScriptTarget | undefined;
118-
const isJsFile = /\.[cm]?js$/.test(this.resourcePath);
119-
120-
if (isJsFile && customOptions.supportedBrowsers?.length) {
109+
if (customOptions.supportedBrowsers?.length) {
121110
// Applications code ES version can be controlled using TypeScript's `target` option.
122111
// However, this doesn't effect libraries and hence we use preset-env to downlevel ES fetaures
123112
// based on the supported browsers in browserlist.
124113
customOptions.forcePresetEnv = true;
125114
}
126115

127-
if ((esTarget !== undefined && esTarget >= ScriptTarget.ES2017) || isJsFile) {
128-
// Application code (TS files) will only contain native async if target is ES2017+.
129-
// However, third-party libraries can regardless of the target option.
130-
// APF packages with code in [f]esm2015 directories is downlevelled to ES2015 and
131-
// will not have native async.
132-
customOptions.forceAsyncTransformation =
133-
!/[\\/][_f]?esm2015[\\/]/.test(this.resourcePath) && source.includes('async');
134-
}
116+
// Application code (TS files) will only contain native async if target is ES2017+.
117+
// However, third-party libraries can regardless of the target option.
118+
// APF packages with code in [f]esm2015 directories is downlevelled to ES2015 and
119+
// will not have native async.
120+
customOptions.forceAsyncTransformation =
121+
!/[\\/][_f]?esm2015[\\/]/.test(this.resourcePath) && source.includes('async');
135122

136123
shouldProcess ||=
137124
customOptions.forceAsyncTransformation || customOptions.forcePresetEnv || false;

packages/angular_devkit/build_angular/src/builders/browser-esbuild/compiler-plugin.ts

+7-10
Original file line numberDiff line numberDiff line change
@@ -182,16 +182,13 @@ export function createCompilerPlugin(
182182
enableResourceInlining: false,
183183
});
184184

185-
// Adjust the esbuild output target based on the tsconfig target
186-
if (
187-
compilerOptions.target === undefined ||
188-
compilerOptions.target <= ts.ScriptTarget.ES2015
189-
) {
190-
build.initialOptions.target = 'es2015';
191-
} else if (compilerOptions.target >= ts.ScriptTarget.ESNext) {
192-
build.initialOptions.target = 'esnext';
193-
} else {
194-
build.initialOptions.target = ts.ScriptTarget[compilerOptions.target].toLowerCase();
185+
if (compilerOptions.target === undefined || compilerOptions.target < ts.ScriptTarget.ES2022) {
186+
// If 'useDefineForClassFields' is already defined in the users project leave the value as is.
187+
// Othereise fallback to false to due https://github.com/microsoft/TypeScript/issues/45995
188+
// which breaks the deprecated @Effects NGRX decorator.
189+
compilerOptions.target = ts.ScriptTarget.ES2022;
190+
compilerOptions.useDefineForClassFields ??= false;
191+
// TODO: show warning about this override when we have access to the logger.
195192
}
196193

197194
// The file emitter created during `onStart` that will be used during the build in `onLoad` callbacks for TS files

packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ import * as path from 'path';
1414
import { NormalizedOptimizationOptions, deleteOutputDir } from '../../utils';
1515
import { copyAssets } from '../../utils/copy-assets';
1616
import { assertIsError } from '../../utils/error';
17+
import { transformSupportedBrowsersToTargets } from '../../utils/esbuild-targets';
1718
import { FileInfo } from '../../utils/index-file/augment-index-html';
1819
import { IndexHtmlGenerator } from '../../utils/index-file/index-html-generator';
1920
import { generateEntryPoints } from '../../utils/package-chunk-sort';
2021
import { augmentAppWithServiceWorker } from '../../utils/service-worker';
22+
import { getSupportedBrowsers } from '../../utils/supported-browsers';
2123
import { getIndexInputFile, getIndexOutputFile } from '../../utils/webpack-browser-config';
2224
import { resolveGlobalStyles } from '../../webpack/configs';
2325
import { createCompilerPlugin } from './compiler-plugin';
@@ -89,6 +91,10 @@ export async function buildEsbuildBrowser(
8991
return { success: false };
9092
}
9193

94+
const target = transformSupportedBrowsersToTargets(
95+
getSupportedBrowsers(projectRoot, context.logger),
96+
);
97+
9298
const [codeResults, styleResults] = await Promise.all([
9399
// Execute esbuild to bundle the application code
94100
bundleCode(
@@ -99,6 +105,7 @@ export async function buildEsbuildBrowser(
99105
optimizationOptions,
100106
sourcemapOptions,
101107
tsconfig,
108+
target,
102109
),
103110
// Execute esbuild to bundle the global stylesheets
104111
bundleGlobalStylesheets(
@@ -107,6 +114,7 @@ export async function buildEsbuildBrowser(
107114
options,
108115
optimizationOptions,
109116
sourcemapOptions,
117+
target,
110118
),
111119
]);
112120

@@ -248,6 +256,7 @@ async function bundleCode(
248256
optimizationOptions: NormalizedOptimizationOptions,
249257
sourcemapOptions: SourceMapClass,
250258
tsconfig: string,
259+
target: string[],
251260
) {
252261
let fileReplacements: Record<string, string> | undefined;
253262
if (options.fileReplacements) {
@@ -267,7 +276,7 @@ async function bundleCode(
267276
entryPoints,
268277
entryNames: outputNames.bundles,
269278
assetNames: outputNames.media,
270-
target: 'es2020',
279+
target,
271280
supported: {
272281
// Native async/await is not supported with Zone.js. Disabling support here will cause
273282
// esbuild to downlevel async/await and for await...of to a Zone.js supported form. However, esbuild
@@ -313,6 +322,7 @@ async function bundleCode(
313322
outputNames,
314323
includePaths: options.stylePreprocessorOptions?.includePaths,
315324
externalDependencies: options.externalDependencies,
325+
target,
316326
},
317327
),
318328
],
@@ -329,6 +339,7 @@ async function bundleGlobalStylesheets(
329339
options: BrowserBuilderOptions,
330340
optimizationOptions: NormalizedOptimizationOptions,
331341
sourcemapOptions: SourceMapClass,
342+
target: string[],
332343
) {
333344
const outputFiles: OutputFile[] = [];
334345
const initialFiles: FileInfo[] = [];
@@ -360,6 +371,7 @@ async function bundleGlobalStylesheets(
360371
includePaths: options.stylePreprocessorOptions?.includePaths,
361372
preserveSymlinks: options.preserveSymlinks,
362373
externalDependencies: options.externalDependencies,
374+
target,
363375
},
364376
);
365377

packages/angular_devkit/build_angular/src/builders/browser-esbuild/stylesheets.ts

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export interface BundleStylesheetOptions {
2020
outputNames?: { bundles?: string; media?: string };
2121
includePaths?: string[];
2222
externalDependencies?: string[];
23+
target: string[];
2324
}
2425

2526
async function bundleStylesheet(
@@ -43,6 +44,7 @@ async function bundleStylesheet(
4344
outdir: options.workspaceRoot,
4445
write: false,
4546
platform: 'browser',
47+
target: options.target,
4648
preserveSymlinks: options.preserveSymlinks,
4749
external: options.externalDependencies,
4850
conditions: ['style', 'sass'],

packages/angular_devkit/build_angular/src/builders/browser/specs/allow-js_spec.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ describe('Browser Builder allow js', () => {
3131

3232
host.replaceInFile(
3333
'tsconfig.json',
34-
'"target": "es2020"',
35-
'"target": "es2020", "allowJs": true',
34+
'"target": "es2022"',
35+
'"target": "es2022", "allowJs": true',
3636
);
3737

3838
const run = await architect.scheduleTarget(targetSpec);
@@ -56,8 +56,8 @@ describe('Browser Builder allow js', () => {
5656

5757
host.replaceInFile(
5858
'tsconfig.json',
59-
'"target": "es2020"',
60-
'"target": "es2020", "allowJs": true',
59+
'"target": "es2022"',
60+
'"target": "es2022", "allowJs": true',
6161
);
6262

6363
const overrides = { aot: true };
@@ -83,8 +83,8 @@ describe('Browser Builder allow js', () => {
8383

8484
host.replaceInFile(
8585
'tsconfig.json',
86-
'"target": "es2020"',
87-
'"target": "es2020", "allowJs": true',
86+
'"target": "es2022"',
87+
'"target": "es2022", "allowJs": true',
8888
);
8989

9090
const overrides = { watch: true };

packages/angular_devkit/build_angular/src/builders/browser/specs/aot_spec.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ describe('Browser Builder AOT', () => {
2727
const run = await architect.scheduleTarget(targetSpec, overrides);
2828
const output = (await run.result) as BrowserBuilderOutput;
2929

30-
expect(output.success).toBe(true);
30+
expect(output.success).toBeTrue();
3131

32-
const fileName = join(normalize(output.outputPath), 'main.js');
32+
const fileName = join(normalize(output.outputs[0].path), 'main.js');
3333
const content = virtualFs.fileBufferToString(await host.read(normalize(fileName)).toPromise());
34-
expect(content).toContain('AppComponent.ɵcmp');
34+
expect(content).toContain('AppComponent_Factory');
3535

3636
await run.stop();
3737
});

packages/angular_devkit/build_angular/src/builders/browser/specs/lazy-module_spec.ts

+7-8
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,7 @@ describe('Browser Builder lazy modules', () => {
6565

6666
const { files } = await browserBuild(architect, host, target, { aot: true });
6767
const data = await files['src_app_lazy_lazy_module_ts.js'];
68-
expect(data).not.toBeUndefined();
69-
expect(data).toContain('LazyModule.ɵmod');
68+
expect(data).toContain('this.ɵmod');
7069
});
7170
});
7271

@@ -126,7 +125,7 @@ describe('Browser Builder lazy modules', () => {
126125
});
127126

128127
const { files } = await browserBuild(architect, host, target);
129-
expect(files['src_lazy-module_ts.js']).not.toBeUndefined();
128+
expect(files['src_lazy-module_ts.js']).toBeDefined();
130129
});
131130

132131
it(`supports lazy bundle for dynamic import() calls`, async () => {
@@ -140,7 +139,7 @@ describe('Browser Builder lazy modules', () => {
140139
host.replaceInFile('src/tsconfig.app.json', '"main.ts"', `"main.ts","lazy-module.ts"`);
141140

142141
const { files } = await browserBuild(architect, host, target);
143-
expect(files['lazy-module.js']).not.toBeUndefined();
142+
expect(files['lazy-module.js']).toBeDefined();
144143
});
145144

146145
it(`supports making a common bundle for shared lazy modules`, async () => {
@@ -151,8 +150,8 @@ describe('Browser Builder lazy modules', () => {
151150
});
152151

153152
const { files } = await browserBuild(architect, host, target);
154-
expect(files['src_one_ts.js']).not.toBeUndefined();
155-
expect(files['src_two_ts.js']).not.toBeUndefined();
153+
expect(files['src_one_ts.js']).toBeDefined();
154+
expect(files['src_two_ts.js']).toBeDefined();
156155
expect(files['default-node_modules_angular_common_fesm2020_http_mjs.js']).toBeDefined();
157156
});
158157

@@ -164,8 +163,8 @@ describe('Browser Builder lazy modules', () => {
164163
});
165164

166165
const { files } = await browserBuild(architect, host, target, { commonChunk: false });
167-
expect(files['src_one_ts.js']).not.toBeUndefined();
168-
expect(files['src_two_ts.js']).not.toBeUndefined();
166+
expect(files['src_one_ts.js']).toBeDefined();
167+
expect(files['src_two_ts.js']).toBeDefined();
169168
expect(files['default-node_modules_angular_common_fesm2020_http_mjs.js']).toBeUndefined();
170169
});
171170
});

packages/angular_devkit/build_angular/src/builders/browser/specs/resolve-json-module_spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ describe('Browser Builder resolve json module', () => {
2929

3030
host.replaceInFile(
3131
'tsconfig.json',
32-
'"target": "es2020"',
33-
'"target": "es2020", "resolveJsonModule": true',
32+
'"target": "es2022"',
33+
'"target": "es2022", "resolveJsonModule": true',
3434
);
3535

3636
const overrides = { watch: true };

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

+13-13
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,12 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
6565
});
6666

6767
it('warns when IE is present in browserslist', async () => {
68-
await harness.writeFile(
68+
await harness.appendToFile(
6969
'.browserslistrc',
7070
`
71-
IE 9
72-
IE 11
73-
`,
71+
IE 9
72+
IE 11
73+
`,
7474
);
7575

7676
harness.useTarget('build', {
@@ -84,9 +84,9 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
8484
jasmine.objectContaining({
8585
level: 'warn',
8686
message:
87-
`One or more browsers which are configured in the project's Browserslist configuration ` +
88-
'will be ignored as ES5 output is not supported by the Angular CLI.\n' +
89-
`Ignored browsers: ie 11, ie 9`,
87+
`One or more browsers which are configured in the project's Browserslist ` +
88+
'configuration will be ignored as ES5 output is not supported by the Angular CLI.\n' +
89+
'Ignored browsers: ie 11, ie 9',
9090
}),
9191
);
9292
});
@@ -96,12 +96,12 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
9696
await harness.writeFile(
9797
'src/main.ts',
9898
`
99-
(async () => {
100-
for await (const o of [1, 2, 3]) {
101-
console.log("for await...of");
102-
}
103-
})();
104-
`,
99+
(async () => {
100+
for await (const o of [1, 2, 3]) {
101+
console.log("for await...of");
102+
}
103+
})();
104+
`,
105105
);
106106

107107
harness.useTarget('build', {

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ describeBuilder(serveWebpackBrowser, DEV_SERVER_BUILDER_INFO, (harness) => {
4242
};
4343

4444
describe('Behavior: "dev-server builder serves service worker"', () => {
45-
beforeEach(() => {
45+
beforeEach(async () => {
46+
// Application code is not needed for these tests
47+
await harness.writeFile('src/main.ts', '');
48+
await harness.writeFile('src/polyfills.ts', '');
49+
4650
harness.useProject('test', {
4751
root: '.',
4852
sourceRoot: 'src',

packages/angular_devkit/build_angular/src/builders/server/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ async function initialize(
150150
context,
151151
(wco) => {
152152
// We use the platform to determine the JavaScript syntax output.
153+
wco.buildOptions.supportedBrowsers ??= [];
153154
wco.buildOptions.supportedBrowsers.push(...browserslist('maintained node versions'));
154155

155156
return [getCommonConfig(wco), getStylesConfig(wco)];

packages/angular_devkit/build_angular/src/utils/build-options.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export interface BuildOptions {
7272
cache: NormalizedCachedOptions;
7373
codeCoverage?: boolean;
7474
codeCoverageExclude?: string[];
75-
supportedBrowsers: string[];
75+
supportedBrowsers?: string[];
7676
}
7777

7878
export interface WebpackDevServerOptions
@@ -87,6 +87,5 @@ export interface WebpackConfigOptions<T = BuildOptions> {
8787
buildOptions: T;
8888
tsConfig: ParsedConfiguration;
8989
tsConfigPath: string;
90-
scriptTarget: import('typescript').ScriptTarget;
9190
projectName: string;
9291
}

0 commit comments

Comments
 (0)