Skip to content

Commit 641b740

Browse files
nolanlawsonwjhsf
andauthored
feat: allow passing default export to renderComponent (#4727)
Co-authored-by: Will Harney <[email protected]>
1 parent 59251bf commit 641b740

File tree

9 files changed

+147
-19
lines changed

9 files changed

+147
-19
lines changed

packages/@lwc/perf-benchmarks/src/__benchmarks__/ssr/table-render-10k.benchmark.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import { renderComponent } from '@lwc/ssr-runtime';
99

10-
import { generateMarkup } from '@lwc/perf-benchmarks-components/dist/ssr/benchmark/table/table.js';
10+
import Table from '@lwc/perf-benchmarks-components/dist/ssr/benchmark/table/table.js';
1111
import Store from '@lwc/perf-benchmarks-components/dist/ssr/benchmark/store/store.js';
1212

1313
const SSR_MODE = 'asyncYield';
@@ -19,7 +19,7 @@ benchmark(`ssr/table-v2/render/10k`, () => {
1919

2020
return renderComponent(
2121
'benchmark-table',
22-
generateMarkup,
22+
Table,
2323
{
2424
rows: store.data,
2525
},

packages/@lwc/perf-benchmarks/src/__benchmarks__/ssr/tablecmp-render-10k.benchmark.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import { renderComponent } from '@lwc/ssr-runtime';
99

10-
import { generateMarkup } from '@lwc/perf-benchmarks-components/dist/ssr/benchmark/tableComponent/tableComponent.js';
10+
import Table from '@lwc/perf-benchmarks-components/dist/ssr/benchmark/tableComponent/tableComponent.js';
1111
import Store from '@lwc/perf-benchmarks-components/dist/ssr/benchmark/store/store.js';
1212

1313
const SSR_MODE = 'asyncYield';
@@ -19,7 +19,7 @@ benchmark(`ssr/table-component/render/10k`, () => {
1919

2020
return renderComponent(
2121
'benchmark-table',
22-
generateMarkup,
22+
Table,
2323
{
2424
rows: store.data,
2525
},

packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import type { CompilationMode } from '../index';
1717
interface FixtureModule {
1818
tagName: string;
1919
default: any;
20-
generateMarkup: any;
2120
props?: { [key: string]: any };
2221
features?: FeatureFlagName[];
2322
}
@@ -88,7 +87,7 @@ function testFixtures() {
8887
try {
8988
result = await serverSideRenderComponent(
9089
module!.tagName,
91-
module!.generateMarkup,
90+
module!.default,
9291
config?.props ?? {},
9392
SSR_MODE
9493
);

packages/@lwc/ssr-compiler/src/compile-js/generate-markup.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import { parse as pathParse } from 'node:path';
9+
import { BlockStatement as EsBlockStatement } from 'estree';
910
import { is, builders as b } from 'estree-toolkit';
1011
import { esTemplate } from '../estemplate';
1112
import { isIdentOrRenderCall } from '../estree/validators';
@@ -51,6 +52,12 @@ const bGenerateMarkup = esTemplate`
5152
}
5253
`<ExportNamedDeclaration>;
5354

55+
const bAssignGenerateMarkupToComponentClass = esTemplate`
56+
{
57+
${/* lwcClassName */ is.identifier}[__SYMBOL__GENERATE_MARKUP] = generateMarkup;
58+
}
59+
`<EsBlockStatement>;
60+
5461
/**
5562
* This builds a generator function `generateMarkup` and adds it to the component JS's
5663
* compilation output. `generateMarkup` acts as the glue between component JS and its
@@ -96,3 +103,18 @@ export function addGenerateMarkupExport(
96103
program.body.unshift(bImportDeclaration(['hasScopedStaticStylesheets']));
97104
program.body.push(bGenerateMarkup(classIdentifier, renderCall));
98105
}
106+
107+
/**
108+
* Attach the `generateMarkup` function to the Component class so that it can be found later
109+
* during `renderComponent`.
110+
*/
111+
export function assignGenerateMarkupToComponent(program: Program, state: ComponentMetaState) {
112+
program.body.unshift(
113+
bImportDeclaration([
114+
{
115+
SYMBOL__GENERATE_MARKUP: '__SYMBOL__GENERATE_MARKUP',
116+
},
117+
])
118+
);
119+
program.body.push(bAssignGenerateMarkupToComponentClass(b.identifier(state.lwcClassName!)));
120+
}

packages/@lwc/ssr-compiler/src/compile-js/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { transmogrify } from '../transmogrify';
1313
import { replaceLwcImport } from './lwc-import';
1414
import { catalogTmplImport } from './catalog-tmpls';
1515
import { catalogStaticStylesheets, catalogAndReplaceStyleImports } from './stylesheets';
16-
import { addGenerateMarkupExport } from './generate-markup';
16+
import { addGenerateMarkupExport, assignGenerateMarkupToComponent } from './generate-markup';
1717

1818
import type { Identifier as EsIdentifier, Program as EsProgram } from 'estree';
1919
import type { Visitors, ComponentMetaState } from './types';
@@ -140,6 +140,7 @@ export default function compileJS(src: string, filename: string, compilationMode
140140
}
141141

142142
addGenerateMarkupExport(ast, state, filename);
143+
assignGenerateMarkupToComponent(ast, state);
143144

144145
if (compilationMode === 'async' || compilationMode === 'sync') {
145146
ast = transmogrify(ast, compilationMode);
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import path from 'node:path';
2+
import fs from 'node:fs/promises';
3+
import { rollup } from 'rollup';
4+
import lwcRollupPlugin from '@lwc/rollup-plugin';
5+
import { renderComponent } from '../index';
6+
7+
interface ComponentModule {
8+
default: any;
9+
generateMarkup: any;
10+
}
11+
12+
async function compileComponent({
13+
input,
14+
files,
15+
}: {
16+
input: string;
17+
files: { [name: string]: string };
18+
}) {
19+
const dirname = path.resolve(__dirname, 'dist/render-component');
20+
const modulesDir = path.resolve(dirname, './src');
21+
const outputFile = path.resolve(dirname, './dist/index.js');
22+
23+
for (const [name, content] of Object.entries(files)) {
24+
const filename = path.join(modulesDir, name);
25+
await fs.mkdir(path.dirname(filename), { recursive: true });
26+
await fs.writeFile(filename, content, 'utf-8');
27+
}
28+
29+
const bundle = await rollup({
30+
input: path.resolve(modulesDir, input),
31+
external: ['lwc', '@lwc/ssr-runtime'],
32+
plugins: [
33+
lwcRollupPlugin({
34+
targetSSR: true,
35+
modules: [{ dir: modulesDir }],
36+
}),
37+
],
38+
});
39+
40+
await bundle.write({
41+
file: outputFile,
42+
format: 'esm',
43+
exports: 'named',
44+
});
45+
46+
return outputFile;
47+
}
48+
49+
describe('renderComponent', () => {
50+
let module;
51+
52+
beforeAll(async () => {
53+
const files = {
54+
'x/component/component.js': `
55+
import { LightningElement } from 'lwc';
56+
export default class extends LightningElement {}
57+
`,
58+
'x/component/component.html': `
59+
<template><h1>Hello world</h1></template>
60+
`,
61+
};
62+
const outputFile = await compileComponent({
63+
input: 'x/component/component.js',
64+
files,
65+
});
66+
67+
module = (await import(outputFile)) as ComponentModule;
68+
});
69+
70+
// TODO [#4726]: remove `generateMarkup` export
71+
test('can call `renderComponent()` on `generateMarkup`', async () => {
72+
const result = await renderComponent('x-component', module!.generateMarkup, {});
73+
74+
expect(result).toContain('<h1>Hello world</h1>');
75+
});
76+
77+
test('can call `renderComponent()` on the default export', async () => {
78+
const result = await renderComponent('x-component', module!.default, {});
79+
80+
expect(result).toContain('<h1>Hello world</h1>');
81+
});
82+
83+
test('does not throw if props are not provided', async () => {
84+
const result = await renderComponent('x-component', module!.default);
85+
86+
expect(result).toContain('<h1>Hello world</h1>');
87+
});
88+
89+
test('throws if tagName is not provided', async () => {
90+
await expect(() => renderComponent(undefined as any, module!.default, {})).rejects.toThrow(
91+
'tagName must be a string, found: undefined'
92+
);
93+
});
94+
});

packages/@lwc/ssr-runtime/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export {
1010
LightningElement,
1111
LightningElementConstructor,
1212
SYMBOL__SET_INTERNALS,
13+
SYMBOL__GENERATE_MARKUP,
1314
} from './lightning-element';
1415
export { mutationTracker } from './mutation-tracker';
1516
// renderComponent is an alias for serverSideRenderComponent

packages/@lwc/ssr-runtime/src/lightning-element.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ interface PropsAvailableAtConstruction {
4040
}
4141

4242
export const SYMBOL__SET_INTERNALS = Symbol('set-internals');
43+
export const SYMBOL__GENERATE_MARKUP = Symbol('generate-markup');
4344

4445
export class LightningElement implements PropsAvailableAtConstruction {
4546
static renderMode?: 'light' | 'shadow';

packages/@lwc/ssr-runtime/src/render.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
*/
77

88
import { mutationTracker } from './mutation-tracker';
9-
import type { LightningElement, LightningElementConstructor } from './lightning-element';
9+
import {
10+
LightningElement,
11+
LightningElementConstructor,
12+
SYMBOL__GENERATE_MARKUP,
13+
} from './lightning-element';
1014
import type { Attributes, Properties } from './types';
1115

1216
const escapeAttrVal = (attrVal: string) =>
@@ -99,19 +103,31 @@ type GenerateMarkupFnVariants =
99103
| GenerateMarkupFnAsyncNoGen
100104
| GenerateMarkupFnSyncNoGen;
101105

106+
interface ComponentWithGenerateMarkup {
107+
[SYMBOL__GENERATE_MARKUP]: GenerateMarkupFnVariants;
108+
}
109+
102110
export async function serverSideRenderComponent(
103111
tagName: string,
104-
compiledGenerateMarkup: GenerateMarkupFnVariants,
105-
props: Properties,
112+
Component: GenerateMarkupFnVariants | ComponentWithGenerateMarkup,
113+
props: Properties = {},
106114
mode: 'asyncYield' | 'async' | 'sync' = 'asyncYield'
107115
): Promise<string> {
116+
if (typeof tagName !== 'string') {
117+
throw new Error(`tagName must be a string, found: ${tagName}`);
118+
}
119+
120+
// TODO [#4726]: remove `generateMarkup` export
121+
const generateMarkup =
122+
SYMBOL__GENERATE_MARKUP in Component ? Component[SYMBOL__GENERATE_MARKUP] : Component;
123+
108124
let markup = '';
109125
const emit = (segment: string) => {
110126
markup += segment;
111127
};
112128

113129
if (mode === 'asyncYield') {
114-
for await (const segment of (compiledGenerateMarkup as GenerateMarkupFn)(
130+
for await (const segment of (generateMarkup as GenerateMarkupFn)(
115131
tagName,
116132
props,
117133
null,
@@ -120,15 +136,9 @@ export async function serverSideRenderComponent(
120136
markup += segment;
121137
}
122138
} else if (mode === 'async') {
123-
await (compiledGenerateMarkup as GenerateMarkupFnAsyncNoGen)(
124-
emit,
125-
tagName,
126-
props,
127-
null,
128-
null
129-
);
139+
await (generateMarkup as GenerateMarkupFnAsyncNoGen)(emit, tagName, props, null, null);
130140
} else if (mode === 'sync') {
131-
(compiledGenerateMarkup as GenerateMarkupFnSyncNoGen)(emit, tagName, props, null, null);
141+
(generateMarkup as GenerateMarkupFnSyncNoGen)(emit, tagName, props, null, null);
132142
} else {
133143
throw new Error(`Invalid mode: ${mode}`);
134144
}

0 commit comments

Comments
 (0)