-
Notifications
You must be signed in to change notification settings - Fork 9.4k
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
core(driver): create eval code using interface #10816
Conversation
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 is looking great.
It is still going to be weird/misleading to have files that are half running in node and half running in the browser so I think it's probably worth opening up to the eng sync next week, but I'm feeling net-good about the tradeoff :)
Copied
HTMLElementTagNameMap
fromdom.js
forgetElementsInDocument
. I renamed it because TS wasn't picking it up, I'm surprised it even worked indom.js
because it's overriding the same identifier.
Isn't it called HTMLElementByTagName
though? I don't think it should be overriding anything..
lighthouse-core/gather/driver.js
Outdated
* @template T, R | ||
* @param {string | ((...args: T[]) => R)} expression | ||
* @param {{useIsolation?: boolean, mode?: 'iffe'|'function', args?: T[], deps?: (Function|string)[]}=} options | ||
* @return {Promise<R>} |
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.
Should we just split these into two eval functions?. Keep evaluateAsync
and make a new evaluateFunction
or something:
- js-style "overloading" tends to grow in implementation complications over time and is harder to understand as a caller (e.g.
options
could be included with astring
expression
, but would it do anything in that case? Now we have to document that, etc) - There seems to be a bug in jsdoc generics where
R
defaults toany
here ifexpression
is astring
(so there's no inferredR
). In the typescript equivalentR
will default tounknown
(as of Typescript 3.5). If that gets fixed for JS, all theevaluateAsync(string)
calls will have to be changed too. I can't think of a great way to set the return type the right way since you still can't do generic defaults or conditional types in jsdoc (AFAIK).
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.
("fun" fact: evaluateAsync
is called evaluateAsync
and not evaluate
because way back, "evaluate
" sounded synchronous and we didn't have types to give IDE clues or type checking to ensure that calling code would handle a promised return value. That does make variations on evaluateAsync
a little awkward, though, since the "Async" part seems really unnecessary now :)
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 great feedback!
js-style "overloading" tends to grow in implementation complications over time and is harder to understand as a caller (e.g. options could be included with a string expression, but would it do anything in that case? Now we have to document that, etc)
I took a second pass to incorporate this feedback, and added some docs. Running to a meeting, will respond to the rest later.
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.
Would you prefer splitting this out to evaluate(fn, ...)
and evaluateString(code, ...)
(alias for evalauteAsync
)?
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.
rather than add another evaluate*()
hop with _evaluate
, what do you think about keeping it called evaluateAsync
, so
evaluate(fn, ...)
calls evaluateAsync(string, opts)
calls _evaluateInContext(string, contextId)
then in the next breaking change we either incorporate the guts of evaluateAsync
into evaluate
so it's back to only two functions, or we rename evaluateAsync
to evaluateString
or whatever if we do want to keep it around for folks to use?
I see the reason for doing it the _evaluate
way, but the number of evaluate layers doesn't seem worth it when external to driver we still only have evaluate()
and evaluateAsync
either way.
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.
done
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'm a little confused about the purpose of _evaluate
. Is it for a possible patch where evaluateAsync
is removed but we still need a function for executing short expressions?
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 I just forgot to delete _evaluate
.
Yeah, I messed up the copy/paste and got confused. Put the names back. Should one of these files do a type import for the other? |
lighthouse-core/gather/driver.js
Outdated
* @return {Promise<*>} | ||
*/ | ||
async evaluateAsync(expression, options = {}) { | ||
return this.evaluate(expression, options); |
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.
Could drop this. if we like this, I'd follow up with a PR that changes all existing usages to this.
lighthouse-core/gather/driver.js
Outdated
* See the documentation for `createEvalCode` in `page-functions.js`. | ||
* @template T, R | ||
* @param {string | ((...args: T[]) => R)} expressionOrMainFn | ||
* @param {{useIsolation?: boolean, args?: T[], deps?: (Function|string)[]}=} options |
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.
FYI I would like to drop the string
part eventually.
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.
FYI I would like to drop the
string
part eventually.
could we drop it now and say "if you still want to use a string use the legacy evaluateAsync()
"?
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.
done
I had the same thought but the files are in such different contexts I don't know which one should import from the other :) edit: maybe it doesn't matter and it could be in both. |
@brendankenny any more feedback? |
lighthouse-core/gather/driver.js
Outdated
* @template T, R | ||
* @param {string | ((...args: T[]) => R)} expression | ||
* @param {{useIsolation?: boolean, mode?: 'iffe'|'function', args?: T[], deps?: (Function|string)[]}=} options | ||
* @return {Promise<R>} |
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.
rather than add another evaluate*()
hop with _evaluate
, what do you think about keeping it called evaluateAsync
, so
evaluate(fn, ...)
calls evaluateAsync(string, opts)
calls _evaluateInContext(string, contextId)
then in the next breaking change we either incorporate the guts of evaluateAsync
into evaluate
so it's back to only two functions, or we rename evaluateAsync
to evaluateString
or whatever if we do want to keep it around for folks to use?
I see the reason for doing it the _evaluate
way, but the number of evaluate layers doesn't seem worth it when external to driver we still only have evaluate()
and evaluateAsync
either way.
* declaration statement. Args should match the args of `mainFn`, and can be any serializable | ||
* value. `deps` are functions that must be defined for `mainFn` to work. | ||
*/ | ||
function createEvalCode(mainFn, {mode, args, deps} = {}) { |
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.
could this be directly incorporated into evaluate()
(and evaluateFunctionOnObject
) instead of splitting it into this file? It's mostly not shared code since the two callers take different branches in the conditional (and it's not really a "page function" in the same way as the other functions in this file are).
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 prefer it here bc/ it's easier to test (driver has some overhead with beforeEach
etc.) and
keeps the driver file smaller (it's so big). Maybe lib/eval.js
?
And then export two function: createFunctionEvalCode
and createIffeEvalCode
?
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 prefer it here bc/ it's easier to test (driver has some overhead with
beforeEach
etc.) and
keeps the driver file smaller (it's so big). Maybelib/eval.js
?And then export two function:
createFunctionEvalCode
andcreateIffeEvalCode
?
well it would definitely be good to split the two, but it'll also have a new home after #11633, and it also looks trivial to add to .evaluateAsync
in driver-test
?
const {expression} = connectionStub.sendCommand.findInvocation('Runtime.evaluate');
expect(expression).toEqual(`(() => {
function mainFn() {
return true;
}
return mainFn();
})()`);
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.
@patrickhulce would you prefer this going into driver.js
or a new file lib/eval.js
?
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.
new lib/eval.js
file SGTM, though I'll also +1 @brendankenny's test suggest to make sure .evaluate
uses it :)
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.
new lib/eval.js file
Isn't the (currently named) RuntimeController
going to be this already in #11633, though? I guess I don't understand what the problem is just inlining the five lines or so :)
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.
Isn't the (currently named) RuntimeController going to be this already in #11633, though?
Ya, that's why I don't really care much where it lives in this PR :)
I guess I don't understand what the problem is just inlining the five lines or so
I mean I don't feel terribly strongly about it, the rebase gets slightly more annoying with inline but not by much.
saving for separate pr
|
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.
LGTM
}); | ||
}())`.trim(); | ||
expect(expression).toBe(expected); | ||
expect(await eval(expression)).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.
with this eval
idea you're introducing we could probably add some tests that exercise the error catching some time
cc @patrickhulce :)
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.
my hope is that everything in driver becomes less scary and more obvious to test well when it's not all together :)
|
||
/** | ||
* @param {{a: number, b: number}} _ | ||
* @param {any} passThru |
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.
no need to change, but I'm curious if there's a reason to make this any
?
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.
nothing happens to it, so the most applicable type is any.
oof, globalThis requires node 12. #11656 well, this has been open for 544 days (I really thought I opened this much more recently...woah), so what's a few more :) |
This and #11633 will have quite the merge conundrum to sort out :) Are you planning on landing this imminently? |
shouldn't it be not too hard? It's just the one function now and the tests are a separate block ( |
I mean sure, no PRs of reasonable size are ever really "too hard", but on the spectrum from normal Lighthouse PR with no merge conflicts to literally every line has a conflict, we're closer to the latter if we assume |
Blocked on #11656 for usage of |
Sorry bud 😞 Don't worry though, I hear it's not too hard 😉 |
it's not automatic, but it's not hard :P
everything is good!
|
not sure what to do with the jsdocs. 100% duplicate in both places? |
Idea 1
from #10781Where simple, I also converted from
getElementsInDocumentString
togetElementsInDocument
. Skipped for tap-targets, was too much.Copied
HTMLElementTagNameMap
fromdom.js
forgetElementsInDocument
. I renamed it because TS wasn't picking it up, I'm surprised it even worked indom.js
because it's overriding the same identifier.