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
Avoid resolveTargets
call if browsers
is an empty array
#14294
Conversation
In the process of using babel, I found that when `browserslistConfigFile` is not set to `false` and browsersList is not set, babel will still call the related methods of browsersList and caniuse-lite, I think this is unnecessary, Because browsers is an empty array at this time, it is a truthy value, so I modified the judgment condition to not handle it when it is undefined or an empty array.
Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/51477/ |
You can add a test after babel/packages/babel-helper-compilation-targets/test/targets-parser.spec.js Lines 287 to 301 in aa5ff36
The behaviour of |
I'm sorry I didn't reply to you until a week later. On the one hand, I was too busy with work, on the other hand, I was seriously thinking about how to test this code. Here is my opinion:
(process.env.BABEL_8_BREAKING ? it.skip : it)(
"'browsers' option will have no effect if it is an empty array - Babel 7",
() => {
expect(getTargets({ esmodules: "intersect", browsers: [] })).toEqual(
getTargets({ esmodules: "intersect" }),
);
},
); I added it and this code passed the test. But I don't think the two are necessarily equivalent. Observe the code for let { browsers, esmodules } = inputTargets;
const { configPath = "." } = options;
validateBrowsers(browsers);
const input = generateTargets(inputTargets);
let targets: TargetsTuple = validateTargetNames(input);
const shouldParseBrowsers = !!browsers;
const hasTargets = shouldParseBrowsers || Object.keys(targets).length > 0;
const shouldSearchForConfig =
!options.ignoreBrowserslistConfig && !hasTargets;
// ... For if (!browsers && shouldSearchForConfig) {
browsers = browserslist.loadConfig({
config: options.configFile,
path: configPath,
env: options.browserslistEnv,
});
if (browsers == null) {
if (process.env.BABEL_8_BREAKING) {
// In Babel 8, if no targets are passed, we use browserslist's defaults
// and exclude IE 11.
browsers = ["defaults, not ie 11"];
} else {
// If no targets are passed, we need to overwrite browserslist's defaults
// so that we enable all transforms (acting like the now deprecated
// preset-latest).
browsers = [];
}
}
} In the scenario of That said, the behavior is consistent here without getting to where I've changed it, but in practice it also depends on the value of To prove that The behavior that I think is more necessary to test is that the behavior of (process.env.BABEL_8_BREAKING ? it.skip : it)(
"The final 'browsers' handled variable will have no effect if it is an empty array - Babel 7",
() => {
expect(getTargets({ esmodules: "intersect", browsers: [] })).toEqual(
getTargets(
{ esmodules: "intersect" },
{ ignoreBrowserslistConfig: true },
),
);
},
); It also passed the test.
As mentioned above, the reason for this PR is that I don't want to execute if (browsers) {
const queryBrowsers = resolveTargets(browsers, options.browserslistEnv);
if (esmodules === "intersect") {
for (const browser of Object.keys(queryBrowsers)) {
const version = queryBrowsers[browser];
if (ESM_SUPPORT[browser]) {
queryBrowsers[browser] = getHighestUnreleased(
version,
semverify(ESM_SUPPORT[browser]),
browser,
);
} else {
delete queryBrowsers[browser];
}
}
}
targets = Object.assign(queryBrowsers, targets);
} Let's first observe the code of function resolveTargets(queries: Browsers, env?: string): Targets {
const resolved = browserslist(queries, {
mobileToDesktop: true,
env,
});
return getLowestVersions(resolved);
} It calls the
function getLowestVersions(browsers: Array<string>): Targets {
return browsers.reduce((all: any, browser: string): any => {
// ...
}, {});
} When |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the detailed explainer. It is very helpful.
That said, the behavior is consistent here without getting to where I've changed it
I agree. The unit test is to ensure consistent behaviour. Whether resolveTargets
is called is implementation details somehow hidden from end users. Technically we can mock the browserslist
and asserts that resolveTargets
is not called, when it doesn't have to be, but I am cool with that.
The new test case looks good to me.
Yes, I think it would be a good idea to modify the definition of the I looked up the usage of Jest and found that it is possible to use But when trying to add the In addition, I have commented the changes in the source code to make it easier to understand. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! We use a custom Jest setup and run our tests on native ESM, which doesn't easily support mocking.
(process.env.BABEL_8_BREAKING ? it.skip : it)( | ||
"'browsers' option will have no effect if it is an empty array - Babel 7", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this test has the same behavior in Babel 7 and Babel 8:
(process.env.BABEL_8_BREAKING ? it.skip : it)( | |
"'browsers' option will have no effect if it is an empty array - Babel 7", | |
it("'browsers' option will have no effect if it is an empty array", |
also the other one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, In your opinion, the difference of Babel 7 and Babel 8 is only browsers
variable:
if (browsers == null) {
if (process.env.BABEL_8_BREAKING) {
// In Babel 8, if no targets are passed, we use browserslist's defaults
// and exclude IE 11.
browsers = ["defaults, not ie 11"];
} else {
// If no targets are passed, we need to overwrite browserslist's defaults
// so that we enable all transforms (acting like the now deprecated
// preset-latest).
browsers = [];
}
}
So if the result isn't have any effect of this, this test can also run on Babel 8.
I will update this commit later.
Thanks for comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Observe the following code:
(process.env.BABEL_8_BREAKING ? it.skip : it)(
"'browsers' option will have no effect if it is an empty array - Babel 7", () => {
expect(getTargets({ esmodules: "intersect", browsers: [] })).toEqual(
getTargets({ esmodules: "intersect" }),
);
},
);
it("The final 'browsers' handled variable will have no effect if it is an empty array", () => {
expect(getTargets({ esmodules: "intersect", browsers: [] })).toEqual(
getTargets(
{ esmodules: "intersect" },
{ ignoreBrowserslistConfig: true },
),
);
});
When ignoreBrowserslistConfig
is set to true
, no action is taken, but when this variable is not set and browsers
is not set, babel7 and babel8 behave differently.
So in my opinion, the first test can not remove the conditional judgment, the second test can.
During this time, I have searched for a lot of information and made various attempts, but there is still no way to modify the imported
So, we can only prove that it was called by making some side effects, and nothing can be done about the test cases that do not perform side effects. For the former, I tried adding a test. For the latter, I'm still reluctant to give up, but try to list the pseudocode below, it might work if I find a suitable way later: it("'resolveTargets' will not be called if 'browsers' is an empty array", () => {
getTargets({ esmodules: "intersect", browsers: [] });
expect(/* the number of browserslist function called */).toBe(0);
});
(process.env.BABEL_8_BREAKING ? it : it.skip)(
"'resolveTargets' will not be called if 'browsers' is not defined - Babel 7", () => {
getTargets({ esmodules: "intersect" });
expect(/* the number of browserslist function called */).toBe(0);
}
); |
let x = 0; | ||
|
||
// 'test' is an unknown browser query, so methods of 'browserslist' library will throw an error | ||
try { | ||
getTargets({ esmodules: "intersect", browsers: ["test"] }); | ||
} catch { | ||
x++; | ||
} | ||
|
||
expect(x).toBe(1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Jest has native support for this:
expect(() => getTargets({ esmodules: "intersect", browsers: ["test"] })).toThrow();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great idea, I've fixed it.
The tests are good enough; we only have to check that the old behavior is preserved! |
resolveTargets
call if browsers is emptyresolveTargets
call if browsers
is an empty array
In the process of using babel, I found that when
browserslistConfigFile
is not set tofalse
and browsersList is not set, babel will still call the related methods of browsersList and caniuse-lite, I think this is unnecessary, Because browsers is an empty array at this time, it is a truthy value, so I modified the judgment condition to not handle it when it is undefined or an empty array.