Skip to content
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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

wycats
Copy link
Member

@wycats wycats commented Oct 22, 2024

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.

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 {
Copy link
Contributor

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? 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

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
Copy link
Contributor

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? 😅

Copy link
Member Author

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(
Copy link
Contributor

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)

Copy link
Member Author

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

} else {
throw new Error(
'Accessing `this.element` is not allowed in non-interactive environments (such as FastBoot).'
);
}
}

getBounds(view: View): {
getBounds(component: View): {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's a "view"? 😅 🙈

Copy link
Member Author

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 {
Copy link
Contributor

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?

Copy link
Member Author

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 },
Copy link
Contributor

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?

Copy link
Member Author

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 } });
Copy link
Contributor

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

Copy link
Member Author

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 } });
Copy link
Contributor

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}}',
Copy link
Contributor

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

Copy link
Contributor

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 🙃

Copy link
Contributor

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)

Copy link
Member Author

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}}
Copy link
Contributor

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?

Copy link
Member Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants