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

rfc: engine plugins #899

Closed
wants to merge 9 commits into from
Closed

rfc: engine plugins #899

wants to merge 9 commits into from

Conversation

davidturissini
Copy link
Contributor

Details

RFC for LWC plugins

Does this PR introduce a breaking change?

  • Yes
  • No

@davidturissini davidturissini changed the title Dturissini/services rfc: engine plugins Dec 19, 2018
}
```

- `wiring` - invoked at component creationg time
Copy link
Contributor

@kevinv11n kevinv11n Dec 20, 2018

Choose a reason for hiding this comment

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

Typo in creation. Also this hook fires post constructor pre prop assignment.

- `locator` - invoked before element "click" handler is executed
- `connected` - invoked sync after a component element has been appended, but before component has rendered (lifecycle service for wire)
- `disconnected` - invoked sync after a component has been disconnected (lifecycle service for wire)
- `rendered` - invoked sync after a component has rendered, but before `renderedCallback` had been called (lifecycle service for wire)
Copy link
Contributor

Choose a reason for hiding this comment

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

wire doesn't use this

register('wire', { ... });
```

If an element does not have `locator` data on its context, the locator protocol is never invoked. Likewisem if no `wire` data is defined on context, the protocol will not be invoked. This will simplify plugin code that has to do manual checks today.
Copy link
Contributor

Choose a reason for hiding this comment

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

typo is likewise


The only change in registering services would require a service id argument. With the service id argument, the engine can make a decision about calling an external service based on data being present:

```
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
```
```js



#### Locators
```
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
```
```js

const resolved = {
target : locatorContext.id,
host : hostContext.id,
targetContext : isFunction(locatorContext.context) && locatorContext.context(),
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
targetContext : isFunction(locatorContext.context) && locatorContext.context(),
targetContext : typeof locatorContext.context === 'function' && locatorContext.context(),

target : locatorContext.id,
host : hostContext.id,
targetContext : isFunction(locatorContext.context) && locatorContext.context(),
hostContext : isFunction(hostElement.context) && hostElement.context()
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
hostContext : isFunction(hostElement.context) && hostElement.context()
hostContext : typeof hostElement.context === function && hostElement.context()


## Potential alternatives

- Instead of passing component instance, maybe we can pass a facade that only exposes public component methods and properties
Copy link
Member

Choose a reason for hiding this comment

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

IMO plugins is considered as trusted code, I don't see any issue with giving the actual object instead of a facade.

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah, today we give them the component instance, which is supposed to have less capabilities than the element, although in reality, from the component you can always access the element via this.template.host, which was blocked in the pass. My fears were mostly about wire adapters doing funky stuff by using the DOM as a transport layer, but for that we do have another layer, the wire decorator itself, so I think we will be fine exposing both.

import { register } from 'lwc';

if (process.env.NODE_ENV !== 'production') {
register('benchmark', {
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand how this plugin would work.

If an element does not have locator data on its context, the locator protocol is never invoked.

When we want to profile a component, we would need to a to all components the benchmark flag in their context?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is an open question I have as well. Is context a requirement for executing a plugin or should the engine ask if the plugin should be executed for a particular element?

Potentially, we could add a shouldInvokePlugin method (we can bikeshed on name) that tells the engine whether a plugin should be invoked for a given element:

shouldInvokePlugin(element: HTMLElement, componentInstance?: ComponentInstance) -> Bool

Copy link
Contributor

@tariqrafique tariqrafique Dec 20, 2018

Choose a reason for hiding this comment

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

benchmark could be one of those categories of plugins that you would want to run for all elements. We shouldn't need components to be tagged with a benchmark attribute

Copy link
Contributor

Choose a reason for hiding this comment

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

This is not a problem. Remember, you can add metadata to all elements if you can associate that to LightningElement. Similarly, the compiler can add metadata to any class via registerComponent() to instrument all or a subset of them, saying, I want to instrument only lightning namespace components.

Copy link
Contributor

Choose a reason for hiding this comment

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

So if I choose to, at runtime I can make a plugin run for all components ?

Copy link
Contributor

Choose a reason for hiding this comment

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

yes, sort of... you must have accesss to the plugin, but we haven't really get into that just yet.

elementDidDisconnect?(element: HTMLElement, componentInstance?: ComponentInstance, context: ServiceContext)
- Invoked immediately after the element has been removed from the DOM

elementWillRender?(element: HTMLElement, componentInstance?: ComponentInstance, context: ServiceContext)
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand what is hooks applied to all the elements rendered by the engine, while others are used only on components. For example, I would expect elementWillRender to be only applied to the element associated with a component.

Copy link
Contributor

Choose a reason for hiding this comment

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

this is a good question. I have a similar reaction, and I think we can change the name to something like elementWasMutated, which for custom elements means that it is (or might be) about to be rehydrated/rendered, and for a regular element it means that it was just mutated, nothing else.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we will need a reflection for this one, elementWillMutate and elementDidMutate, so, wire and other services can prepare ahead of time.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree, this hook can be limited to only custom elements. @caridy when do you envision elementDidMutate firing? Before a component has rendered but after props have been applied?

Copy link
Contributor

Choose a reason for hiding this comment

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

no, not related to rendering because regular elements don't have this. this is about I'm about to diff this element, vs I'm done diffing the element.


## Open Questions

- Should we expose lifecycle for constructor methods?
Copy link
Member

Choose a reason for hiding this comment

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

That's a good question, I think there is an overlap here with my comments about the APIs. Some of the APIs today feels that they are only applied to components and not to elements.

Copy link
Contributor

Choose a reason for hiding this comment

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

well, the hook is really about creation, which is the process of creating the element, whatever that element isn't or not is not that important.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The motivation for this was around benchmark, which only cares about the timing of Custom Elements constructor, not native elements

## Open Questions

- Should we expose lifecycle for constructor methods?
- How do we handle plugins that might not need context data but should only execute for a subset of elements? (For example, only custom elements, only native elements)
Copy link
Member

Choose a reason for hiding this comment

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

As far as I understand this proposal the only way to add context to an element is via the template. There is theoretically no way to add new plugins without instructing the compiler. Right?

Copy link
Contributor

Choose a reason for hiding this comment

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

@pmdartus no, this proposal contemplates decoration of the class definition to add metadata to the component instance, so it is analogous to adding the same info via a template onto instances of this component.

As for the note, I don't think we want to differentiate them, but we can chat more about this.

```
import { register } from 'lwc';
register('locator', { ... });
register('wire', { ... });
Copy link
Contributor

Choose a reason for hiding this comment

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

just a clarification, with this change, the wire service can be only one... you can't call register with wire more than once, is that correct?

Copy link
Contributor

Choose a reason for hiding this comment

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

if the answer to my question is yes, then that means you can always call register('wire', {}) to disable that service for testing and other scenarios that are not possible today.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yea, the idea here is that you should only call register with a given identifier once. Essentially, this will add the plugin to an internal map. They identifier would be used to grab the context data and pass it to the plugin.

Copy link
Contributor

Choose a reason for hiding this comment

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

If we ever start using this for internal engine job (internal plugins), we need to guarantee that they can't be overrule by another one, maybe we can achieve this with some sort of aggregation, e.g.: assign(create(null), externalPlugins, internalPlugins);

this also pose another restriction, two independent pieces of code can't really actuate over one plugin identifier, but I think that's a cleaner design overall.


Today, locators and lwc:dom directives are defined in the component template and are initialized with the `context` property on the VNode. Wire, on the other hand, is defined on the component class and is initialized with the `wire` property on the Component Definition.

This document proposes that _all_ plugin be migrated to a single `context` object that is composed of `template context` and `class context`. The `context` object should be a dictionary of `PluginContext` associated by a `PluginIdentifier`:
Copy link
Contributor

Choose a reason for hiding this comment

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

one open question here is what to do when both, the class context and the template content for instances of that class are defined for a service, which one wins? or is this aggregated? or is this provided to the service somehow? I don't have a specific scenario where that is a thing, but could be possible for other services in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll need to think about this some more, but I think one scenario where this might happen would be with wire. IIRC, there was a wire directive at some point for query rollup. Not sure if thats still the plan, but this is definitely something to think about.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

One way we could approach this, is to have the plugin itself handle merging data. For example, if plugin foo has class context and template context, we could ask foo to generate a single context object from that. That way, the plugin can decide which one wins.

Copy link
Contributor

Choose a reason for hiding this comment

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

I will like to see more details about this.

@caridy
Copy link
Contributor

caridy commented Dec 20, 2018

more folks that should review this:

@tariqrafique tariqrafique self-requested a review December 20, 2018 19:05

The following are examples on how existing services might use the plugin protocol:

### Locators
Copy link
Contributor

Choose a reason for hiding this comment

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

One of the nice things we have today is that locators can get some sanity checking today with the compiler. locator:id must be a string and locator:context must be an expression. Additional this locator:context with MemberExpression is also blocked.

<a ... locator:context={foo.bar}>

The reason for this is that the user shouldn't have any expectation that we will execute the context function callback within non-component context. The locator:context function is always executed within the Component this context.

You can see some examples of parser validation here

Copy link
Contributor

Choose a reason for hiding this comment

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

We'll need make a decision at compile time on what context we would expect plugin attribute expressions to be bound to.

<template for:each={state.todos} for:item="todo" for:index="index">
    <li key={todo.id}>
        <button onclick={todo.clickHandler}
                locator:id="todo-item" locator:context={locatorProviderTodo}>
            {todo.text}
        </button>
    </li>
</template>

In the example above, locatorProviderTodo is bound to the component instance

if instead, it was written to use a function from the todo item in the iteration,

<template for:each={state.todos} for:item="todo" for:index="index">
    <li key={todo.id}>
        <button onclick={todo.clickHandler}
                locator:id="todo-item" locator:context={todo.locatorProviderTodoItem}>
            {todo.text}
        </button>
    </li>
</template>

what should the function bind to ? probably the todo item. or maybe not.

To avoid this decision, we enforce that for locators, you can only use context function that's directly on the component and it will only be bound to the component at invoke time. Anything with a member expression is blocked

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I think schema validation for custom directives are not that hard to support, it is just a configuration option in the compiler IMO, @pmdartus will know better.

const resolved = {
target : locatorContext.id,
host : hostContext.id,
targetContext : typeof locatorContext.context === 'function' && locatorContext.context(),
Copy link
Contributor

Choose a reason for hiding this comment

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

for locatorContext.context() to execute within with the Component this context, we'll need to make sure that the compiler always binds any plugin expression functions to the component.

Copy link
Contributor

Choose a reason for hiding this comment

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

this is an easy fix, we have that in the scope.

host : hostContext.id,
targetContext : typeof locatorContext.context === 'function' && locatorContext.context(),
hostContext : typeof hostElement.context === 'function' && hostElement.context()
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
};
};
handler(evt);

@diervo
Copy link
Contributor

diervo commented Jan 2, 2019

I overall like the proposal to unify context, however I'm concerned with giving access to pretty much all internals. I would rather be opening this slowly and with abstractions rather than give access to all things.

Moreover it seems that we will have way too many hooking points, which may have some bad perf impact plus might be confusing. We should try to figure out if we can boile down both the hooks and the arguments being passed in.

@davidturissini
Copy link
Contributor Author

Closing for now. If we need to, we can reopen

@davidturissini davidturissini deleted the dturissini/services branch May 24, 2019 16:50
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.

None yet

7 participants