diff --git a/.changeset/wild-boxes-chew.md b/.changeset/wild-boxes-chew.md new file mode 100644 index 0000000000..26cfe2dcf7 --- /dev/null +++ b/.changeset/wild-boxes-chew.md @@ -0,0 +1,7 @@ +--- +'@lit-labs/ssr-react': minor +--- + +Initial release of `@lit-labs/ssr-react` package. + +This package contains tools to deeply server render Lit components being used in React projects. diff --git a/.eslintignore b/.eslintignore index 246e5797b3..4094f4b8a4 100644 --- a/.eslintignore +++ b/.eslintignore @@ -277,6 +277,14 @@ packages/labs/ssr-client/index.* packages/labs/ssr-dom-shim/index.* +packages/labs/ssr-react/node/ +packages/labs/ssr-react/lib/ +packages/labs/ssr-react/test/ +packages/labs/ssr-react/index.* +packages/labs/ssr-react/jsx-runtime.* +packages/labs/ssr-react/jsx-dev-runtime.* +packages/labs/ssr-react/enable-lit-ssr.* + packages/labs/task/development/ packages/labs/task/test/ packages/labs/task/node_modules/ diff --git a/.prettierignore b/.prettierignore index ae79df652f..c07bc5f26e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -264,6 +264,14 @@ packages/labs/ssr-client/index.* packages/labs/ssr-dom-shim/index.* +packages/labs/ssr-react/node/ +packages/labs/ssr-react/lib/ +packages/labs/ssr-react/test/ +packages/labs/ssr-react/index.* +packages/labs/ssr-react/jsx-runtime.* +packages/labs/ssr-react/jsx-dev-runtime.* +packages/labs/ssr-react/enable-lit-ssr.* + packages/labs/task/development/ packages/labs/task/test/ packages/labs/task/node_modules/ diff --git a/package-lock.json b/package-lock.json index 6e11099091..488d847160 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2722,6 +2722,10 @@ "resolved": "packages/labs/ssr-dom-shim", "link": true }, + "node_modules/@lit-labs/ssr-react": { + "resolved": "packages/labs/ssr-react", + "link": true + }, "node_modules/@lit-labs/task": { "resolved": "packages/labs/task", "link": true @@ -24851,6 +24855,108 @@ "version": "1.0.0", "license": "BSD-3-Clause" }, + "packages/labs/ssr-react": { + "name": "@lit-labs/ssr-react", + "version": "0.0.0", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr": "^3.0.0", + "lit": "^2.6.1" + }, + "devDependencies": { + "@types/react": "^18.0.27", + "@types/react-dom": "^18.0.10", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "uvu": "^0.5.6" + }, + "peerDependencies": { + "@types/react": "17 || 18", + "react": "17 || 18" + } + }, + "packages/labs/ssr-react/node_modules/@types/react": { + "version": "18.0.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.27.tgz", + "integrity": "sha512-3vtRKHgVxu3Jp9t718R9BuzoD4NcQ8YJ5XRzsSKxNDiDonD2MXIT1TmSkenxuCycZJoQT5d2vE8LwWJxBC1gmA==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "packages/labs/ssr-react/node_modules/@types/react-dom": { + "version": "18.0.10", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz", + "integrity": "sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "packages/labs/ssr-react/node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dev": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "packages/labs/ssr-react/node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dev": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "packages/labs/ssr-react/node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dev": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "packages/labs/ssr/node_modules/entities": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", + "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "packages/labs/ssr/node_modules/node-fetch": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.0.tgz", + "integrity": "sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "packages/labs/ssr/node_modules/parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", @@ -27435,6 +27541,68 @@ "@lit-labs/ssr-dom-shim": { "version": "file:packages/labs/ssr-dom-shim" }, + "@lit-labs/ssr-react": { + "version": "file:packages/labs/ssr-react", + "requires": { + "@lit-labs/ssr": "^3.0.0", + "@types/react": "18", + "@types/react-dom": "18", + "lit": "^2.6.1", + "react": "18", + "react-dom": "18", + "uvu": "^0.5.6" + }, + "dependencies": { + "@types/react": { + "version": "18.0.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.27.tgz", + "integrity": "sha512-3vtRKHgVxu3Jp9t718R9BuzoD4NcQ8YJ5XRzsSKxNDiDonD2MXIT1TmSkenxuCycZJoQT5d2vE8LwWJxBC1gmA==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.0.10", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz", + "integrity": "sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + } + }, + "scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0" + } + } + } + }, "@lit-labs/task": { "version": "file:packages/labs/task", "requires": { diff --git a/package.json b/package.json index 61ff88e5c0..1ed64c23a7 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "./packages/labs/ssr:build", "./packages/labs/ssr-client:build", "./packages/labs/ssr-dom-shim:build", + "./packages/labs/ssr-react:build", "./packages/labs/task:build", "./packages/labs/testing:build", "./packages/labs/virtualizer:build", @@ -96,6 +97,7 @@ "./packages/labs/ssr:build:ts", "./packages/labs/ssr-client:build:ts", "./packages/labs/ssr-dom-shim:build:ts", + "./packages/labs/ssr-react:build:ts", "./packages/labs/task:build:ts", "./packages/labs/testing:build:ts", "./packages/labs/virtualizer:build:ts", @@ -140,6 +142,7 @@ "./packages/labs/gen-wrapper-react:test", "./packages/labs/gen-wrapper-vue:test", "./packages/labs/ssr:test", + "./packages/labs/ssr-react:test", "./packages/labs/testing:test", "./packages/labs/virtualizer:test" ] diff --git a/packages/labs/ssr-react/.gitignore b/packages/labs/ssr-react/.gitignore new file mode 100644 index 0000000000..9c1f5ce1a1 --- /dev/null +++ b/packages/labs/ssr-react/.gitignore @@ -0,0 +1,7 @@ +/node/ +/lib/ +/test/ +/index.* +/jsx-runtime.* +/jsx-dev-runtime.* +/enable-lit-ssr.* diff --git a/packages/labs/ssr-react/README.md b/packages/labs/ssr-react/README.md new file mode 100644 index 0000000000..68cad99c5c --- /dev/null +++ b/packages/labs/ssr-react/README.md @@ -0,0 +1,96 @@ +# @lit-labs/ssr-react + +A package for integrating Lit SSR with React and React frameworks. + +## Overview + +By default, React's SSR library renders custom elements _shallowly_, i.e. only the element's open and closing tags, attributes, and light DOM children are present in the server-rendered HTML - shadow DOM contents are not rendered. + +This package provides tools to integrate [`@lit-labs/ssr`](../ssr/README.md) with React SSR so that Lit components are deeply rendered, including their shadow DOM contents. + +## Usage + +To get React SSR to deeply render Lit components, we'll need React JSX code to call an enhanced version of `createElement()` provided by this package. The way to achieve this depends on your project configuration. + +### Using the Classic Runtime JSX Transform + +The classic JSX transform replaces JSX expressions with `React.createElement()` function calls. In the default mode, it requires that `React` is imported and available in the scope of the JSX file. + +This package provides a couple different ways to handle the classic runtime: + +#### Monkey patching `React.createElement()` (recommended) + +This package provides a module that, when imported in a server environment, has the side-effect of monkey patching `React.createElement()` to be an enhanced to add the declarative shadow DOM output to registered custom elements. This can be imported at the entry point of the application before `React` is imported. + +```js +// index.js +import '@lit-labs/ssr-react/enable-lit-ssr.js'; + +import React from 'react'; +import ReactDOM from 'react-dom'; + +... +``` + +In the browser environment, this module does not patch `React.createElement()` but instead imports `lit/experimental-hydrate-support.js` which must be imported before the `lit` package to allow hydration of server-rendered Lit elements. + +This approach has the advantage of being compatible with Lit components wrapped as React components using the `@lit-labs/react` package, which calls `React.createElement()` directly. It'll also work for any external React components pre-compiled with the classic JSX runtime transform. + +#### Specifying an alternative `createElement()` function + +If you wish to control which components use the enhanced `createElement()` function without a global monkey patch, you may do so by using a JSX pragma. + +```diff +- import React from 'react'; ++ /** @jsx createElement */ ++ import {createElement} from '@lit-labs/ssr-react'; + +const Component = (props) => { + return ; +} +``` + +You may also set the compiler options to specify the function to use instead of the JSX pragma. + +- For Babel: set the [`pragma`](https://babeljs.io/docs/en/babel-preset-react#pragma) option for `@babel/preset-react` to `"createElement"`. +- For TypeScript: set the [`jsxFactory`](https://www.typescriptlang.org/tsconfig#jsxFactory) option in `tsconfig.json` to `"createElement"`. + +Note that the import line must still be present for every file that contains JSX expressions to transform in the classic runtime mode. + +This approach only works for server-rendering custom elements added to the project in JSX expressions. It will not affect any pre-compiled JSX expressions or direct calls to `React.createElement()`. You will also need to manually import the `lit/experimental-hydrate-support.js` to your client JS. For those scenarios, use the [monkey patching](#monkey-patching-reactcreateelement-recommended) approach. + +### Using the Automatic Runtime JSX Transform + +If your project is using the [runtime JSX transform](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html), this package can serve as the JSX import source. + +- For Babel: set the [`importSource`](https://babeljs.io/docs/en/babel-preset-react#importsource) option in `@babel/preset-react` to `@lit-labs/ssr-react`. +- For TypeScript: set the [`jsxImportSource`](https://www.typescriptlang.org/tsconfig#jsxImportSource) option in `tsconfig.json` to `@lit-labs/ssr-react`. + +These JSX runtime modules contain jsx functions enhanced to add the declarative shadow DOM output to registered custom elements when imported into server environemtns. They also automatically import `lit/experimental-hydrate-support.js` in the browser environment. + +This method will not work for any pre-compiled JSX expressions or direct calls to `React.createElement()`, including those in the usage of the `@lit-labs/react` package's `createElement()`. Consider combining this with the [monkey patching](#monkey-patching-reactcreateelement-recommended) approach to handle such scenarios. + +In the unlikely event that you wish to use React components that are pre-compiled with the automatic transform, i.e. those already written using `jsx` or `jsxs` functions, that also contain Lit components you wish to SSR, a build tool will need to be used to replace the import source of those functions to be from `@lit-labs/ssr-react`. + +### Advanced Usage + +For composing multiple `createElement()` functions, e.g. for use along side other React libraries that enhancee `createElement()`, this package also provides a `wrapCreateElement()` function which accepts a `createElement()` function and returns an enhanced one. + +```js +import {wrapCreateElement} from '@lit-labs/ssr-react'; +import React from 'react'; + +const enhancedCreateElement = wrapCreateElement(React.createElement); +``` + +## How it Works + +The enhancements to `React.createElement()` or runtime JSX functions work by adding a `