diff --git a/packages/waku/src/lib/builder/build.ts b/packages/waku/src/lib/builder/build.ts index 10fc21efa..ec2a3245c 100644 --- a/packages/waku/src/lib/builder/build.ts +++ b/packages/waku/src/lib/builder/build.ts @@ -48,6 +48,7 @@ import { rscEntriesPlugin } from '../plugins/vite-plugin-rsc-entries.js'; import { rscServePlugin } from '../plugins/vite-plugin-rsc-serve.js'; import { rscEnvPlugin } from '../plugins/vite-plugin-rsc-env.js'; import { rscPrivatePlugin } from '../plugins/vite-plugin-rsc-private.js'; +import { rscManagedPlugin } from '../plugins/vite-plugin-rsc-managed.js'; import { emitVercelOutput } from './output-vercel.js'; import { emitNetlifyOutput } from './output-netlify.js'; import { emitCloudflareOutput } from './output-cloudflare.js'; @@ -103,7 +104,10 @@ const analyzeEntries = async ( } } await buildVite({ - plugins: [rscAnalyzePlugin(clientFileSet, serverFileSet, fileHashMap)], + plugins: [ + rscAnalyzePlugin(clientFileSet, serverFileSet, fileHashMap), + rscManagedPlugin(config), + ], ssr: { target: 'webworker', resolve: { @@ -172,6 +176,7 @@ const buildServerBundle = async ( }), rscEnvPlugin({ config }), rscPrivatePlugin(config), + rscManagedPlugin(config), rscEntriesPlugin({ entriesFile, moduleMap: { @@ -274,6 +279,7 @@ const buildSsrBundle = async ( rscIndexPlugin({ ...config, cssAssets }), rscEnvPlugin({ config }), rscPrivatePlugin(config), + rscManagedPlugin(config), ], ssr: isNodeCompatible ? { @@ -338,6 +344,7 @@ const buildClientBundle = async ( rscIndexPlugin({ ...config, cssAssets }), rscEnvPlugin({ config }), rscPrivatePlugin(config), + rscManagedPlugin(config), ], build: { outDir: joinPath(rootDir, config.distDir, config.publicDir), diff --git a/packages/waku/src/lib/middleware/dev-server.ts b/packages/waku/src/lib/middleware/dev-server.ts index e1319e2ae..dce5ff07d 100644 --- a/packages/waku/src/lib/middleware/dev-server.ts +++ b/packages/waku/src/lib/middleware/dev-server.ts @@ -20,6 +20,7 @@ import { rscIndexPlugin } from '../plugins/vite-plugin-rsc-index.js'; import { rscHmrPlugin, hotUpdate } from '../plugins/vite-plugin-rsc-hmr.js'; import { rscEnvPlugin } from '../plugins/vite-plugin-rsc-env.js'; import { rscPrivatePlugin } from '../plugins/vite-plugin-rsc-private.js'; +import { rscManagedPlugin } from '../plugins/vite-plugin-rsc-managed.js'; import { mergeUserViteConfig } from '../utils/merge-vite-config.js'; import type { Middleware } from './types.js'; @@ -78,6 +79,7 @@ export const devServer: Middleware = (options) => { patchReactRefresh(viteReact()), rscEnvPlugin({ config }), rscPrivatePlugin(config), + rscManagedPlugin(config), rscIndexPlugin(config), rscHmrPlugin(), { name: 'nonjs-resolve-plugin' }, // dummy to match with dev-worker-impl.ts diff --git a/packages/waku/src/lib/plugins/vite-plugin-rsc-managed.ts b/packages/waku/src/lib/plugins/vite-plugin-rsc-managed.ts new file mode 100644 index 000000000..34e9ab228 --- /dev/null +++ b/packages/waku/src/lib/plugins/vite-plugin-rsc-managed.ts @@ -0,0 +1,105 @@ +import type { Plugin } from 'vite'; + +import { joinPath } from '../utils/path.js'; + +const getManagedMain = () => ` +import { Component, StrictMode } from 'react'; +import { createRoot, hydrateRoot } from 'react-dom/client'; +import { Router } from 'waku/router/client'; + +class ErrorBoundary extends Component { + constructor(props) { + super(props); + this.state = {}; + } + static getDerivedStateFromError(error) { + return { error }; + } + render() { + if ('error' in this.state) { + return this.props.fallback(this.state.error); + } + return this.props.children; + } +} + +const rootElement = ( + +

{String(error)}

}> + +
+
+); + +if (document.body.dataset.hydrate) { + hydrateRoot(document.body, rootElement); +} else { + createRoot(document.body).render(rootElement); +} +`; + +const getManagedEntries = () => ` +import { fsRouter } from 'waku/router/server'; + +export default fsRouter(import.meta.url, loader); + +function loader(dir, file) { + const p = file.replace(/\\.\\w+$/, '').split('/'); + switch (p.length) { +${[...new Array(50).keys()] + .map( + (i) => + ' case ' + + (i + 1) + + ': ' + + 'return import(`./${dir}/' + + [...new Array(i + 1).keys()].map((j) => '${p[' + j + ']}').join('/') + + '.tsx`);', + ) + .join('\n')} + default: throw new Error('too deep'); + } +} +`; + +export function rscManagedPlugin(opts: { + srcDir: string; + entriesJs: string; + mainJs?: string; +}): Plugin { + let entriesFile: string | undefined; + let mainFile: string | undefined; + const mainJsPath = opts.mainJs && '/' + joinPath(opts.srcDir, opts.mainJs); + let managedEntries = false; + let managedMain = false; + return { + name: 'rsc-managed-plugin', + enforce: 'pre', + configResolved(config) { + entriesFile = joinPath(config.root, opts.srcDir, opts.entriesJs); + if (opts.mainJs) { + mainFile = joinPath(config.root, opts.srcDir, opts.mainJs); + } + }, + async resolveId(id, importer, options) { + const resolved = await this.resolve(id, importer, options); + if (!resolved && id === entriesFile) { + managedEntries = true; + return id; + } + if (!resolved && (id === mainFile || id === mainJsPath)) { + managedMain = true; + return id; + } + return resolved; + }, + load(id) { + if (managedEntries && id === entriesFile) { + return getManagedEntries(); + } + if (managedMain && (id === mainFile || id === mainJsPath)) { + return getManagedMain(); + } + }, + }; +} diff --git a/packages/waku/src/lib/renderers/dev-worker-impl.ts b/packages/waku/src/lib/renderers/dev-worker-impl.ts index 30f475792..9ce9dd8aa 100644 --- a/packages/waku/src/lib/renderers/dev-worker-impl.ts +++ b/packages/waku/src/lib/renderers/dev-worker-impl.ts @@ -20,6 +20,7 @@ import { nonjsResolvePlugin } from '../plugins/vite-plugin-nonjs-resolve.js'; import { rscTransformPlugin } from '../plugins/vite-plugin-rsc-transform.js'; import { rscEnvPlugin } from '../plugins/vite-plugin-rsc-env.js'; import { rscPrivatePlugin } from '../plugins/vite-plugin-rsc-private.js'; +import { rscManagedPlugin } from '../plugins/vite-plugin-rsc-managed.js'; import { rscDelegatePlugin } from '../plugins/vite-plugin-rsc-delegate.js'; import { mergeUserViteConfig } from '../utils/merge-vite-config.js'; import { viteHot } from '../plugins/vite-plugin-rsc-hmr.js'; @@ -132,6 +133,7 @@ const mergedViteConfig = await mergeUserViteConfig({ viteReact(), rscEnvPlugin({}), rscPrivatePlugin({ privateDir: configPrivateDir }), + rscManagedPlugin({ srcDir: configSrcDir, entriesJs: configEntriesJs }), { name: 'rsc-index-plugin' }, // dummy to match with handler-dev.ts { name: 'rsc-hmr-plugin', enforce: 'post' }, // dummy to match with handler-dev.ts nonjsResolvePlugin(),