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

feat: add dynamic components for @lwc/ssr-compiler #4847

Merged
merged 19 commits into from
Nov 15, 2024

Conversation

jmsjtu
Copy link
Member

@jmsjtu jmsjtu commented Nov 13, 2024

Details

This PR adds support for dynamic components in @lwc/ssr-compiler.

Does this pull request introduce a breaking change?

  • 😮‍💨 No, it does not introduce a breaking change.

Does this pull request introduce an observable change?

  • 🤞 No, it does not introduce an observable change.

GUS work item

W-16872192

@jmsjtu jmsjtu requested a review from a team as a code owner November 13, 2024 01:49
@@ -95,3 +103,38 @@ export function normalizeClassAttributeValue(value: string) {
// @ts-expect-error weird indirection results in wrong overload being picked up
return StringReplace.call(StringTrim.call(value), /\s+/g, ' ');
}

export function getChildAttrsOrProps(
Copy link
Member Author

@jmsjtu jmsjtu Nov 13, 2024

Choose a reason for hiding this comment

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

I moved this function to the shared file because I'm reusing it in lwc-component.ts.

Open to keeping it in the Component.ts if it's got things specific to Components only.

Comment on lines 86 to 99
return [
bIfLwcIsExpressionDefined(
lwcIsExpression,
bifLwcIsExpressionTypeCorrect(
lwcIsExpression,
bYieldFromDynamicComponentConstructorGenerator(
getChildAttrsOrProps(node.properties, cxt),
getChildAttrsOrProps(node.attributes, cxt),
lwcIsExpression
),
bThrowErrorForInvalidConstructor(lwcIsExpression)
)
),
];
Copy link
Member Author

Choose a reason for hiding this comment

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

Here's a sample output:

if (instance.customCtor !== undefined && instance.customCtor !== null) {
    if (typeof instance.customCtor === "function" && instance.customCtor.prototype instanceof LightningElement) {
      const childProps = cloneAndDeepFreeze({});
      const childAttrs = {};
      yield* instance.customCtor[SYMBOL__GENERATE_MARKUP](null, childProps, childAttrs);
    } else {
      throw new Error(`Invalid constructor ${String(instance.customCtor)} is not a LightningElement constructor.`);
    }
  }

Copy link
Contributor

Choose a reason for hiding this comment

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

nit: It's not immediately apparent to me why a bunch little builders were used instead of a larger esTemplate. This seems to work fine though - just not quite as readable.

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 wasn't sure what parts we wanted to keep in an esTemplate vs using a builder but this helps clear things up!

I'll redo this to use an esTemplate, I think it'll be easier to read as well!

Copy link
Member Author

Choose a reason for hiding this comment

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

@@ -30,6 +31,7 @@ type RenderCallExpression = SimpleCallExpression & {

const bGenerateMarkup = esTemplate`
export async function* generateMarkup(tagName, props, attrs, slotted, parent, scopeToken) {
tagName = tagName ?? ${/*component tag name*/ is.literal}
Copy link
Member Author

Choose a reason for hiding this comment

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

Sample output will look like this:

async function* generateMarkup$1(tagName, props, attrs, slotted, parent, scopeToken) {
  tagName = tagName ?? "x-test";

We need this to get the tag name that's passed to the transformer, otherwise, the constructor doesn't have a tag name to use.

// This is needed to generate markup for dynamic components which are invoked through
// the generateMarkup function on the constructor.
// At the time of generation, the invoker does not have reference to its tag name to pass as an argument.
const defaultTagName = b.literal(`${namespace}-${name}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

Just double checking: is the name value here kebab-cased or camel-cased?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Just checked. Yes it is camel-cased. So this needs to be transformed.

// Extract module name and namespace from file path.
// Specifier will only exist for modules with alias paths.
// Otherwise, use the file directory structure to resolve namespace and name.
const [namespace, name] =
specifier?.split('/') ?? path.dirname(filename).split(path.sep).slice(-2);

Copy link
Member Author

Choose a reason for hiding this comment

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

Added a helper function!

@@ -87,10 +89,17 @@ const bAssignGenerateMarkupToComponentClass = esTemplate`
export function addGenerateMarkupExport(
program: Program,
state: ComponentMetaState,
options: TransformOptions,
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure what @nolanlawson thinks about this, but it might be worth scoping this down to { name: string, namespace: string }.

Copy link
Member Author

Choose a reason for hiding this comment

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

Comment on lines 86 to 99
return [
bIfLwcIsExpressionDefined(
lwcIsExpression,
bifLwcIsExpressionTypeCorrect(
lwcIsExpression,
bYieldFromDynamicComponentConstructorGenerator(
getChildAttrsOrProps(node.properties, cxt),
getChildAttrsOrProps(node.attributes, cxt),
lwcIsExpression
),
bThrowErrorForInvalidConstructor(lwcIsExpression)
)
),
];
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: It's not immediately apparent to me why a bunch little builders were used instead of a larger esTemplate. This seems to work fine though - just not quite as readable.

Copy link
Collaborator

@nolanlawson nolanlawson left a comment

Choose a reason for hiding this comment

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

First pass of a review.

// This is needed to generate markup for dynamic components which are invoked through
// the generateMarkup function on the constructor.
// At the time of generation, the invoker does not have reference to its tag name to pass as an argument.
const defaultTagName = b.literal(`${namespace}-${name}`);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just checked. Yes it is camel-cased. So this needs to be transformed.

// Extract module name and namespace from file path.
// Specifier will only exist for modules with alias paths.
// Otherwise, use the file directory structure to resolve namespace and name.
const [namespace, name] =
specifier?.split('/') ?? path.dirname(filename).split(path.sep).slice(-2);

@nolanlawson
Copy link
Collaborator

This part doesn't seem to be rendering correctly:

  const hostHasScopedStylesheets = tmplFn.hasScopedStylesheets || hasScopedStaticStylesheets(["customCtor"]);
  // ...
  yield* tmplFn(props, attrs, slotted, ["customCtor"], instance);

Notice the ["customCtor"] which I think should be props.customCtor.

@nolanlawson
Copy link
Collaborator

I also notice that we don't have any tests for slots passed in to a dynamic component, or scope token (scoped style) classes on the dynamic component. We should add these for safety.

@nolanlawson
Copy link
Collaborator

Looks like slotted content is still not working; opened a PR with tests to confirm: #4872.

The good news though is that scoped slots are broken already so we don't have to handle those 😆 #4871

Copy link
Collaborator

@nolanlawson nolanlawson left a comment

Choose a reason for hiding this comment

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

We can get this PR in now and then figure out slotted content in a follow-up PR. Looks good!

@jmsjtu
Copy link
Member Author

jmsjtu commented Nov 15, 2024

@nolanlawson sounds good, I'll work on the follow-up PR to get the slotted content in!

@jmsjtu jmsjtu merged commit b777da2 into master Nov 15, 2024
11 checks passed
@jmsjtu jmsjtu deleted the jtu/dynamic-components branch November 15, 2024 00:39
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.

3 participants