diff --git a/packages/waku/src/cli.ts b/packages/waku/src/cli.ts index 79febe71a..bed77bd55 100644 --- a/packages/waku/src/cli.ts +++ b/packages/waku/src/cli.ts @@ -11,6 +11,7 @@ import * as dotenv from 'dotenv'; import type { Config } from './config.js'; import { runner } from './lib/hono/runner.js'; import { build } from './lib/builder/build.js'; +import { DIST_ENTRIES_JS, DIST_PUBLIC } from './lib/builder/constants.js'; const require = createRequire(new URL('.', import.meta.url)); @@ -120,19 +121,15 @@ async function runBuild() { }); } -async function runStart({ - distDir = 'dist', - entriesJs = 'entries.js', - publicDir = 'public', -}) { +async function runStart({ distDir = 'dist' }) { const loadEntries = () => - import(pathToFileURL(path.resolve(distDir, entriesJs)).toString()); + import(pathToFileURL(path.resolve(distDir, DIST_ENTRIES_JS)).toString()); const app = new Hono(); - app.use('*', serveStatic({ root: path.join(distDir, publicDir) })); + app.use('*', serveStatic({ root: path.join(distDir, DIST_PUBLIC) })); app.use('*', runner({ cmd: 'start', loadEntries, env: process.env as any })); app.notFound((c) => { // FIXME better implementation using node stream? - const file = path.join(distDir, publicDir, '404.html'); + const file = path.join(distDir, DIST_PUBLIC, '404.html'); if (existsSync(file)) { return c.html(readFileSync(file, 'utf8'), 404); } diff --git a/packages/waku/src/config.ts b/packages/waku/src/config.ts index 91ec0ffad..cf932c3b9 100644 --- a/packages/waku/src/config.ts +++ b/packages/waku/src/config.ts @@ -10,7 +10,6 @@ export interface Config { basePath?: string; /** * The source directory relative to root. - * This will be the actual root in the development mode. * Defaults to "src". */ srcDir?: string; @@ -21,35 +20,6 @@ export interface Config { */ distDir?: string; /** - * The public directory relative to distDir. - * It's different from Vite's build.publicDir config. - * Defaults to "public". - */ - publicDir?: string; - /** - * The assets directory relative to distDir and publicDir. - * Defaults to "assets". - */ - assetsDir?: string; - /** - * The SSR directory relative to distDir. - * Defaults to "ssr". - */ - ssrDir?: string; - /** - * The client main file relative to srcDir. - * The extension should be `.js`, - * but resolved with other extensions in the development mode. - * Defaults to "main.js". - */ - mainJs?: string; - /** - * The entries.js file relative to srcDir or distDir. - * The extension should be `.js`, - * but resolved with other extensions in the development mode. - * Defaults to "entries.js". - */ - entriesJs?: string; /** * The list of directries to preserve server module structure. * Relative to srcDir. @@ -62,12 +32,6 @@ export interface Config { * Defaults to "private". */ privateDir?: string; - /** - * The serve.js file relative distDir. - * This file is used for deployment. - * Defaults to "serve.js". - */ - serveJs?: string; /** * Prefix for HTTP requests to indicate RSC requests. * Defaults to "RSC". diff --git a/packages/waku/src/lib/builder/build.ts b/packages/waku/src/lib/builder/build.ts index 35630f1b7..0e5585958 100644 --- a/packages/waku/src/lib/builder/build.ts +++ b/packages/waku/src/lib/builder/build.ts @@ -54,6 +54,13 @@ import { emitNetlifyOutput } from './output-netlify.js'; import { emitCloudflareOutput } from './output-cloudflare.js'; import { emitPartyKitOutput } from './output-partykit.js'; import { emitAwsLambdaOutput } from './output-aws-lambda.js'; +import { + DIST_ENTRIES_JS, + DIST_SERVE_JS, + DIST_PUBLIC, + DIST_ASSETS, + DIST_SSR, +} from './constants.js'; // TODO this file and functions in it are too long. will fix. @@ -75,11 +82,7 @@ const onwarn = (warning: RollupLog, defaultHandler: LoggingFunction) => { defaultHandler(warning); }; -const analyzeEntries = async ( - rootDir: string, - config: ResolvedConfig, - entriesFile: string, -) => { +const analyzeEntries = async (rootDir: string, config: ResolvedConfig) => { const wakuClientDist = decodeFilePathFromAbsolute( joinPath(fileURLToFilePath(import.meta.url), '../../../client.js'), ); @@ -106,7 +109,7 @@ const analyzeEntries = async ( await buildVite({ plugins: [ rscAnalyzePlugin(clientFileSet, serverFileSet, fileHashMap), - rscManagedPlugin(config), + rscManagedPlugin({ ...config, addEntriesToInput: true }), ], ssr: { target: 'webworker', @@ -122,22 +125,19 @@ const analyzeEntries = async ( target: 'node18', rollupOptions: { onwarn, - input: { - ...Object.fromEntries(moduleFileMap), - entries: entriesFile, - }, + input: Object.fromEntries(moduleFileMap), }, }, }); const clientEntryFiles = Object.fromEntries( Array.from(clientFileSet).map((fname, i) => [ - `${config.assetsDir}/rsc${i}-${fileHashMap.get(fname)}`, + `${DIST_ASSETS}/rsc${i}-${fileHashMap.get(fname)}`, fname, ]), ); const serverEntryFiles = Object.fromEntries( Array.from(serverFileSet).map((fname, i) => [ - `${config.assetsDir}/rsf${i}`, + `${DIST_ASSETS}/rsf${i}`, fname, ]), ); @@ -153,7 +153,6 @@ const analyzeEntries = async ( const buildServerBundle = async ( rootDir: string, config: ResolvedConfig, - entriesFile: string, clientEntryFiles: Record, serverEntryFiles: Record, serverModuleFiles: Record, @@ -177,9 +176,12 @@ const buildServerBundle = async ( }), rscEnvPlugin({ config }), rscPrivatePlugin(config), - rscManagedPlugin(config), + rscManagedPlugin({ + ...config, + addEntriesToInput: true, + }), rscEntriesPlugin({ - entriesFile, + srcDir: config.srcDir, moduleMap: { ...Object.fromEntries( Object.keys(SERVER_MODULE_MAP).map((key) => [key, `./${key}.js`]), @@ -187,13 +189,13 @@ const buildServerBundle = async ( ...Object.fromEntries( Object.keys(CLIENT_MODULE_MAP).map((key) => [ `${CLIENT_PREFIX}${key}`, - `./${config.ssrDir}/${key}.js`, + `./${DIST_SSR}/${key}.js`, ]), ), ...Object.fromEntries( Object.keys(clientEntryFiles || {}).map((key) => [ - `${config.ssrDir}/${key}.js`, - `./${config.ssrDir}/${key}.js`, + `${DIST_SSR}/${key}.js`, + `./${DIST_SSR}/${key}.js`, ]), ), ...Object.fromEntries( @@ -208,7 +210,8 @@ const buildServerBundle = async ( ? [ rscServePlugin({ ...config, - entriesFile, + distServeJs: DIST_SERVE_JS, + distPublic: DIST_PUBLIC, srcServeFile: decodeFilePathFromAbsolute( joinPath( fileURLToFilePath(import.meta.url), @@ -251,7 +254,6 @@ const buildServerBundle = async ( rollupOptions: { onwarn, input: { - entries: entriesFile, ...SERVER_MODULE_MAP, ...serverModuleFiles, ...clientEntryFiles, @@ -270,7 +272,6 @@ const buildServerBundle = async ( const buildSsrBundle = async ( rootDir: string, config: ResolvedConfig, - mainJsFile: string, clientEntryFiles: Record, serverBuildOutput: Awaited>, isNodeCompatible: boolean, @@ -284,11 +285,10 @@ const buildSsrBundle = async ( rscIndexPlugin({ ...config, cssAssets, - mainJs: mainJsFile.split('/').pop()!, }), rscEnvPlugin({ config }), rscPrivatePlugin(config), - rscManagedPlugin(config), + rscManagedPlugin({ ...config, addMainToInput: true }), ], ssr: isNodeCompatible ? { @@ -312,11 +312,10 @@ const buildSsrBundle = async ( build: { ssr: true, target: 'node18', - outDir: joinPath(rootDir, config.distDir, config.ssrDir), + outDir: joinPath(rootDir, config.distDir, DIST_SSR), rollupOptions: { onwarn, input: { - main: mainJsFile, ...clientEntryFiles, ...CLIENT_MODULE_MAP, }, @@ -330,7 +329,7 @@ const buildSsrBundle = async ( ) { return '[name].js'; } - return config.assetsDir + '/[name]-[hash].js'; + return DIST_ASSETS + '/[name]-[hash].js'; }, }, }, @@ -342,7 +341,6 @@ const buildSsrBundle = async ( const buildClientBundle = async ( rootDir: string, config: ResolvedConfig, - mainJsFile: string, clientEntryFiles: Record, serverBuildOutput: Awaited>, ) => { @@ -357,28 +355,24 @@ const buildClientBundle = async ( rscIndexPlugin({ ...config, cssAssets, - mainJs: mainJsFile.split('/').pop()!, }), rscEnvPlugin({ config }), rscPrivatePlugin(config), - rscManagedPlugin(config), + rscManagedPlugin({ ...config, addMainToInput: true }), ], build: { - outDir: joinPath(rootDir, config.distDir, config.publicDir), + outDir: joinPath(rootDir, config.distDir, DIST_PUBLIC), rollupOptions: { onwarn, - input: { - main: mainJsFile, - // rollup will ouput the style files related to clientEntryFiles, but since it does not find any link to them in the index.html file, it will not inject them. They are only mentioned by the standalone `clientEntryFiles` - ...clientEntryFiles, - }, + // rollup will ouput the style files related to clientEntryFiles, but since it does not find any link to them in the index.html file, it will not inject them. They are only mentioned by the standalone `clientEntryFiles` + input: clientEntryFiles, preserveEntrySignatures: 'exports-only', output: { entryFileNames: (chunkInfo) => { if (clientEntryFiles[chunkInfo.name]) { return '[name].js'; } - return config.assetsDir + '/[name]-[hash].js'; + return DIST_ASSETS + '/[name]-[hash].js'; }, }, }, @@ -389,7 +383,7 @@ const buildClientBundle = async ( } for (const nonJsAsset of nonJsAssets) { const from = joinPath(rootDir, config.distDir, nonJsAsset); - const to = joinPath(rootDir, config.distDir, config.publicDir, nonJsAsset); + const to = joinPath(rootDir, config.distDir, DIST_PUBLIC, nonJsAsset); await rename(from, to); } return clientBuildOutput; @@ -428,7 +422,7 @@ const emitRscFiles = async ( const destRscFile = joinPath( rootDir, config.distDir, - config.publicDir, + DIST_PUBLIC, config.rscPath, encodeInput(input), ); @@ -489,7 +483,7 @@ const emitHtmlFiles = async ( const publicIndexHtmlFile = joinPath( rootDir, config.distDir, - config.publicDir, + DIST_PUBLIC, 'index.html', ); const publicIndexHtml = await readFile(publicIndexHtmlFile, { @@ -548,7 +542,7 @@ const emitHtmlFiles = async ( const destHtmlFile = joinPath( rootDir, config.distDir, - config.publicDir, + DIST_PUBLIC, extname(pathname) ? pathname : pathname === '/404' @@ -609,16 +603,6 @@ export const publicIndexHtml = ${JSON.stringify(publicIndexHtml)}; await appendFile(distEntriesFile, code); }; -const resolveFileName = (fname: string) => { - for (const ext of EXTENSIONS) { - const resolvedName = fname.slice(0, -extname(fname).length) + ext; - if (existsSync(resolvedName)) { - return resolvedName; - } - } - return fname; // returning the default one -}; - export async function build(options: { config: Config; env?: Record; @@ -638,26 +622,17 @@ export async function build(options: { const rootDir = ( await resolveViteConfig({}, 'build', 'production', 'production') ).root; - const entriesFile = resolveFileName( - joinPath(rootDir, config.srcDir, config.entriesJs), - ); - const distEntriesFile = resolveFileName( - joinPath(rootDir, config.distDir, config.entriesJs), - ); - const mainJsFile = resolveFileName( - joinPath(rootDir, config.srcDir, config.mainJs), - ); + const distEntriesFile = joinPath(rootDir, config.distDir, DIST_ENTRIES_JS); const isNodeCompatible = options.deploy !== 'cloudflare' && options.deploy !== 'partykit' && options.deploy !== 'deno'; const { clientEntryFiles, serverEntryFiles, serverModuleFiles } = - await analyzeEntries(rootDir, config, entriesFile); + await analyzeEntries(rootDir, config); const serverBuildOutput = await buildServerBundle( rootDir, config, - entriesFile, clientEntryFiles, serverEntryFiles, serverModuleFiles, @@ -672,7 +647,6 @@ export async function build(options: { await buildSsrBundle( rootDir, config, - mainJsFile, clientEntryFiles, serverBuildOutput, isNodeCompatible, @@ -680,7 +654,6 @@ export async function build(options: { const clientBuildOutput = await buildClientBundle( rootDir, config, - mainJsFile, clientEntryFiles, serverBuildOutput, ); @@ -713,18 +686,20 @@ export async function build(options: { await emitVercelOutput( rootDir, config, + DIST_SERVE_JS, options.deploy.slice('vercel-'.length) as 'static' | 'serverless', ); } else if (options.deploy?.startsWith('netlify-')) { await emitNetlifyOutput( rootDir, config, + DIST_SERVE_JS, options.deploy.slice('netlify-'.length) as 'static' | 'functions', ); } else if (options.deploy === 'cloudflare') { - await emitCloudflareOutput(rootDir, config); + await emitCloudflareOutput(rootDir, config, DIST_SERVE_JS); } else if (options.deploy === 'partykit') { - await emitPartyKitOutput(rootDir, config); + await emitPartyKitOutput(rootDir, config, DIST_SERVE_JS); } else if (options.deploy === 'aws-lambda') { await emitAwsLambdaOutput(config); } diff --git a/packages/waku/src/lib/builder/constants.ts b/packages/waku/src/lib/builder/constants.ts new file mode 100644 index 000000000..6edf165f9 --- /dev/null +++ b/packages/waku/src/lib/builder/constants.ts @@ -0,0 +1,7 @@ +// Some file and dir names for dist +// We may change this in the future +export const DIST_ENTRIES_JS = 'entries.js'; +export const DIST_SERVE_JS = 'serve.js'; +export const DIST_PUBLIC = 'public'; +export const DIST_ASSETS = 'assets'; +export const DIST_SSR = 'ssr'; diff --git a/packages/waku/src/lib/builder/output-cloudflare.ts b/packages/waku/src/lib/builder/output-cloudflare.ts index d6546299e..b27dafc5d 100644 --- a/packages/waku/src/lib/builder/output-cloudflare.ts +++ b/packages/waku/src/lib/builder/output-cloudflare.ts @@ -2,11 +2,13 @@ import path from 'node:path'; import { existsSync, writeFileSync } from 'node:fs'; import type { ResolvedConfig } from '../config.js'; +import { DIST_PUBLIC } from './constants.js'; // XXX this can be very limited. FIXME if anyone has better knowledge. export const emitCloudflareOutput = async ( rootDir: string, config: ResolvedConfig, + serveJs: string, ) => { const wranglerTomlFile = path.join(rootDir, 'wrangler.toml'); if (!existsSync(wranglerTomlFile)) { @@ -14,12 +16,12 @@ export const emitCloudflareOutput = async ( wranglerTomlFile, ` name = "waku-project" -main = "${config.distDir}/${config.serveJs}" +main = "${config.distDir}/${serveJs}" compatibility_date = "2023-12-06" compatibility_flags = [ "nodejs_als" ] [site] -bucket = "./${config.distDir}/${config.publicDir}" +bucket = "./${config.distDir}/${DIST_PUBLIC}" `, ); } diff --git a/packages/waku/src/lib/builder/output-netlify.ts b/packages/waku/src/lib/builder/output-netlify.ts index bdb5fb1ee..c37e210cc 100644 --- a/packages/waku/src/lib/builder/output-netlify.ts +++ b/packages/waku/src/lib/builder/output-netlify.ts @@ -2,10 +2,12 @@ import path from 'node:path'; import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs'; import type { ResolvedConfig } from '../config.js'; +import { DIST_PUBLIC } from './constants.js'; export const emitNetlifyOutput = async ( rootDir: string, config: ResolvedConfig, + serveJs: string, type: 'static' | 'functions', ) => { if (type === 'functions') { @@ -16,7 +18,7 @@ export const emitNetlifyOutput = async ( const notFoundFile = path.join( rootDir, config.distDir, - config.publicDir, + DIST_PUBLIC, '404.html', ); const notFoundHtml = existsSync(notFoundFile) @@ -26,7 +28,7 @@ export const emitNetlifyOutput = async ( path.join(functionsDir, 'serve.js'), ` globalThis.__WAKU_NOT_FOUND_HTML__ = ${JSON.stringify(notFoundHtml)}; -export { default } from '../../${config.distDir}/${config.serveJs}'; +export { default } from '../../${config.distDir}/${serveJs}'; export const config = { preferStatic: true, path: ['/', '/*'], @@ -41,7 +43,7 @@ export const config = { ` [build] command = "npm run build -- --with-netlify" - publish = "${config.distDir}/${config.publicDir}" + publish = "${config.distDir}/${DIST_PUBLIC}" [functions] included_files = ["${config.privateDir}/**"] `, diff --git a/packages/waku/src/lib/builder/output-partykit.ts b/packages/waku/src/lib/builder/output-partykit.ts index fb41247da..8a93b4573 100644 --- a/packages/waku/src/lib/builder/output-partykit.ts +++ b/packages/waku/src/lib/builder/output-partykit.ts @@ -2,11 +2,13 @@ import path from 'node:path'; import { existsSync, writeFileSync } from 'node:fs'; import type { ResolvedConfig } from '../config.js'; +import { DIST_PUBLIC } from './constants.js'; // XXX this can be very limited. FIXME if anyone has better knowledge. export const emitPartyKitOutput = async ( rootDir: string, config: ResolvedConfig, + serveJs: string, ) => { const partykitJsonFile = path.join(rootDir, 'partykit.json'); if (!existsSync(partykitJsonFile)) { @@ -15,9 +17,9 @@ export const emitPartyKitOutput = async ( JSON.stringify( { name: 'waku-project', - main: `${config.distDir}/${config.serveJs}`, + main: `${config.distDir}/${serveJs}`, compatibilityDate: '2023-02-16', - serve: `./${config.distDir}/${config.publicDir}`, + serve: `./${config.distDir}/${DIST_PUBLIC}`, }, null, 2, diff --git a/packages/waku/src/lib/builder/output-vercel.ts b/packages/waku/src/lib/builder/output-vercel.ts index d7e98299f..eafdcc217 100644 --- a/packages/waku/src/lib/builder/output-vercel.ts +++ b/packages/waku/src/lib/builder/output-vercel.ts @@ -2,14 +2,16 @@ import path from 'node:path'; import { cpSync, existsSync, mkdirSync, writeFileSync } from 'node:fs'; import type { ResolvedConfig } from '../config.js'; +import { DIST_PUBLIC } from './constants.js'; // https://vercel.com/docs/build-output-api/v3 export const emitVercelOutput = async ( rootDir: string, config: ResolvedConfig, + serveJs: string, type: 'static' | 'serverless', ) => { - const publicDir = path.join(rootDir, config.distDir, config.publicDir); + const publicDir = path.join(rootDir, config.distDir, DIST_PUBLIC); const outputDir = path.resolve('.vercel', 'output'); cpSync(publicDir, path.join(outputDir, 'static'), { recursive: true }); @@ -37,7 +39,7 @@ export const emitVercelOutput = async ( } const vcConfigJson = { runtime: 'nodejs20.x', - handler: `${config.distDir}/${config.serveJs}`, + handler: `${config.distDir}/${serveJs}`, launcherType: 'Nodejs', }; writeFileSync( diff --git a/packages/waku/src/lib/config.ts b/packages/waku/src/lib/config.ts index 8b4b42297..66137b52f 100644 --- a/packages/waku/src/lib/config.ts +++ b/packages/waku/src/lib/config.ts @@ -32,14 +32,8 @@ export async function resolveConfig(config: Config) { basePath: '/', srcDir: 'src', distDir: 'dist', - publicDir: 'public', - assetsDir: 'assets', - ssrDir: 'ssr', - mainJs: 'main.js', - entriesJs: 'entries.js', preserveModuleDirs: ['pages', 'templates', 'routes', 'components'], privateDir: 'private', - serveJs: 'serve.js', rscPath: 'RSC', htmlAttrs: '', htmlHead: DEFAULT_HTML_HEAD, diff --git a/packages/waku/src/lib/middleware/dev-server.ts b/packages/waku/src/lib/middleware/dev-server.ts index da35a54eb..71480847e 100644 --- a/packages/waku/src/lib/middleware/dev-server.ts +++ b/packages/waku/src/lib/middleware/dev-server.ts @@ -16,7 +16,12 @@ 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 { + // HACK depending on these constants is not ideal + SRC_ENTRIES, + SRC_MAIN, + rscManagedPlugin, +} from '../plugins/vite-plugin-rsc-managed.js'; import { mergeUserViteConfig } from '../utils/merge-vite-config.js'; import type { ClonableModuleNode, Middleware } from './types.js'; @@ -86,7 +91,7 @@ export const devServer: Middleware = (options) => { include: ['react-server-dom-webpack/client', 'react-dom'], exclude: ['waku'], entries: [ - `${config.srcDir}/${config.entriesJs}`.replace(/\.js$/, '.*'), + `${config.srcDir}/${SRC_ENTRIES}.*`, // HACK hard-coded "pages" `${config.srcDir}/pages/**/*.*`, ], @@ -170,7 +175,7 @@ export const devServer: Middleware = (options) => { if (!initialModules) { // pre-process the mainJs file to see which modules are being sent to the browser by vite // and using the same modules if possible in the bundlerConfig in the stream - const mainJs = `${config.basePath}${config.srcDir}/${config.mainJs}`; + const mainJs = `${config.basePath}${config.srcDir}/${SRC_MAIN}`; await vite.transformRequest(mainJs); const resolved = await vite.pluginContainer.resolveId(mainJs); const resolvedModule = vite.moduleGraph.idToModuleMap.get(resolved!.id)!; diff --git a/packages/waku/src/lib/plugins/vite-plugin-rsc-entries.ts b/packages/waku/src/lib/plugins/vite-plugin-rsc-entries.ts index d6b2bbc40..38c538733 100644 --- a/packages/waku/src/lib/plugins/vite-plugin-rsc-entries.ts +++ b/packages/waku/src/lib/plugins/vite-plugin-rsc-entries.ts @@ -3,7 +3,11 @@ import path from 'node:path'; import { normalizePath } from 'vite'; import type { Plugin } from 'vite'; -import { extname } from '../utils/path.js'; +// HACK Depending on a different plugin isn't ideal. +// Maybe we could put in vite config object? +import { SRC_ENTRIES } from './vite-plugin-rsc-managed.js'; + +import { extname, joinPath } from '../utils/path.js'; const stripExt = (fname: string) => { const ext = extname(fname); @@ -13,7 +17,7 @@ const stripExt = (fname: string) => { const CONFIG_FILE = 'waku.config.ts'; // XXX only ts extension export function rscEntriesPlugin(opts: { - entriesFile: string; + srcDir: string; moduleMap: Record; }): Plugin { const codeToPrepend = ` @@ -31,20 +35,24 @@ export function loadModule(id) { } } `; - if (existsSync(CONFIG_FILE)) { - const file = normalizePath( - path.relative(path.dirname(opts.entriesFile), path.resolve(CONFIG_FILE)), - ); - codeToAppend += ` + let entriesFile = ''; + return { + name: 'rsc-entries-plugin', + configResolved(config) { + entriesFile = joinPath(config.root, opts.srcDir, SRC_ENTRIES); + if (existsSync(CONFIG_FILE)) { + const file = normalizePath( + path.relative(path.dirname(entriesFile), path.resolve(CONFIG_FILE)), + ); + codeToAppend += ` export const loadConfig = async () => (await import('${file}')).default; `; - } else { - codeToAppend += ` + } else { + codeToAppend += ` export const loadConfig = async () => ({}); `; - } - return { - name: 'rsc-entries-plugin', + } + }, transform(code, id) { if ( // FIXME this is too hacky and not the right place to patch @@ -52,7 +60,7 @@ export const loadConfig = async () => ({}); ) { return codeToPrepend + code; } - if (stripExt(id) === stripExt(opts.entriesFile)) { + if (stripExt(id) === entriesFile) { return code + codeToAppend; } }, diff --git a/packages/waku/src/lib/plugins/vite-plugin-rsc-index.ts b/packages/waku/src/lib/plugins/vite-plugin-rsc-index.ts index 33fdac627..8ab0169e7 100644 --- a/packages/waku/src/lib/plugins/vite-plugin-rsc-index.ts +++ b/packages/waku/src/lib/plugins/vite-plugin-rsc-index.ts @@ -1,11 +1,14 @@ import type { Plugin } from 'vite'; +// HACK Depending on a different plugin isn't ideal. +// Maybe we could put in vite config object? +import { SRC_MAIN } from './vite-plugin-rsc-managed.js'; + import { codeToInject } from '../renderers/utils.js'; export function rscIndexPlugin(opts: { basePath: string; srcDir: string; - mainJs: string; htmlAttrs: string; htmlHead: string; cssAssets?: string[]; @@ -18,7 +21,7 @@ export function rscIndexPlugin(opts: { ${opts.htmlHead} - + `; @@ -27,7 +30,7 @@ ${opts.htmlHead} config() { return { optimizeDeps: { - entries: [`${opts.srcDir}/${opts.mainJs}`.replace(/\.js$/, '.*')], + entries: [`${opts.srcDir}/${SRC_MAIN}.*`], }, }; }, diff --git a/packages/waku/src/lib/plugins/vite-plugin-rsc-managed.ts b/packages/waku/src/lib/plugins/vite-plugin-rsc-managed.ts index 00033089c..7ed78cd93 100644 --- a/packages/waku/src/lib/plugins/vite-plugin-rsc-managed.ts +++ b/packages/waku/src/lib/plugins/vite-plugin-rsc-managed.ts @@ -1,7 +1,26 @@ import type { Plugin } from 'vite'; import { EXTENSIONS } from '../config.js'; -import { joinPath } from '../utils/path.js'; +import { extname, joinPath } from '../utils/path.js'; + +export const SRC_MAIN = 'main'; +export const SRC_ENTRIES = 'entries'; + +const stripExt = (fname: string) => { + const ext = extname(fname); + return ext ? fname.slice(0, -ext.length) : fname; +}; + +const getManagedEntries = () => ` +import { fsRouter } from 'waku/router/server'; + +export default fsRouter( + import.meta.url, + (file) => import.meta.glob('./pages/**/*.{${EXTENSIONS.map((ext) => + ext.replace(/^\./, ''), + ).join(',')}}')[\`./pages/\${file}\`]?.(), +); +`; const getManagedMain = () => ` import { Component, StrictMode } from 'react'; @@ -21,65 +40,63 @@ if (document.body.dataset.hydrate) { } `; -const getManagedEntries = () => ` -import { fsRouter } from 'waku/router/server'; - -export default fsRouter( - import.meta.url, - (file) => import.meta.glob('./pages/**/*.{${EXTENSIONS.map((ext) => - ext.replace(/^\./, ''), - ).join(',')}}')[\`./pages/\${file}\`]?.(), -); -`; - -const addSuffixX = (fname: string | undefined) => { - if (!fname) { - return fname; - } - if (fname.endsWith('x')) { - return fname; - } - return fname + 'x'; -}; - export function rscManagedPlugin(opts: { + basePath: string; srcDir: string; - entriesJs: string; - mainJs?: string; + addEntriesToInput?: boolean; + addMainToInput?: boolean; }): Plugin { let entriesFile: string | undefined; let mainFile: string | undefined; - const mainJsPath = opts.mainJs && '/' + joinPath(opts.srcDir, opts.mainJs); + const mainPath = `${opts.basePath}${opts.srcDir}/${SRC_MAIN}`; 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); + entriesFile = joinPath(config.root, opts.srcDir, SRC_ENTRIES); + mainFile = joinPath(config.root, opts.srcDir, SRC_MAIN); + }, + options(options) { + if (typeof options.input === 'string') { + throw new Error('string input is unsupported'); } + if (Array.isArray(options.input)) { + throw new Error('array input is unsupported'); + } + return { + ...options, + input: { + ...(opts.addEntriesToInput && { entries: entriesFile! }), + ...(opts.addMainToInput && { main: mainFile! }), + ...options.input, + }, + }; }, async resolveId(id, importer, options) { const resolved = await this.resolve(id, importer, options); - if (!resolved && id === entriesFile) { + if ((!resolved || resolved.id === id) && id === entriesFile) { managedEntries = true; - return addSuffixX(id); + return entriesFile + '.jsx'; + } + if ((!resolved || resolved.id === id) && id === mainFile) { + managedMain = true; + return mainFile + '.jsx'; } - if (!resolved && (id === mainFile || id === mainJsPath)) { + if ((!resolved || resolved.id === id) && stripExt(id) === mainPath) { managedMain = true; - return addSuffixX(id); + return mainPath + '.jsx'; } return resolved; }, load(id) { - if (managedEntries && id === addSuffixX(entriesFile)) { + if (managedEntries && id === entriesFile + '.jsx') { return getManagedEntries(); } if ( managedMain && - (id === addSuffixX(mainFile) || id === addSuffixX(mainJsPath)) + (id === mainFile + '.jsx' || id === mainPath + '.jsx') ) { return getManagedMain(); } diff --git a/packages/waku/src/lib/plugins/vite-plugin-rsc-serve.ts b/packages/waku/src/lib/plugins/vite-plugin-rsc-serve.ts index 360244d7e..5f4b50055 100644 --- a/packages/waku/src/lib/plugins/vite-plugin-rsc-serve.ts +++ b/packages/waku/src/lib/plugins/vite-plugin-rsc-serve.ts @@ -1,10 +1,29 @@ +import { existsSync } from 'node:fs'; +import path from 'node:path'; import type { Plugin } from 'vite'; +// HACK: Depending on a different plugin isn't ideal. +// Maybe we could put in vite config object? +import { SRC_ENTRIES } from './vite-plugin-rsc-managed.js'; + +import { EXTENSIONS } from '../config.js'; +import { extname } from '../utils/path.js'; + +const resolveFileName = (fname: string) => { + for (const ext of EXTENSIONS) { + const resolvedName = fname.slice(0, -extname(fname).length) + ext; + if (existsSync(resolvedName)) { + return resolvedName; + } + } + return fname; // returning the default one +}; + export function rscServePlugin(opts: { - serveJs: string; + srcDir: string; + distServeJs: string; distDir: string; - publicDir: string; - entriesFile: string; + distPublic: string; srcServeFile: string; serve: | 'vercel' @@ -17,16 +36,20 @@ export function rscServePlugin(opts: { return { name: 'rsc-serve-plugin', config(viteConfig) { + // FIXME This seems too hacky (The use of viteConfig.root, '.', path.resolve and resolveFileName) + const entriesFile = resolveFileName( + path.resolve(viteConfig.root || '.', opts.srcDir, SRC_ENTRIES + '.js'), + ); const { input } = viteConfig.build?.rollupOptions ?? {}; if (input && !(typeof input === 'string') && !(input instanceof Array)) { - input[opts.serveJs.replace(/\.js$/, '')] = opts.srcServeFile; + input[opts.distServeJs.replace(/\.js$/, '')] = opts.srcServeFile; } viteConfig.define = { ...viteConfig.define, - 'import.meta.env.WAKU_ENTRIES_FILE': JSON.stringify(opts.entriesFile), + 'import.meta.env.WAKU_ENTRIES_FILE': JSON.stringify(entriesFile), 'import.meta.env.WAKU_CONFIG_DIST_DIR': JSON.stringify(opts.distDir), 'import.meta.env.WAKU_CONFIG_PUBLIC_DIR': JSON.stringify( - opts.publicDir, + opts.distPublic, ), }; if (opts.serve === 'cloudflare' || opts.serve === 'partykit') { diff --git a/packages/waku/src/lib/renderers/dev-worker-api.ts b/packages/waku/src/lib/renderers/dev-worker-api.ts index b8d4652d3..e61d0c664 100644 --- a/packages/waku/src/lib/renderers/dev-worker-api.ts +++ b/packages/waku/src/lib/renderers/dev-worker-api.ts @@ -8,6 +8,9 @@ import type { HotUpdatePayload } from '../plugins/vite-plugin-rsc-hmr.js'; import type { RenderRscArgs, GetSsrConfigArgs } from './rsc-renderer.js'; import type { ClonableModuleNode } from '../middleware/types.js'; +// HACK depending on these constants is not ideal +import { SRC_ENTRIES } from '../plugins/vite-plugin-rsc-managed.js'; + export type BuildOutput = { rscFiles: string[]; htmlFiles: string[]; @@ -74,8 +77,9 @@ export function initializeWorker(config: ResolvedConfig) { '__WAKU_PRIVATE_ENV__', (globalThis as any).__WAKU_PRIVATE_ENV__, ); + setEnvironmentData('CONFIG_BASE_PATH', config.basePath); setEnvironmentData('CONFIG_SRC_DIR', config.srcDir); - setEnvironmentData('CONFIG_ENTRIES_JS', config.entriesJs); + setEnvironmentData('CONFIG_ENTRIES', SRC_ENTRIES); setEnvironmentData('CONFIG_PRIVATE_DIR', config.privateDir); const worker = new Worker( new URL('dev-worker-impl.js', import.meta.url), diff --git a/packages/waku/src/lib/renderers/dev-worker-impl.ts b/packages/waku/src/lib/renderers/dev-worker-impl.ts index e56d55ca2..291f867e6 100644 --- a/packages/waku/src/lib/renderers/dev-worker-impl.ts +++ b/packages/waku/src/lib/renderers/dev-worker-impl.ts @@ -40,8 +40,9 @@ if (HAS_MODULE_REGISTER) { (globalThis as any).__WAKU_PRIVATE_ENV__ = getEnvironmentData( '__WAKU_PRIVATE_ENV__', ); +const configBasePath = getEnvironmentData('CONFIG_BASE_PATH') as string; const configSrcDir = getEnvironmentData('CONFIG_SRC_DIR') as string; -const configEntriesJs = getEnvironmentData('CONFIG_ENTRIES_JS') as string; +const configEntries = getEnvironmentData('CONFIG_ENTRIES') as string; const configPrivateDir = getEnvironmentData('CONFIG_PRIVATE_DIR') as string; const resolveClientEntryForDev = ( @@ -182,7 +183,7 @@ const mergedViteConfig = await mergeUserViteConfig({ nonjsResolvePlugin(), rscEnvPlugin({}), rscPrivatePlugin({ privateDir: configPrivateDir, hotUpdateCallback }), - rscManagedPlugin({ srcDir: configSrcDir, entriesJs: configEntriesJs }), + rscManagedPlugin({ basePath: configBasePath, srcDir: configSrcDir }), rscTransformPlugin({ isBuild: false }), rscDelegatePlugin(hotUpdateCallback), ], @@ -190,7 +191,7 @@ const mergedViteConfig = await mergeUserViteConfig({ include: ['react-server-dom-webpack/client', 'react-dom'], exclude: ['waku'], entries: [ - `${configSrcDir}/${configEntriesJs}`.replace(/\.js$/, '.*'), + `${configSrcDir}/${configEntries}.*`, // HACK hard-coded "pages" `${configSrcDir}/pages/**/*.*`, ], @@ -231,14 +232,14 @@ const loadServerModule = async (id: string) => { return vite.ssrLoadModule(id); }; -const loadEntries = async (config: { srcDir: string; entriesJs: string }) => { +const loadEntries = async (config: { srcDir: string }) => { const vite = await vitePromise; - const filePath = joinPath(vite.config.root, config.srcDir, config.entriesJs); + const filePath = joinPath(vite.config.root, config.srcDir, configEntries); return vite.ssrLoadModule(filePath) as Promise; }; // load entries eagerly -loadEntries({ srcDir: configSrcDir, entriesJs: configEntriesJs }).catch(() => { +loadEntries({ srcDir: configSrcDir }).catch(() => { // ignore }); diff --git a/packages/waku/src/lib/renderers/html-renderer.ts b/packages/waku/src/lib/renderers/html-renderer.ts index b1da9b810..87f13dc2e 100644 --- a/packages/waku/src/lib/renderers/html-renderer.ts +++ b/packages/waku/src/lib/renderers/html-renderer.ts @@ -21,6 +21,10 @@ import { } from '../utils/path.js'; import { encodeInput, hasStatusCode } from './utils.js'; +// HACK depending on these constants is not ideal +import { SRC_MAIN } from '../plugins/vite-plugin-rsc-managed.js'; +import { DIST_SSR } from '../builder/constants.js'; + export const CLIENT_MODULE_MAP = { react: 'react', 'rd-server': 'react-dom/server.edge', @@ -319,11 +323,9 @@ export const renderHtml = async ( if (!moduleLoading.has(id)) { moduleLoading.set( id, - opts - .loadModule(joinPath(config.ssrDir, id)) - .then((m: any) => { - moduleCache.set(id, m); - }), + opts.loadModule(joinPath(DIST_SSR, id)).then((m: any) => { + moduleCache.set(id, m); + }), ); } return { id, chunks: [id], name }; @@ -368,7 +370,7 @@ export const renderHtml = async ( .pipeThrough( injectScript( config.basePath + config.rscPath + '/' + encodeInput(ssrConfig.input), - isDev ? `${config.basePath}${config.srcDir}/${config.mainJs}` : '', + isDev ? `${config.basePath}${config.srcDir}/${SRC_MAIN}` : '', ), ) .pipeThrough(injectRSCPayload(stream2));