-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Remove extend dependency, becoming a zero-dependency package #9821
Conversation
I can confirm that this produces exactly the same object (compared after It would be great if the new |
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.
Some ideas, but ultimately these are suggestions, what's in this PR seems to already work just fine.
index.js
Outdated
@@ -34,6 +33,29 @@ function load() { | |||
return result; | |||
} | |||
|
|||
function extend(a, b) { | |||
// iterate over all direct and inherited enumerable properties |
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.
Including inherited properties should be harmless since there aren't any given that a
and b
were parsed from JSON, but it should also be unnecessary. Could Object.keys()
be used here, to end up with behavior more like Object.assign()
which copies enumerable own properties?
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.
Thank you for all of these thoughtful comments and questions. I also agree so much!
This leniency was done purposefully, because that’s how it behaved in the old (jQuery) extend method. From the extend
package documentation:
... properties inherited from the object's prototype will be copied over.
This is why I added the explicit comment. At least now it’s out in the open? 😅
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, very good to shine a light on it. It made me nervous since in BCD we have properties like "toString" and "hasOwnProperty" where p in {}
would return true. It seems like we don't have any accidents because of it, but dealing only with enumerable own properties seems preferable, or really matching whatever the behavior of Object.assign
is. (I'm still not sure about the case where there's an enumerable own property on the one side and a non-enumerable own property on the other. We can't have that in BCD, though.)
index.js
Outdated
for (const name in b) { | ||
let newValue = b[name]; | ||
|
||
// check if the new value is an object and its property exists on the former |
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.
This merging logic is more lenient than BCD needs, and would allow for weird errors in the BCD data which would go unnoticed unless there's a lint for it. This might be a good opportunity to simplify this to make more assumptions about the data, throwing exceptions if they're not met.
I think that any time a property exists on both a
and b
, both values must be regular objects (not arrays, for example), otherwise something is wrong.
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.
Doing this will reveal that there is some broken structure in BCD, the first one I run into is "bcd.css.selectors.not_match_link" defined in multiple files by accident.
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've fixed that in #9842.
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.
This might be a good opportunity to simplify this to make more assumptions about the data, throwing exceptions if they're not met.
Yes. I’d like to help here, if I can. 😄
This PR aims to provide a simplified method that would not throw new errors, and then also honor the ** peculiar** bits from the (jQuery) extend dependency implementation; versus something like a deep Object.assign
.
I see two options, and defer to your preference. One option is to "do the most", and improve the usefulness of this function and then bundle any resulting fixes that are surfaced. The other option is to "do the least", and approve or improve this function to replace the existing dependency, with its peculiar bits and leniencies out in the open.
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.
With that fixed, here's a less forgiving variant:
function isPlainObject(v) {
return typeof v === 'object' && v !== null && !Array.isArray(v);
}
function extend(target, source) {
if (!isPlainObject(target) || !isPlainObject(source)) {
throw new Error('Both target and source must be plain objects');
}
// iterate over all enumerable properties
for (const [key, value] of Object.entries(source)) {
// recursively extend if target has the same key, otherwise just assign
if (Object.prototype.hasOwnProperty.call(target, key)) {
extend(target[key], value);
} else {
target[key] = value;
}
}
return target;
}
Feel free to use as much or as little of that as you like.
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.
Sorry, I commented before I read your comment. A deep Object.assign
is indeed what I think we should use here, something that throws if used with arrays in particular. Object.assign({}, null)
doesn't throw, but that's a case we should also disallow to catch more errors I think.
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.
Hi, @foolip! After rebasing with the latest main
branch and updating the package-lock.json
file, everything continues to run as expected.
However, if I update the extend
function with the less forgiving variant then the lint
command fails.
The error is rather catastrophic on a terminal as the linter attempts to log massive objects as being invalid, but it starts like this:
✖ api/ANGLE_instanced_arrays.json
JSON Schema – 1 error:
I suspect the deep mutation of target
causes this error, and something else expects deep objects not to change.
I also tried updating the function to always return a new plain object, and the lint command failed again. This time, I suspect the missing shallow mutation of target
causes this error! Haha.
This place in the middle works, and is done in bde56ca 🎉
function isPlainObject(v) {
return typeof v === 'object' && v !== null && !Array.isArray(v);
}
function extend(target, source) {
if (!isPlainObject(target) || !isPlainObject(source)) {
throw new Error('Both target and source must be plain objects');
}
// iterate over all enumerable properties
for (const [key, value] of Object.entries(source)) {
// recursively extend if target has the same key, otherwise just assign
if (Object.prototype.hasOwnProperty.call(target, key)) {
target[key] = extend(extend({}, target[key]), value);
} else {
target[key] = value;
}
}
return target;
}
Of course, I’m glad to continue working with you on this, if you’d like to remove this last peculiar bit. 😄
index.js
Outdated
// check if the former value is also an object | ||
if (typeof oldValue === 'object' && oldValue !== null) { | ||
// update the new value as a new object extended by the former and later | ||
newValue = extend(extend({}, oldValue), newValue); |
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.
Are two extend
calls needed here? Since extend
modifies a
in-place, I think this could be just extend(oldValue, newValue)
, assuming the a[name] = newValue
is done in an else branch and not unconditionallly.
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, I agree that this is peculiar, which is a reason I think it belongs in the codebase and not abstracted away.
From my PR, with emphasis added:
... it extends them in a unique way, where any two nested objects are merged into one new object
The (jQuery) extend method mutates a root object, but it does not mutate any of its nested objects.
If this (new) extend method were simplified so that the nested objects were also mutated, then many tests would fail.
From those failures I inferred that this particular functionality is by design.
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 defer to @ddbeck, but I'd say that as long as the resulting object is identical to before, various peculiarities of extend
that we don't depend on need not be preserved, indeed it's better if they don't work :)
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 defer to @ddbeck, but I'd say that as long as the resulting object is identical to before, various peculiarities of
extend
that we don't depend on need not be preserved, indeed it's better if they don't work :)
I agree with this. I'm fine with making this less weird, if the resulting object is the same or it breaks in ways that reveal genuine data problems (as in #9842).
I think extend
was originally adopted when BCD was quite immature and it was convenient for experimentation, particularly when the schema went through some major changes early on (before my time—@Elchi3 can speak to this history better than I can). I don't think we've ever actually benefited from any of extend'
s oddities.
So I guess what I'm saying here is: feel free to make this less permissive, but let's make sure to do a full accounting of any differences in the resulting object before we merge.
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.
As it is, the resulting objects are identical before and after this change, and also if #9842 is merged, my shorter and stricter extend
produces the exact same result.
This should be confirmed once more before merging, of course.
@jonathantneal Thank you for opening this PR. This is pretty compelling (and reminds me that I've neglected some other dependent-friendly PRs 😬 ). I'm definitely drawn to things that are more explicit (and less peculiar). I'll respond to some of the line-comments shortly. But first, to respond to @foolip's top-level question:
On this point, could we go a half-step and check-in the tests now, but not permanently wire them up? There are a couple of PRs to compile all the JSON down to a single file to shrink the package size (see #7374). If we did that after this PR, we could simplify |
I love this! Replacing over 23kB of unpacked |
I’ll be around in an hour to implement the requests. |
a24671a
to
abfe33e
Compare
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.
Hi @jonathantneal, were you planning to come back to this PR? |
@ddbeck I've applied my own suggestions now and think this is good to go without @jonathantneal making any further changes, if you want to give it a review. |
I've also confirmed that the resulting data (serialized as JSON) is still exactly the same with this change, comparing commit 176d4ed and this PR merged into the same commit. |
Thanks, @foolip! 👍 |
Ping @ddbeck for review. If this goes stale, the work to validate that it doesn't change anything will have to be done again. |
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.
Thank you! 🎉
Actually, that wasn't enough: excellent work here, @jonathantneal and @foolip! I'm really pleased with what this achieves and the data problems it should prevent in the future. Well done! |
* Bump version to v3.3.6 * Add release note for #10646 * Add release note for #10581 * Add release note for #10685 * Add release note for #10691 * Add release note for #6957 * Add release note for #10721 * Add release note for #10695 * Add release note for #9821 * Add release note for #10681 * Add release note for #10725 * Add stats * Add release date * Wordsmith
This PR replaces the lone
extend
dependency with an explicit function, possibly improving the readability [a] ofindex.js
.a: this package only extends plain objects, and it extends them in a unique way, where any two nested objects are merged into one new object, rather than continually merging into the former. This unique strategy seems useful to have in the open.