Skip to content

Commit 1ea8939

Browse files
committed
feat(@angular/build): expand browser support policy to widely available Baseline
This uses `bl2bl` to generate a `browserslist` configuration from a given widely available Baseline date. The date is hard-coded in `browserslist/package.json`, generating a `.browserslistrc` file at build-time. Using a browser outside of Angular's minimum defined browser set is still allowed as we expect that _most_ of the time this will work just fine. However, we log a warning to be clear to users that they are outside Angular's supported browserset. I had to apply a local patch to `bl2bl` as it would unconditionally attempt to write to `package.json` which is read-only in the Bazel file system since it is an input listed in `srcs`. I'll follow up with the maintainers to apply this fix upstream and remove the patch. I've pinned Angular to the March 31st baseline, but this will likely be updated again as we get closer to the v20 release. The current set of browsers generated are: ``` Chrome >= 107 ChromeAndroid >= 107 Edge >= 107 Firefox >= 104 FirefoxAndroid >= 104 Safari >= 16 iOS >= 16 ``` I opted _not_ to use "downstream browsers". This refers to browsers like Opera, which technically uses Blink and shares the same featureset as Chrome for a particular version. I decided against this to maintain a stricter interpretation of Baseline, given that those browsers are not included in Baseline today. Developers can manually widen their own `.browserslistrc` if they really want to and are comfortable accepting the risks that brings. Using `bl2bl` as part of the build process means there is a potential risk that a bugfix in `bl2bl` generates a different browserslist file which leads to a different build configuration that causes a problem for existing users. However, it's also just as likely (if not moreso) to fix a problem than cause one, so I'm inclined to call that WAI. If it becomes an issue in the future, we can potentially check in the generated `.browserslistrc` file itself rather than the Baseline date, meaning the list of browsers would be frozen until we explicitly update it between majors.
1 parent 92e193c commit 1ea8939

File tree

12 files changed

+149
-31
lines changed

12 files changed

+149
-31
lines changed

modules/testing/builder/projects/hello-world-app/.browserslistrc

-4
This file was deleted.

package.json

+10
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
"ajv": "8.17.1",
100100
"ansi-colors": "4.1.3",
101101
"beasties": "0.2.0",
102+
"bl2bl": "^0.2.0",
102103
"buffer": "6.0.3",
103104
"esbuild": "0.25.2",
104105
"esbuild-wasm": "0.25.2",
@@ -173,9 +174,18 @@
173174
"rxjs": "*"
174175
}
175176
}
177+
},
178+
"patchedDependencies": {
179+
"bl2bl": "patches/bl2bl.patch"
176180
}
177181
},
178182
"resolutions": {
179183
"typescript": "5.8.2"
184+
},
185+
"bl2bl": {
186+
"baselineThreshold": "widely available",
187+
"useBrowserslistrc": false,
188+
"downstreamBrowsers": false,
189+
"savePrevious": true
180190
}
181191
}

packages/angular/build/BUILD.bazel

+9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
load("@devinfra//bazel/api-golden:index_rjs.bzl", "api_golden_test_npm_package")
2+
load("@npm2//:bl2bl/package_json.bzl", bl2bl_bin = "bin")
23
load("@npm2//:defs.bzl", "npm_link_all_packages")
34
load("//tools:defaults2.bzl", "copy_to_bin", "jasmine_test", "npm_package", "ts_project")
45
load("//tools:ts_json_schema.bzl", "ts_json_schema")
@@ -39,6 +40,13 @@ copy_to_bin(
3940
srcs = glob(["**/schema.json"]),
4041
)
4142

43+
bl2bl_bin.bl2bl(
44+
name = "angular_browserslist",
45+
srcs = ["src/browserslist/package.json"],
46+
outs = ["src/browserslist/.browserslistrc"],
47+
chdir = "%s/src/browserslist" % package_name(),
48+
)
49+
4250
RUNTIME_ASSETS = glob(
4351
include = [
4452
"src/**/schema.json",
@@ -49,6 +57,7 @@ RUNTIME_ASSETS = glob(
4957
) + [
5058
"builders.json",
5159
"package.json",
60+
":angular_browserslist",
5261
]
5362

5463
ts_project(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"bl2bl": {
3+
"baselineThreshold": "2025-03-31",
4+
"useBrowserslistrc": true,
5+
"downstreamBrowsers": false,
6+
"savePrevious": false
7+
}
8+
}

packages/angular/build/src/builders/application/execute-build.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export async function executeBuild(
5959
} = options;
6060

6161
// TODO: Consider integrating into watch mode. Would require full rebuild on target changes.
62-
const browsers = getSupportedBrowsers(projectRoot, context.logger);
62+
const browsers = await getSupportedBrowsers(projectRoot, context.logger);
6363

6464
// Load active translations if inlining
6565
// TODO: Integrate into watch mode and only load changed translations

packages/angular/build/src/builders/application/tests/behavior/browser-support_spec.ts

+30-8
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,38 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
8484
});
8585

8686
it('warns when IE is present in browserslist', async () => {
87-
await harness.appendToFile(
87+
await harness.writeFile(
8888
'.browserslistrc',
8989
`
90-
IE 9
91-
IE 11
92-
`,
90+
IE 9
91+
IE 11
92+
`.trim(),
93+
);
94+
95+
harness.useTarget('build', {
96+
...BASE_OPTIONS,
97+
});
98+
99+
const { result, logs } = await harness.executeOnce();
100+
expect(result?.success).toBeTrue();
101+
102+
expect(logs).toContain(
103+
jasmine.objectContaining({
104+
level: 'warn',
105+
message: jasmine.stringContaining('ES5 output is not supported'),
106+
}),
107+
);
108+
109+
// Don't duplicate the error.
110+
expect(logs).not.toContain(
111+
jasmine.objectContaining({
112+
message: jasmine.stringContaining("fall outside Angular's browser support"),
113+
}),
93114
);
115+
});
116+
117+
it("warns when targeting a browser outside Angular's minimum support", async () => {
118+
await harness.writeFile('.browserslistrc', 'Chrome >= 100');
94119

95120
harness.useTarget('build', {
96121
...BASE_OPTIONS,
@@ -102,10 +127,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
102127
expect(logs).toContain(
103128
jasmine.objectContaining({
104129
level: 'warn',
105-
message:
106-
`One or more browsers which are configured in the project's Browserslist ` +
107-
'configuration will be ignored as ES5 output is not supported by the Angular CLI.\n' +
108-
'Ignored browsers: ie 11, ie 9',
130+
message: jasmine.stringContaining("fall outside Angular's browser support"),
109131
}),
110132
);
111133
});

packages/angular/build/src/builders/dev-server/vite-server.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ export async function* serveWithVite(
398398

399399
const { root = '' } = await context.getProjectMetadata(projectName);
400400
const projectRoot = join(context.workspaceRoot, root as string);
401-
const browsers = getSupportedBrowsers(projectRoot, context.logger);
401+
const browsers = await getSupportedBrowsers(projectRoot, context.logger);
402402

403403
const target = transformSupportedBrowsersToTargets(browsers);
404404
// Needed for browser-esbuild as polyfills can be a string.

packages/angular/build/src/utils/supported-browsers.ts

+30-13
Original file line numberDiff line numberDiff line change
@@ -7,40 +7,57 @@
77
*/
88

99
import browserslist from 'browserslist';
10+
import { promises as fs } from 'fs';
1011

11-
export function getSupportedBrowsers(
12+
export async function getSupportedBrowsers(
1213
projectRoot: string,
1314
logger: { warn(message: string): void },
14-
): string[] {
15-
browserslist.defaults = [
16-
'last 2 Chrome versions',
17-
'last 1 Firefox version',
18-
'last 2 Edge major versions',
19-
'last 2 Safari major versions',
20-
'last 2 iOS major versions',
21-
'last 2 Android major versions',
22-
'Firefox ESR',
23-
];
15+
): Promise<string[]> {
16+
// Read the browserslist configuration containing Angular's browser support policy.
17+
const angularBrowserslist = (
18+
await fs.readFile(require.resolve('../browserslist/.browserslistrc'), 'utf8')
19+
)
20+
.split('\n')
21+
.filter((line) => !line.startsWith('#')); // Omit comments.
22+
23+
// Use Angular's configuration as the default.
24+
browserslist.defaults = angularBrowserslist;
25+
26+
// Get the minimum set of browser versions supported by Angular.
27+
const minimumBrowsers = new Set(browserslist(angularBrowserslist));
2428

2529
// Get browsers from config or default.
2630
const browsersFromConfigOrDefault = new Set(browserslist(undefined, { path: projectRoot }));
2731

2832
// Get browsers that support ES6 modules.
2933
const browsersThatSupportEs6 = new Set(browserslist('supports es6-module'));
3034

35+
const nonEs6Browsers: string[] = [];
3136
const unsupportedBrowsers: string[] = [];
3237
for (const browser of browsersFromConfigOrDefault) {
3338
if (!browsersThatSupportEs6.has(browser)) {
39+
// Any browser which does not support ES6 is explicitly ignored, as Angular will not build successfully.
3440
browsersFromConfigOrDefault.delete(browser);
41+
nonEs6Browsers.push(browser);
42+
} else if (!minimumBrowsers.has(browser)) {
43+
// Any other unsupported browser we will attempt to use, but provide no support for.
3544
unsupportedBrowsers.push(browser);
3645
}
3746
}
3847

39-
if (unsupportedBrowsers.length) {
48+
if (nonEs6Browsers.length) {
4049
logger.warn(
4150
`One or more browsers which are configured in the project's Browserslist configuration ` +
4251
'will be ignored as ES5 output is not supported by the Angular CLI.\n' +
43-
`Ignored browsers: ${unsupportedBrowsers.join(', ')}`,
52+
`Ignored browsers:\n${nonEs6Browsers.join(', ')}`,
53+
);
54+
}
55+
56+
if (unsupportedBrowsers.length) {
57+
logger.warn(
58+
`One or more browsers which are configured in the project's Browserslist configuration ` +
59+
"fall outside Angular's browser support for this version.\n" +
60+
`Unsupported browsers:\n${unsupportedBrowsers.join(', ')}`,
4461
);
4562
}
4663

packages/angular_devkit/build_angular/src/utils/normalize-builder-schema.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@ export type NormalizedBrowserBuilderSchema = BrowserBuilderSchema &
3636
polyfills: string[];
3737
};
3838

39-
export function normalizeBrowserSchema(
39+
export async function normalizeBrowserSchema(
4040
workspaceRoot: string,
4141
projectRoot: string,
4242
projectSourceRoot: string | undefined,
4343
options: BrowserBuilderSchema,
4444
metadata: json.JsonObject,
4545
logger: logging.LoggerApi,
46-
): NormalizedBrowserBuilderSchema {
46+
): Promise<NormalizedBrowserBuilderSchema> {
4747
return {
4848
...options,
4949
cache: normalizeCacheOptions(metadata, workspaceRoot),
@@ -73,6 +73,6 @@ export function normalizeBrowserSchema(
7373
// A value of 0 is falsy and will disable polling rather then enable
7474
// 500 ms is a sensible default in this case
7575
poll: options.poll === 0 ? 500 : options.poll,
76-
supportedBrowsers: getSupportedBrowsers(projectRoot, logger),
76+
supportedBrowsers: await getSupportedBrowsers(projectRoot, logger),
7777
};
7878
}

packages/angular_devkit/build_angular/src/utils/webpack-browser-config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ export async function generateBrowserWebpackConfigFromContext(
145145
const sourceRoot = projectMetadata.sourceRoot as string | undefined;
146146
const projectSourceRoot = sourceRoot ? path.join(workspaceRoot, sourceRoot) : undefined;
147147

148-
const normalizedOptions = normalizeBrowserSchema(
148+
const normalizedOptions = await normalizeBrowserSchema(
149149
workspaceRoot,
150150
projectRoot,
151151
projectSourceRoot,

patches/bl2bl.patch

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
diff --git a/bin/cli.js b/bin/cli.js
2+
index d1a554e6de9e9b4497ffeae5476b3e838c79b408..2bd1955d7120a7992fe6676b1d4da47a7be1b153 100755
3+
--- a/bin/cli.js
4+
+++ b/bin/cli.js
5+
@@ -114,6 +114,7 @@ import("baseline-browser-mapping").then((bbm) => {
6+
});
7+
8+
// Behaviour varies depending on whether userBrowserslistrc=true or false
9+
+ const originallyHadBrowserslistrc = Boolean(packageJSON["browserslist"]);
10+
if (!bl2blConfig.useBrowserslistrc) {
11+
// if false, add baselineVersions to packageJSON object for later
12+
packageJSON["browserslist"] = browserslistOutput;
13+
@@ -149,7 +150,7 @@ import("baseline-browser-mapping").then((bbm) => {
14+
}
15+
16+
// Whatever happens, update package.json as long as the packageJSON object isn't null
17+
- if (packageJSON != null) {
18+
+ if (packageJSON != null && (!bl2blConfig.useBrowserslistrc || originallyHadBrowserslistrc)) {
19+
fs.writeFileSync(
20+
process.cwd() + "/package.json",
21+
JSON.stringify(packageJSON, null, 2),

pnpm-lock.yaml

+35
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)