diff --git a/packages/react/README.md b/packages/react/README.md index 27adb03e..af2e16b8 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -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) | diff --git a/packages/react/src/react-output-target/create-component-wrappers.test.ts b/packages/react/src/react-output-target/create-component-wrappers.test.ts index 24ad1dc8..070fb74f 100644 --- a/packages/react/src/react-output-target/create-component-wrappers.test.ts +++ b/packages/react/src/react-output-target/create-component-wrappers.test.ts @@ -261,39 +261,71 @@ export const MyComponent: StencilReactComponent; + const code = sourceFile.getFullText(); + expect(code).toContain('createComponent({'); + expect(code).toContain('createSSRComponent({'); + }); -export const MyComponent: StencilReactComponent = typeof window !== 'undefined' - ? /*@__PURE__*/ createComponent({ - 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({ - 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({'); + expect(code).not.toContain('createSSRComponent({'); + expect(code).toContain('createComponent({'); + expect(code).toContain('createSSRComponent({'); }); }); diff --git a/packages/react/src/react-output-target/create-component-wrappers.ts b/packages/react/src/react-output-target/create-component-wrappers.ts index 983a164f..b3366404 100644 --- a/packages/react/src/react-output-target/create-component-wrappers.ts +++ b/packages/react/src/react-output-target/create-component-wrappers.ts @@ -14,6 +14,7 @@ export const createComponentWrappers = async ({ excludeComponents, project, hydrateModule, + excludeServerSideRenderingFor, }: { stencilPackageName: string; components: ComponentCompilerMeta[]; @@ -23,6 +24,7 @@ export const createComponentWrappers = async ({ excludeComponents?: string[]; project: Project; hydrateModule?: string; + excludeServerSideRenderingFor?: string[]; }) => { const sourceFiles: SourceFile[] = []; @@ -62,6 +64,7 @@ export const createComponentWrappers = async ({ customElementsDir, defaultExport: true, hydrateModule, + excludeServerSideRenderingFor, }); fileContents[outputPath] = stencilReactComponent; } @@ -84,6 +87,7 @@ export const createComponentWrappers = async ({ customElementsDir, defaultExport: false, hydrateModule, + excludeServerSideRenderingFor, }); fileContents[outputPath] = stencilReactComponent; } diff --git a/packages/react/src/react-output-target/create-stencil-react-components.ts b/packages/react/src/react-output-target/create-stencil-react-components.ts index 5d633806..3b51ddd2 100644 --- a/packages/react/src/react-output-target/create-stencil-react-components.ts +++ b/packages/react/src/react-output-target/create-stencil-react-components.ts @@ -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 @@ -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, }, ], }); diff --git a/packages/react/src/react-output-target/react-output-target.ts b/packages/react/src/react-output-target/react-output-target.ts index 6a5eef59..e1bd0050 100644 --- a/packages/react/src/react-output-target/react-output-target.ts +++ b/packages/react/src/react-output-target/react-output-target.ts @@ -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'; @@ -60,6 +64,7 @@ export const reactOutputTarget = ({ excludeComponents, customElementsDir: customElementsDirOverride, hydrateModule, + excludeServerSideRenderingFor, }: ReactOutputTargetOptions): ReactOutputTarget => { let customElementsDir = DIST_CUSTOM_ELEMENTS_DEFAULT_DIR; return { @@ -152,6 +157,7 @@ export const reactOutputTarget = ({ excludeComponents, project, hydrateModule, + excludeServerSideRenderingFor, }); await Promise.all(