Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[labs/nextjs, labs/ssr-react, lit/react] Add support for Next.js v14 and App Router #4575

Merged
merged 10 commits into from
Apr 24, 2024
9 changes: 9 additions & 0 deletions .changeset/quiet-shirts-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@lit-labs/nextjs': minor
---

Add support for Next.js 14 and App Router. No longer supports Next.js 12.
augustjk marked this conversation as resolved.
Show resolved Hide resolved

Note: Components in the App Router are by default React Server Components. Deep SSR of Lit components does **not** work within server components as they result in React hydration mismatch due to the presence of the `<template>` element in React Flight serialized server component data, and the custom element definitions will not be included with the client bundle either.
augustjk marked this conversation as resolved.
Show resolved Hide resolved
augustjk marked this conversation as resolved.
Show resolved Hide resolved

Make sure any Lit components you wish to use are beyond the `'use client';` boundary. These will still be server rendered for the initial page load just like they did for the Pages Router.
8 changes: 3 additions & 5 deletions packages/labs/nextjs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,11 @@ The following options are supported:

## Considerations

The plugin has been tested with Next.js versions 12 and 13. It currently does not support usage with the beta `app` directory introduced with version 13. Follow this [issue](https://github.com/lit/lit/issues/3657) for updates on supporting this feature.
The plugin has been tested with Next.js versions 13 and 14.

The plugin may not work properly if you are providing a custom webpack configuration that modifies `config.externals`. Please file an issue if you find a particular configuration is not working.
If you are using Next.js App Router, you must make sure any Lit components you wish to use are beyond the `'use client';` boundary. These will still be server rendered for the initial page load just like they did for the Pages Router.

The server rendered output contains HTML with declarative shadow DOM which may require a polyfill for some browsers. See [Enabling Declarative Shadow DOM from `@lit-labs/ssr-react`](../ssr-react/README.md#enabling-declarative-shadow-dom) for more information.

While running the dev server, modifying any module that contains a custom element registration can cause an error that can only be fixed by restarting the dev server. See [issue #3672](https://github.com/lit/lit/issues/3672).
Components in the App Router are by default React Server Components and deep SSR of Lit components does **not** work within server components as they result in React hydration mismatch due to the presence of the `<template>` element in React Flight serialized server component data, and the custom element definitions will not be included with the client bundle either.

## Contributing

Expand Down
2 changes: 1 addition & 1 deletion packages/labs/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"imports-loader": "^4.0.1"
},
"peerDependencies": {
"next": "12 || 13"
"next": "13 || 14"
},
"scripts": {
"build": "wireit",
Expand Down
71 changes: 10 additions & 61 deletions packages/labs/nextjs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ export = (pluginOptions: LitSsrPluginOptions = {}): NextConfig =>
(nextConfig: NextConfig = {}) => {
return Object.assign({}, nextConfig, {
webpack: (config, options) => {
const {isServer, nextRuntime, webpack} = options;
const {isServer} = options;

const {addDeclarativeShadowDomPolyfill = true} = pluginOptions;

// This adds a side-effectful import which monkey patches
// `React.createElement` in the server and imports
// `@lit-labs/ssr-client/lit-element-hydrate-support.js` in the client.
// `React.createElement` and Runtime JSX functions in the server and
// imports `@lit-labs/ssr-client/lit-element-hydrate-support.js` in the
// client.
const imports = ['side-effects @lit-labs/ssr-react/enable-lit-ssr.js'];

if (!isServer && addDeclarativeShadowDomPolyfill) {
Expand All @@ -38,73 +39,21 @@ export = (pluginOptions: LitSsrPluginOptions = {}): NextConfig =>
}

config.module.rules.unshift({
// Grab entry points for all pages
// TODO(augustjk) This may not work for the new "app" directory
// https://github.com/lit/lit/issues/3657
test: /\/pages\/.*\.(?:jsx?|tsx?)$/,
// Grab entry points for all pages.
// TODO(augustjk) It would nicer to inject only once in either
// `pages/_document.tsx`, `pages/_app.tsx`, or `app/layout.tsx` but
// they're not guaranteed to exist.
test: /\/pages\/.*\.(?:j|t)sx?$|\/app\/.*\.(?:j|t)sx?$/,
// Exclude Next's own distributed files as they're commonjs and won't
// play nice with `imports-loader`
// play nice with `imports-loader`.
exclude: /next\/dist\//,
loader: 'imports-loader',
options: {
imports,
},
});

if (isServer && nextRuntime === 'nodejs') {
// Next.js uses this externals setting to skip webpack bundling
// external code for server environments but we specifically want to
// catch react/jsx-runtime imports for external packages as well.
// This might change in a future update to Next.js.
augustjk marked this conversation as resolved.
Show resolved Hide resolved
// https://github.com/vercel/next.js/issues/46396
// This is what we're grabbing:
// https://github.com/vercel/next.js/blob/412dfc52cc5e378e4f74e44af698dad25031a938/packages/next/src/build/webpack-config.ts#L1378-L1429
const nextHandleExternals = config.externals[0];
config.externals = [
(opt: {request: string}) => {
if (
opt.request === 'react/jsx-dev-runtime' ||
opt.request === 'react/jsx-runtime'
) {
// Returning empty promise makes these requests go through webpack
return Promise.resolve();
}
return nextHandleExternals(opt);
},
];
}

config.plugins.push(
// Replace all module requests of the automatic JSX runtime sources to
// `@lit-lab/ssr-react`'s except the ones within it.
new webpack.NormalModuleReplacementPlugin(
/react/,
function (resource: {request: string; context: string}) {
if (
resource.request === 'react/jsx-runtime' &&
// Don't replace our own imports.
// Regex starts at "labs" instead of "lit-labs" to be able to
// match the path of the package in the Lit monorepo.
!/labs\/ssr-react/.test(resource.context)
) {
resource.request = '@lit-labs/ssr-react/jsx-runtime';
}
if (
resource.request === 'react/jsx-dev-runtime' &&
// Don't replace our own imports.
// Regex starts at "labs" instead of "lit-labs" to be able to
// match the path of the package in the Lit monorepo.
!/labs\/ssr-react/.test(resource.context)
) {
resource.request = '@lit-labs/ssr-react/jsx-dev-runtime';
}
}
)
);

// Apply user provided custom webpack config function if it exists.
// There's potential for conflict here if existing `webpack` config also
// overwrites externals.
if (typeof nextConfig.webpack === 'function') {
return nextConfig.webpack(config, options);
}
Expand Down