-
-
Notifications
You must be signed in to change notification settings - Fork 4.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
Initial implementation of renderComponent #20781
base: main
Are you sure you want to change the base?
Conversation
There's more in this commit than there should be, largely because this commit bundles a change to the plugins that fixes lexical scope bugs. I intend to separate those changes out into their own PR before attempting to merge this one. This PR also needs a flag for the new API. Finally this commit adds some new testing infrastructure to generalize the base render tests so it can be used with templates that are not registered into a container. This is useful more generally, and could be used in other places in the test suite in the future.
private _owner: InternalOwner; | ||
private _context: CompileTimeCompilationContext; | ||
private _runtime: RuntimeContext; | ||
export class RendererState { |
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.
how different is all this from how Ember renders an app? 🤔
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.
ah I see: https://github.com/emberjs/ember.js/pull/20781/files#diff-cdf95f0c6c10226a54fbac271478aaa1defd908f0f998fec0f484e52d2544184R603 and https://github.com/emberjs/ember.js/pull/20781/files#diff-cdf95f0c6c10226a54fbac271478aaa1defd908f0f998fec0f484e52d2544184L492 <- original code here, and a lot is added
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.
For the record, I think that a lot of this could be factored to share more code. But when I tried to do more than I did here, I kept running into subtle assumptions I was missing in the original code, so I decided to share the obvious stuff and allow more sharing to happen incrementally once people have time to take a careful inventory of the expected behavior of the original code.
* This function returns `undefined` if there was an error rendering the | ||
* component. | ||
* | ||
* @fixme restructure this to return a result containing the error rather than |
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.
what's the timeline of this fixme? 😅
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 intended it as a pre-merge note to decide whether it should be fixed or accepted as tolerable. It should still be addressed one way or another before merging.
* @fixme restructure this to return a result containing the error rather than | ||
* undefined. | ||
*/ | ||
export function renderComponent( |
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 we also want a hydrateComponent? (🙈 what fastboot does )
(this would make Astro's runtime behavior with ember super easy)
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 that's sensible. Maybe it should just be an option like it is in Glimmer: do you want the client-side element builder or the rehydrating one. That's the only difference in behavior between hydrating and normal rendering.
private _owner: InternalOwner; | ||
private _context: CompileTimeCompilationContext; | ||
private _runtime: RuntimeContext; | ||
export class RendererState { |
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.
ah I see: https://github.com/emberjs/ember.js/pull/20781/files#diff-cdf95f0c6c10226a54fbac271478aaa1defd908f0f998fec0f484e52d2544184R603 and https://github.com/emberjs/ember.js/pull/20781/files#diff-cdf95f0c6c10226a54fbac271478aaa1defd908f0f998fec0f484e52d2544184L492 <- original code here, and a lot is added
} else { | ||
throw new Error( | ||
'Accessing `this.element` is not allowed in non-interactive environments (such as FastBoot).' | ||
); | ||
} | ||
} | ||
|
||
getBounds(view: View): { | ||
getBounds(component: View): { |
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.
what's a "view"? 😅 🙈
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.
TL;DR almost every use of the word "view" means "classic component". I considered renaming all view
to classicComponent
but it was expanding the scope of this PR a lot. That said, it's actually really clarifying when you see an internal API that literally only takes classic components. It means it must not be all that fundamental to the rendering process since empirically we actually do render a lot of Glimmer components.
That said, it also illustrates a way in which route templates are special: they rely on mounting via APIs on the renderer, and (before this PR) there was literally no way to mount a root component or an outlet component in Ember that was implemented as a Glimmer component.
/** | ||
* Resolution for non built ins is now handled by the vm as we are using strict mode | ||
*/ | ||
export class StrictResolver implements VMRuntimeResolver, VMCompileTimeResolver { |
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.
why do we need a resolver at all? can we not have one? 😅
like, why do we still need lookupHelper?
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.
Originally, I thought: I want Glimmer to support the "no resolver" case, and then we wouldn't need one. We'd still want it for now to avoid blocking this PR on that Glimmer change.
As I was implementing this feature, though, I realized that keywords like array
and hash
and builtins used by AST transformations are not implemented as either Glimmer keywords or AST transformations. Instead, many of them are implemented as custom helpers looked up through the resolver. This is definitely wrong, but it's another thing I don't want to try to fix at the same time as the rest of this PR.
run(() => { | ||
this.component = renderComponent(component, { | ||
owner, | ||
env: { document: document, isInteractive: true, hasDOM: true }, |
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.
what happens when isInteractive is false?
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.
When isInteractive
is false, modifiers don't run. When hasDOM
is true, we default to getting document
(and I think other browser-only globals) from globalThis
. When it's false, you're required to explicitly provide them.
I think isInteractive
makes sense (and exists in Glimmer), but hasDOM
feels like a hack that escaped the lab and should be replaced with being explicit in the internals and using the default browser behavior at the edge.
|
||
'@test Can shadow keywords'() { | ||
let ifComponent = defineComponent({}, 'Hello, world!'); | ||
let Bar = defComponent('{{#if}}{{/if}}', { scope: { if: ifComponent } }); |
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 understand that this is how strict mode is supposed to work, but I hope no one does this in a real project.
Could allow us to make some good memes tho, like how Cish folks redefine true to be false or whatever
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.
It's also impossible to represent using <template>
, since <template>
populates the scope with names in the JavaScript lexical scope, and if
is a keyword. I don't think we ever specified this, but I assume that the content tag syntax is only expected to work in modules, or you'd be able to define a yield
variable and shadow the {{yield}}
keyword :yikes:.
} | ||
|
||
'@test Can use concat'() { | ||
let Root = defComponent('{{(concat "Hello" ", " "world!")}}', { scope: { concat } }); |
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.
wrapping ()
shouldn't be needed
|
||
'@test Can use get'() { | ||
let Root = defComponent( | ||
'{{#let (hash value="Hello, world!") as |hash|}}{{(get hash "value")}}{{/let}}', |
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.
same here with get, {{get hash "value"}}
should be equiv
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.
unless I am missing what is being tested here 🙃
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.
(or if this just isn't an important distinction)
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 test is explicitly testing the get
keyword, so I had to come up with an example that uses get
. ;)
} | ||
|
||
// Ember currently uses AST plugins to implement certain features that | ||
// glimmer-vm does not natively provide, such as {{#each-in}}, {{outlet}} |
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 don't know if I've gotten an answer on this -- but shouldn't the VM have each-in? how important is it where built ins live?
if we put more builtins in ember (say: on, hash, fn, array, element), are we saying that Glimmer shouldn't be used without ember?
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 the VM should indeed have each-in
. I also think that implementing these things as AST plugins is causing a lot more trouble than we seem to have realized. For now, I decided to document and clean up the behavior, but I agree strongly with the implication of your various comments that we should move a lot of the nuts-and-bolts implementations of low-level things into the VM.
There's more in this commit than there should be, largely because this
commit bundles a change to the plugins that fixes lexical scope bugs.
I intend to separate those changes out into their own PR before
attempting to merge this one. This PR also needs a flag for the new API.
Finally this commit adds some new testing infrastructure to generalize
the base render tests so it can be used with templates that are not
registered into a container. This is useful more generally, and could
be used in other places in the test suite in the future.