Skip to content

Commit

Permalink
feat(react): support to ignore certain components for SSR (#540)
Browse files Browse the repository at this point in the history
* feat(react): support to ignore certain components for SSR

* prettier

* revert module extname change
  • Loading branch information
christian-bromann authored Nov 7, 2024
1 parent c3dbb65 commit 528ba3a
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 34 deletions.
1 change: 1 addition & 0 deletions packages/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,4 @@ That's it! You can now import and use your Stencil components as React component
| `excludeComponents` | An array of component tag names to exclude from the React output target. Useful if you want to prevent certain web components from being in the React library. |
| `customElementsDir` | The directory where the custom elements are saved. This value is automatically detected from the Stencil configuration file for the `dist-custom-elements` output target. If you are working in an environment that uses absolute paths, consider setting this value manually. |
| `hydrateModule` | For server side rendering support, provide the package for importing the [Stencil Hydrate module](https://stenciljs.com/docs/hydrate-app#hydrate-app), e.g. `my-package/hydrate`. This will generate two files: a `component.server.ts` which defines all component wrappers and a `components.ts` that re-exports these components for importing in your application. |
| `excludeServerSideRenderingFor` | A list of components that won't be considered for Server Side Rendering (SSR) |
Original file line number Diff line number Diff line change
Expand Up @@ -261,39 +261,71 @@ export const MyComponent: StencilReactComponent<MyComponentElement, MyComponentE

const sourceFile = sourceFiles[0];

expect(sourceFile.getFullText()).toEqual(dedent`
/**
* This file was automatically generated by the Stencil React Output Target.
* Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
* Do __not__ import components from this file as server side rendered components
* may not hydrate due to missing Stencil runtime. Instead, import these components through the generated 'components.ts'
* file that re-exports all components with the 'use client' directive.
*/
/* eslint-disable */
import type { StencilReactComponent } from '@stencil/react-output-target/runtime';
import { createComponent, createSSRComponent } from '@stencil/react-output-target/runtime';
import { MyComponent as MyComponentElement, defineCustomElement as defineMyComponent } from "my-package/dist/custom-elements/my-component.js";
import React from 'react';
type MyComponentEvents = NonNullable<unknown>;
const code = sourceFile.getFullText();
expect(code).toContain('createComponent<MyComponentElement, MyComponentEvents>({');
expect(code).toContain('createSSRComponent<MyComponentElement, MyComponentEvents>({');
});

export const MyComponent: StencilReactComponent<MyComponentElement, MyComponentEvents> = typeof window !== 'undefined'
? /*@__PURE__*/ createComponent<MyComponentElement, MyComponentEvents>({
tagName: 'my-component',
elementClass: MyComponentElement,
// @ts-ignore - React type of Stencil Output Target may differ from the React version used in the Nuxt.js project, this can be ignored.
react: React,
events: {} as MyComponentEvents,
defineCustomElement: defineMyComponent
})
: /*@__PURE__*/ createSSRComponent<MyComponentElement, MyComponentEvents>({
tagName: 'my-component',
properties: { hasMaxLength: 'max-length' },
hydrateModule: import('my-package/hydrate')
it('can exclude components for server side rendering', async () => {
const project = new Project({ useInMemoryFileSystem: true });
const sourceFiles = await createComponentWrappers({
components: [
{
tagName: 'my-component-a',
componentClassName: 'MyComponentA',
properties: [
{
name: 'hasMaxLength',
attribute: 'max-length',
},
{
name: 'links',
},
],
events: [
{
originalName: 'my-event',
name: 'myEvent',
type: 'CustomEvent',
},
],
} as any,
{
tagName: 'my-component-b',
componentClassName: 'MyComponentB',
properties: [
{
name: 'hasMaxLength',
attribute: 'max-length',
},
{
name: 'links',
},
],
events: [
{
originalName: 'my-event',
name: 'myEvent',
type: 'CustomEvent',
},
],
} as any,
],
stencilPackageName: 'my-package',
customElementsDir: 'dist/custom-elements',
outDir: 'dist/my-output-path',
esModules: false,
hydrateModule: 'my-package/hydrate',
excludeServerSideRenderingFor: ['my-component-a'],
project,
});

`);
const sourceFile = sourceFiles[0];

const code = sourceFile.getFullText();
expect(code).toContain('createComponent<MyComponentAElement, MyComponentAEvents>({');
expect(code).not.toContain('createSSRComponent<MyComponentAElement, MyComponentAEvents>({');
expect(code).toContain('createComponent<MyComponentBElement, MyComponentBEvents>({');
expect(code).toContain('createSSRComponent<MyComponentBElement, MyComponentBEvents>({');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const createComponentWrappers = async ({
excludeComponents,
project,
hydrateModule,
excludeServerSideRenderingFor,
}: {
stencilPackageName: string;
components: ComponentCompilerMeta[];
Expand All @@ -23,6 +24,7 @@ export const createComponentWrappers = async ({
excludeComponents?: string[];
project: Project;
hydrateModule?: string;
excludeServerSideRenderingFor?: string[];
}) => {
const sourceFiles: SourceFile[] = [];

Expand Down Expand Up @@ -62,6 +64,7 @@ export const createComponentWrappers = async ({
customElementsDir,
defaultExport: true,
hydrateModule,
excludeServerSideRenderingFor,
});
fileContents[outputPath] = stencilReactComponent;
}
Expand All @@ -84,6 +87,7 @@ export const createComponentWrappers = async ({
customElementsDir,
defaultExport: false,
hydrateModule,
excludeServerSideRenderingFor,
});
fileContents[outputPath] = stencilReactComponent;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@ export const createStencilReactComponents = ({
customElementsDir,
defaultExport = false,
hydrateModule,
excludeServerSideRenderingFor,
}: {
components: ComponentCompilerMeta[];
stencilPackageName: string;
customElementsDir: string;
defaultExport?: boolean;
hydrateModule?: string;
excludeServerSideRenderingFor?: string[];
}) => {
const project = new Project({ useInMemoryFileSystem: true });
const excludeSSRComponents = excludeServerSideRenderingFor || [];

/**
* automatically attach the `use client` directive if we are not generating
Expand Down Expand Up @@ -170,11 +173,12 @@ import type { EventName, StencilReactComponent } from '@stencil/react-output-tar
{
name: reactTagName,
type: `StencilReactComponent<${componentElement}, ${componentEventNamesType}>`,
initializer: hydrateModule
? `typeof window !== 'undefined'
initializer:
hydrateModule && !excludeSSRComponents.includes(tagName)
? `typeof window !== 'undefined'
? ${clientComponentCall}
: ${serverComponentCall}`
: clientComponentCall,
: clientComponentCall,
},
],
});
Expand Down
6 changes: 6 additions & 0 deletions packages/react/src/react-output-target/react-output-target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export interface ReactOutputTargetOptions {
* By default this value is undefined and server side rendering is disabled.
*/
hydrateModule?: string;
/**
* Specify the components that should be excluded from server side rendering.
*/
excludeServerSideRenderingFor?: string[];
}

const PLUGIN_NAME = 'react-output-target';
Expand All @@ -60,6 +64,7 @@ export const reactOutputTarget = ({
excludeComponents,
customElementsDir: customElementsDirOverride,
hydrateModule,
excludeServerSideRenderingFor,
}: ReactOutputTargetOptions): ReactOutputTarget => {
let customElementsDir = DIST_CUSTOM_ELEMENTS_DEFAULT_DIR;
return {
Expand Down Expand Up @@ -152,6 +157,7 @@ export const reactOutputTarget = ({
excludeComponents,
project,
hydrateModule,
excludeServerSideRenderingFor,
});

await Promise.all(
Expand Down

0 comments on commit 528ba3a

Please sign in to comment.