Skip to content

Commit

Permalink
feat(ssr): add support for superclasses @ W-17178263 (#4903)
Browse files Browse the repository at this point in the history
* chore: remove unnecessary block statements

* chore: merge separate build helpers

* test(ssr): add test for super fancy super class

* chore: no longer expect failures

* feat(ssr): give components access to parent component props

* feat(ssr): everything with a superclass is a component

* chore(ssr): move internal setup into setInternals

* revert: remove incorrect implementation

* feat(ssr): implement the thing

it's too late for descriptive commits

* revert: mostly undo changes

* feat(ssr): everything's a component

* feat(ssr): fully support superclass
  • Loading branch information
wjhsf authored Nov 21, 2024
1 parent f12f75d commit ca4fa26
Show file tree
Hide file tree
Showing 13 changed files with 70 additions and 39 deletions.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<x-component data-lwc-host-mutated="data-yolo" data-yolo>
<template shadowrootmode="open">
<div>
base mixin hello
</div>
</template>
</x-component>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const tagName = 'x-component';
export { default } from 'x/component';
export * from 'x/component';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import FancyElement from 'x/fancyElement'
import {FancyMixin} from 'x/fancyMixin'

const FancyMixedInElement = FancyMixin(FancyElement, 'mixin')

export default class extends FancyMixedInElement {
cmp = 'hello'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<div>{base} {mixed} {cmp}</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { LightningElement } from 'lwc';

export default class extends LightningElement {
base = 'base'
mixed = 'nope'
cmp = 'also nope'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const FancyMixin = (clazz, value) => class extends clazz {
mixed = value
connectedCallback() {
this.setAttribute('data-yolo', '')
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ export const expectedFailures = new Set([
'slot-not-at-top-level/ifTrue/shadow/index.js',
'slot-not-at-top-level/lwcIf/light/index.js',
'slot-not-at-top-level/lwcIf/shadow/index.js',
'superclass/mixin/index.js',
'superclass/override/index.js',
'svgs/index.js',
'wire/config/index.js',
'wire/deep-reference/index.js',
Expand Down
40 changes: 22 additions & 18 deletions packages/@lwc/ssr-compiler/src/compile-js/generate-markup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
MemberExpression,
Statement,
ExpressionStatement,
IfStatement,
} from 'estree';
import type { ComponentMetaState } from './types';

Expand Down Expand Up @@ -52,7 +53,7 @@ const bGenerateMarkup = esTemplate`
instance.connectedCallback();
__mutationTracker.disable(instance);
}
const tmplFn = ${isIdentOrRenderCall} ?? __fallbackTmpl;
const tmplFn = ${isIdentOrRenderCall} ?? ${/*component class*/ 3}[__SYMBOL__DEFAULT_TEMPLATE] ?? __fallbackTmpl;
yield \`<\${tagName}\`;
const hostHasScopedStylesheets =
Expand All @@ -65,11 +66,14 @@ const bGenerateMarkup = esTemplate`
yield* tmplFn(props, attrs, slotted, ${/*component class*/ 3}, instance);
yield \`</\${tagName}>\`;
}
`<ExportNamedDeclaration>;
${/* component class */ 3}[__SYMBOL__GENERATE_MARKUP] = generateMarkup;
`<[ExportNamedDeclaration, ExpressionStatement]>;

const bAssignGenerateMarkupToComponentClass = esTemplate`
${/* lwcClassName */ is.identifier}[__SYMBOL__GENERATE_MARKUP] = generateMarkup;
`<ExpressionStatement>;
const bExposeTemplate = esTemplate`
if (${/*template*/ is.identifier}) {
${/* component class */ is.identifier}[__SYMBOL__DEFAULT_TEMPLATE] = ${/*template*/ 0}
}
`<IfStatement>;

/**
* This builds a generator function `generateMarkup` and adds it to the component JS's
Expand Down Expand Up @@ -97,16 +101,22 @@ export function addGenerateMarkupExport(
// At the time of generation, the invoker does not have reference to its tag name to pass as an argument.
const defaultTagName = b.literal(tagName);
const classIdentifier = b.identifier(state.lwcClassName!);
const tmplVar = b.identifier('tmpl');
const renderCall = hasRenderMethod
? (b.callExpression(
b.memberExpression(b.identifier('instance'), b.identifier('render')),
[]
) as RenderCallExpression)
: b.identifier('tmpl');
: tmplVar;

let exposeTemplateBlock: IfStatement | null = null;
if (!tmplExplicitImports) {
const defaultTmplPath = `./${pathParse(filename).name}.html`;
program.body.unshift(bImportDeclaration({ default: 'tmpl' }, defaultTmplPath));
program.body.unshift(bImportDeclaration({ default: tmplVar.name }, defaultTmplPath));
program.body.unshift(
bImportDeclaration({ SYMBOL__DEFAULT_TEMPLATE: '__SYMBOL__DEFAULT_TEMPLATE' })
);
exposeTemplateBlock = bExposeTemplate(tmplVar, classIdentifier);
}

// If no wire adapters are detected on the component, we don't bother injecting the wire-related code.
Expand All @@ -123,12 +133,13 @@ export function addGenerateMarkupExport(
hasScopedStaticStylesheets: undefined,
mutationTracker: '__mutationTracker',
renderAttrs: '__renderAttrs',
SYMBOL__GENERATE_MARKUP: '__SYMBOL__GENERATE_MARKUP',
SYMBOL__SET_INTERNALS: '__SYMBOL__SET_INTERNALS',
establishContextfulRelationship: '__establishContextfulRelationship',
})
);
program.body.push(
bGenerateMarkup(
...bGenerateMarkup(
defaultTagName,
b.arrayExpression(publicFields.map(b.literal)),
b.arrayExpression(privateFields.map(b.literal)),
Expand All @@ -137,15 +148,8 @@ export function addGenerateMarkupExport(
renderCall
)
);
}

/**
* Attach the `generateMarkup` function to the Component class so that it can be found later
* during `renderComponent`.
*/
export function assignGenerateMarkupToComponent(program: Program, state: ComponentMetaState) {
program.body.unshift(
bImportDeclaration({ SYMBOL__GENERATE_MARKUP: '__SYMBOL__GENERATE_MARKUP' })
);
program.body.push(bAssignGenerateMarkupToComponentClass(b.identifier(state.lwcClassName!)));
if (exposeTemplateBlock) {
program.body.push(exposeTemplateBlock);
}
}
27 changes: 10 additions & 17 deletions packages/@lwc/ssr-compiler/src/compile-js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { transmogrify } from '../transmogrify';
import { replaceLwcImport } from './lwc-import';
import { catalogTmplImport } from './catalog-tmpls';
import { catalogStaticStylesheets, catalogAndReplaceStyleImports } from './stylesheets';
import { addGenerateMarkupExport, assignGenerateMarkupToComponent } from './generate-markup';
import { addGenerateMarkupExport } from './generate-markup';
import { catalogWireAdapters } from './wire';

import { removeDecoratorImport } from './remove-decorator-import';
Expand Down Expand Up @@ -46,23 +46,17 @@ const visitors: Visitors = {
);
},
ClassDeclaration(path, state) {
if (!path.node?.superClass) {
const { node } = path;
if (!node?.superClass) {
return;
}

if (
path.node.superClass.type === 'Identifier' &&
// It is possible to inherit from something that inherits from
// LightningElement, so the detection here needs additional work.
path.node.superClass.name === 'LightningElement'
) {
state.isLWC = true;
if (path.node.id) {
state.lwcClassName = path.node.id.name;
} else {
path.node.id = b.identifier('DefaultComponentName');
state.lwcClassName = 'DefaultComponentName';
}
// Assume everything with a superclass is an LWC component
state.isLWC = true;
if (node.id) {
state.lwcClassName = node.id.name;
} else {
node.id = b.identifier('DefaultComponentName');
state.lwcClassName = 'DefaultComponentName';
}
},
PropertyDefinition(path, state) {
Expand Down Expand Up @@ -199,7 +193,6 @@ export default function compileJS(
}

addGenerateMarkupExport(ast, state, tagName, filename);
assignGenerateMarkupToComponent(ast, state);

if (compilationMode === 'async' || compilationMode === 'sync') {
ast = transmogrify(ast, compilationMode);
Expand Down
2 changes: 1 addition & 1 deletion packages/@lwc/ssr-compiler/src/estemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ const getReplacementNode = (
validateReplacement.name ||
'(could not determine)';
const actualType = Array.isArray(replacementNode)
? `[${replacementNode.map((n) => n.type)}.join(', ')]`
? `[${replacementNode.map((n) => n && n.type)}.join(', ')]`
: replacementNode?.type;
throw new Error(
`Validation failed for templated node. Expected type ${expectedType}, but received ${actualType}.`
Expand Down
3 changes: 2 additions & 1 deletion packages/@lwc/ssr-runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ export { ClassList } from './class-list';
export {
LightningElement,
LightningElementConstructor,
SYMBOL__SET_INTERNALS,
SYMBOL__DEFAULT_TEMPLATE,
SYMBOL__GENERATE_MARKUP,
SYMBOL__SET_INTERNALS,
} from './lightning-element';
export { mutationTracker } from './mutation-tracker';
export { filterProperties } from './reflection';
Expand Down
1 change: 1 addition & 0 deletions packages/@lwc/ssr-runtime/src/lightning-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ interface PropsAvailableAtConstruction {

export const SYMBOL__SET_INTERNALS = Symbol('set-internals');
export const SYMBOL__GENERATE_MARKUP = Symbol('generate-markup');
export const SYMBOL__DEFAULT_TEMPLATE = Symbol('default-template');

export class LightningElement implements PropsAvailableAtConstruction {
static renderMode?: 'light' | 'shadow';
Expand Down

0 comments on commit ca4fa26

Please sign in to comment.