Skip to content

Commit e26a7e6

Browse files
committed
support vite build.outDir for custom output directories
1 parent 75519c3 commit e26a7e6

File tree

17 files changed

+88
-55
lines changed

17 files changed

+88
-55
lines changed

packages/one/src/cli.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ const serveCommand = defineCommand({
180180
loadEnv: {
181181
type: 'boolean',
182182
},
183+
outDir: {
184+
type: 'string',
185+
description: 'Build output directory (default: dist)',
186+
},
183187
},
184188
async run({ args }) {
185189
const { serve } = await import('./serve')
@@ -190,6 +194,7 @@ const serveCommand = defineCommand({
190194
host,
191195
compress: args.compress,
192196
loadEnv: !!args.loadEnv,
197+
outDir: lastValue(args.outDir),
193198
})
194199
},
195200
})

packages/one/src/cli/build.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,17 @@ export async function build(args: {
7575
checkNodeVersion()
7676
setServerGlobals()
7777

78-
const { oneOptions } = await loadUserOneOptions('build')
78+
const { oneOptions, config: viteLoadedConfig } = await loadUserOneOptions('build')
7979
const routerRoot = getRouterRootFromOneOptions(oneOptions)
8080

8181
// Set defaultRenderMode env var so getManifest knows the correct route types
8282
if (oneOptions.web?.defaultRenderMode) {
8383
process.env.ONE_DEFAULT_RENDER_MODE = oneOptions.web.defaultRenderMode
8484
}
8585

86+
// respect vite's build.outDir config, default to 'dist'
87+
const outDir = viteLoadedConfig?.config?.build?.outDir ?? 'dist'
88+
8689
const manifest = getManifest({ routerRoot })!
8790

8891
const serverOutputFormat =
@@ -173,7 +176,7 @@ export async function build(args: {
173176
build: {
174177
ssr: true,
175178
emptyOutDir: false,
176-
outDir: `dist/${subFolder}`,
179+
outDir: `${outDir}/${subFolder}`,
177180
copyPublicDir: false,
178181
minify: false,
179182
rolldownOptions: {
@@ -269,7 +272,7 @@ export async function build(args: {
269272
const outChunks = middlewareBuildInfo.output.filter((x) => x.type === 'chunk')
270273
const chunk = outChunks.find((x) => x.facadeModuleId === fullPath)
271274
if (!chunk) throw new Error(`internal err finding middleware`)
272-
builtMiddlewares[middleware.file] = join('dist', 'middlewares', chunk.fileName)
275+
builtMiddlewares[middleware.file] = join(outDir, 'middlewares', chunk.fileName)
273276
}
274277
}
275278

@@ -301,8 +304,8 @@ export async function build(args: {
301304
: `concurrency: ${BUILD_CONCURRENCY}`
302305
console.info(`\n 🔨 build static routes (${modeLabel})\n`)
303306

304-
const staticDir = join(`dist/static`)
305-
const clientDir = join(`dist/client`)
307+
const staticDir = join(`${outDir}/static`)
308+
const clientDir = join(`${outDir}/client`)
306309
await ensureDir(staticDir)
307310

308311
if (!vxrnOutput.serverOutput) {
@@ -643,7 +646,7 @@ export async function build(args: {
643646
})
644647
}
645648

646-
const serverJsPath = join('dist/server', serverFileName)
649+
const serverJsPath = join(`${outDir}/server`, serverFileName)
647650

648651
let exported
649652
try {
@@ -866,6 +869,7 @@ export async function build(args: {
866869
}
867870

868871
const buildInfoForWriting: One.BuildInfo = {
872+
outDir,
869873
oneOptions,
870874
routeToBuildInfo,
871875
pathToRoute,
@@ -882,7 +886,7 @@ export async function build(args: {
882886
useRolldown: await isRolldown(),
883887
}
884888

885-
await writeJSON(toAbsolute(`dist/buildInfo.json`), buildInfoForWriting)
889+
await writeJSON(toAbsolute(`${outDir}/buildInfo.json`), buildInfoForWriting)
886890

887891
// emit version.json for skew protection polling
888892
await FSExtra.writeFile(
@@ -951,7 +955,7 @@ export async function build(args: {
951955
buildInfoForWriting.routeToBuildInfo
952956
)) {
953957
if (info.serverJsPath) {
954-
const importPath = './' + info.serverJsPath.replace(/^dist\//, '')
958+
const importPath = './' + info.serverJsPath.replace(new RegExp(`^${outDir}/`), '')
955959
pageRouteMap.push(` '${routeFile}': () => import('${importPath}')`)
956960
}
957961
}
@@ -971,11 +975,11 @@ export async function build(args: {
971975
// Generate lazy imports for middlewares
972976
// The key must match the contextKey used to look up the middleware (e.g., "dist/middlewares/_middleware.js")
973977
for (const [, builtPath] of Object.entries(builtMiddlewares)) {
974-
const importPath = './' + builtPath.replace(/^dist\//, '')
978+
const importPath = './' + builtPath.replace(new RegExp(`^${outDir}/`), '')
975979
middlewareRouteMap.push(` '${builtPath}': () => import('${importPath}')`)
976980
}
977981

978-
const workerSrcPath = join(options.root, 'dist', '_worker-src.js')
982+
const workerSrcPath = join(options.root, outDir, '_worker-src.js')
979983
const workerCode = `// Polyfill MessageChannel for React SSR (not available in Cloudflare Workers by default)
980984
if (typeof MessageChannel === 'undefined') {
981985
globalThis.MessageChannel = class MessageChannel {
@@ -1066,7 +1070,7 @@ export default {
10661070
mode: 'production',
10671071
logLevel: 'warn',
10681072
build: {
1069-
outDir: 'dist',
1073+
outDir,
10701074
emptyOutDir: false,
10711075
// Use SSR mode with node target for proper Node.js module resolution
10721076
ssr: workerSrcPath,
@@ -1143,12 +1147,12 @@ export default {
11431147
}
11441148
`
11451149
await FSExtra.writeFile(
1146-
join(options.root, 'dist', 'wrangler.jsonc'),
1150+
join(options.root, outDir, 'wrangler.jsonc'),
11471151
wranglerConfig
11481152
)
11491153

1150-
postBuildLogs.push(`Cloudflare worker bundled at dist/worker.js`)
1151-
postBuildLogs.push(`To deploy: cd dist && wrangler deploy`)
1154+
postBuildLogs.push(`Cloudflare worker bundled at ${outDir}/worker.js`)
1155+
postBuildLogs.push(`To deploy: cd ${outDir} && wrangler deploy`)
11521156

11531157
break
11541158
}

packages/one/src/cli/buildPage.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export async function buildPage(
6363

6464
const htmlPath = `${path.endsWith('/') ? `${removeTrailingSlash(path)}/index` : path}.html`
6565
const clientJsPath = clientManifestEntry
66-
? join(`dist/client`, clientManifestEntry.file)
66+
? join(clientDir, clientManifestEntry.file)
6767
: ''
6868
const htmlOutPath = toAbsolute(join(staticDir, htmlPath))
6969
const preloadPath = getPreloadPath(path)
@@ -190,8 +190,10 @@ prefetchCSS()
190190
if (!layoutServerPath) {
191191
return { contextKey: layout.contextKey, loaderData: undefined }
192192
}
193+
// derive server dir from clientDir (e.g. dist/client -> dist/server)
194+
const serverDir = join(clientDir, '..', 'server')
193195
const layoutExported = await import(
194-
toAbsolute(join('./', 'dist/server', layoutServerPath))
196+
toAbsolute(join(serverDir, layoutServerPath))
195197
)
196198
const layoutLoaderData = await layoutExported?.loader?.(loaderProps)
197199
return { contextKey: layout.contextKey, loaderData: layoutLoaderData }

packages/one/src/serve.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@ process.on('uncaughtException', (err) => {
1212
console.error(`[one] Uncaught exception`, err?.stack || err)
1313
})
1414

15-
export async function serve(args: VXRNOptions['server'] & { app?: Hono } = {}) {
16-
const buildInfo = (await FSExtra.readJSON(`dist/buildInfo.json`)) as One.BuildInfo
15+
export async function serve(args: VXRNOptions['server'] & { app?: Hono; outDir?: string } = {}) {
16+
// resolve outDir: explicit arg > buildInfo.json in cwd (serving from within outDir) > default 'dist'
17+
const outDir = args.outDir
18+
|| (FSExtra.existsSync('buildInfo.json') ? '.' : null)
19+
|| 'dist'
20+
const buildInfo = (await FSExtra.readJSON(`${outDir}/buildInfo.json`)) as One.BuildInfo
1721
const { oneOptions } = buildInfo
1822

1923
setServerGlobals()
@@ -33,6 +37,7 @@ export async function serve(args: VXRNOptions['server'] & { app?: Hono } = {}) {
3337
}
3438

3539
return await vxrnServe({
40+
outDir: buildInfo.outDir || outDir,
3641
app: args.app,
3742
// fallback to one plugin
3843
...oneOptions.server,

packages/one/src/server/oneServe.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ import { getFetchStaticHtml } from './staticHtmlFetcher'
2222

2323
const debugRouter = process.env.ONE_DEBUG_ROUTER
2424

25-
async function readStaticHtml(htmlPath: string): Promise<string | null> {
25+
async function readStaticHtml(htmlPath: string, outDir = 'dist'): Promise<string | null> {
2626
const fetchStaticHtml = getFetchStaticHtml()
2727
if (fetchStaticHtml) {
2828
const html = await fetchStaticHtml(htmlPath)
2929
if (html) return html
3030
}
3131
try {
32-
return await readFile(join('dist/client', htmlPath), 'utf-8')
32+
return await readFile(join(`${outDir}/client`, htmlPath), 'utf-8')
3333
} catch {
3434
return null
3535
}
@@ -55,6 +55,7 @@ export async function oneServe(
5555
lazyRoutes?: LazyRoutes
5656
}
5757
) {
58+
const outDir = buildInfo.outDir || 'dist'
5859
const { resolveAPIRoute, resolveLoaderRoute, resolvePageRoute } =
5960
await import('../createHandleRequest')
6061
const { isResponse } = await import('../utils/isResponse')
@@ -128,9 +129,9 @@ export async function oneServe(
128129

129130
try {
130131
const pathToResolve = serverPath || lazyKey || ''
131-
const resolvedPath = pathToResolve.includes('dist/server')
132+
const resolvedPath = pathToResolve.includes(`${outDir}/server`)
132133
? pathToResolve
133-
: join('./', 'dist/server', pathToResolve)
134+
: join('./', `${outDir}/server`, pathToResolve)
134135

135136
const routeExported = lazyKey
136137
? options?.lazyRoutes?.pages?.[lazyKey]
@@ -162,7 +163,7 @@ export async function oneServe(
162163
: await import(
163164
resolve(
164165
process.cwd(),
165-
`${serverOptions.root}/dist/server/_virtual_one-entry.${typeof oneOptions.build?.server === 'object' && oneOptions.build.server.outputFormat === 'cjs' ? 'c' : ''}js`
166+
`${serverOptions.root}/${outDir}/server/_virtual_one-entry.${typeof oneOptions.build?.server === 'object' && oneOptions.build.server.outputFormat === 'cjs' ? 'c' : ''}js`
166167
)
167168
)
168169
render = entry.default.render as (props: RenderAppProps) => any
@@ -180,7 +181,7 @@ export async function oneServe(
180181
const fileName = route.page.slice(1).replace(/\[/g, '_').replace(/\]/g, '_')
181182
const apiFile = join(
182183
process.cwd(),
183-
'dist',
184+
outDir,
184185
'api',
185186
fileName + (apiCJS ? '.cjs' : '.js')
186187
)
@@ -200,10 +201,10 @@ export async function oneServe(
200201
// For workers, look up by routeFile (original file path like "./dynamic/[id]+ssr.tsx")
201202
// For Node.js, use route.file which may be loaderServerPath (already includes dist/server)
202203
const routeFile = (route as any).routeFile || route.file
203-
// route.file may already include dist/server if it came from loaderServerPath
204-
const serverPath = route.file.includes('dist/server')
204+
// route.file may already include outDir/server if it came from loaderServerPath
205+
const serverPath = route.file.includes(`${outDir}/server`)
205206
? route.file
206-
: join('./', 'dist/server', route.file)
207+
: join('./', `${outDir}/server`, route.file)
207208

208209
let exports
209210
try {
@@ -309,7 +310,7 @@ export async function oneServe(
309310
if (nfHtml) {
310311
try {
311312
const html = await readFile(
312-
join(process.cwd(), 'dist/client', nfHtml),
313+
join(process.cwd(), `${outDir}/client`, nfHtml),
313314
'utf-8'
314315
)
315316
return new Response(html, {
@@ -467,7 +468,7 @@ url: ${url}`)
467468
: routeMap[url.pathname] || routeMap[buildInfo?.cleanPath]
468469

469470
if (htmlPath) {
470-
const html = await readStaticHtml(htmlPath)
471+
const html = await readStaticHtml(htmlPath, outDir)
471472

472473
if (html) {
473474
const headers = new Headers()
@@ -486,7 +487,7 @@ url: ${url}`)
486487
const notFoundHtmlPath = routeMap[notFoundRoute]
487488

488489
if (notFoundHtmlPath) {
489-
const notFoundHtml = await readStaticHtml(notFoundHtmlPath)
490+
const notFoundHtml = await readStaticHtml(notFoundHtmlPath, outDir)
490491

491492
if (notFoundHtml) {
492493
// inject 404 marker so client knows this is a 404 response

packages/one/src/vercel/build/buildVercelOutputDirectory.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@ async function moveAllFiles(src: string, dest: string) {
2222
}
2323

2424
function getMiddlewaresByNamedRegex(buildInfoForWriting: One.BuildInfo) {
25+
const outDir = buildInfoForWriting.outDir || 'dist'
26+
const prefix = `${outDir}/middlewares/`
2527
return buildInfoForWriting.manifest.allRoutes
2628
.filter((r) => r.middlewares && r.middlewares.length > 0)
2729
.map((r) => [
2830
r.namedRegex,
2931
r.middlewares!.map((m) =>
30-
m.contextKey.startsWith('dist/middlewares/')
31-
? m.contextKey.substring('dist/middlewares/'.length)
32+
m.contextKey.startsWith(prefix)
33+
? m.contextKey.substring(prefix.length)
3234
: m.contextKey
3335
),
3436
])
@@ -48,6 +50,8 @@ export const buildVercelOutputDirectory = async ({
4850
oneOptionsRoot: string
4951
postBuildLogs: string[]
5052
}) => {
53+
const outDir = buildInfoForWriting.outDir || 'dist'
54+
5155
// clean the vercel output directory to avoid stale files from previous builds
5256
const vercelOutputDir = resolve(join(oneOptionsRoot, '.vercel/output'))
5357
if (existsSync(vercelOutputDir)) {
@@ -73,7 +77,8 @@ export const buildVercelOutputDirectory = async ({
7377
route,
7478
compiledRoute.code,
7579
oneOptionsRoot,
76-
postBuildLogs
80+
postBuildLogs,
81+
outDir
7782
)
7883
} else {
7984
console.warn(
@@ -112,7 +117,7 @@ export const buildVercelOutputDirectory = async ({
112117
}
113118
}
114119

115-
const distMiddlewareDir = resolve(join(oneOptionsRoot, 'dist', 'middlewares'))
120+
const distMiddlewareDir = resolve(join(oneOptionsRoot, outDir, 'middlewares'))
116121
if (existsSync(distMiddlewareDir)) {
117122
const vercelMiddlewareDir = resolve(
118123
join(oneOptionsRoot, '.vercel/output/functions/_middleware.func')
@@ -122,7 +127,7 @@ export const buildVercelOutputDirectory = async ({
122127
`[one.build][vercel] copying middlewares from ${distMiddlewareDir} to ${vercelMiddlewareDir}`
123128
)
124129
await moveAllFiles(
125-
resolve(join(oneOptionsRoot, 'dist', 'middlewares')),
130+
resolve(join(oneOptionsRoot, outDir, 'middlewares')),
126131
vercelMiddlewareDir
127132
)
128133
const vercelMiddlewarePackageJsonFilePath = resolve(

packages/one/src/vercel/build/generate/createApiServerlessFunction.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ export async function createApiServerlessFunction(
1818
route: RouteInfo<string>,
1919
code: string,
2020
oneOptionsRoot: string,
21-
postBuildLogs: string[]
21+
postBuildLogs: string[],
22+
outDir = 'dist'
2223
) {
2324
try {
2425
const path = getPathFromRoute(route, { includeIndex: true })
@@ -45,7 +46,7 @@ export async function createApiServerlessFunction(
4546
postBuildLogs.push(
4647
`[one.build][vercel.createSsrServerlessFunction] copy shared assets to ${distAssetsFolder}`
4748
)
48-
const sourceAssetsFolder = resolve(join(oneOptionsRoot, 'dist', 'api', 'assets'))
49+
const sourceAssetsFolder = resolve(join(oneOptionsRoot, outDir, 'api', 'assets'))
4950
if (await FSExtra.pathExists(sourceAssetsFolder)) {
5051
await fs.copy(sourceAssetsFolder, distAssetsFolder)
5152
}

packages/one/src/vercel/build/generate/createSsrServerlessFunction.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ export async function createSsrServerlessFunction(
2323
)
2424
await fs.ensureDir(funcFolder)
2525

26-
const distServerFrom = resolve(join(oneOptionsRoot, 'dist', 'server'))
26+
const outDir = buildInfo.outDir || 'dist'
27+
const distServerFrom = resolve(join(oneOptionsRoot, outDir, 'server'))
2728
const distServerTo = resolve(join(funcFolder, 'server'))
2829
await fs.ensureDir(distServerTo)
2930
postBuildLogs.push(
@@ -118,7 +119,7 @@ export async function createSsrServerlessFunction(
118119
route = buildInfoConfig.default.routeToBuildInfo[routeName];
119120
}
120121
121-
const exported = await import(route.serverJsPath.replace('dist/','../'))
122+
const exported = await import(route.serverJsPath.replace('${outDir}/','../'))
122123
const loaderData = await exported.loader?.(loaderProps)
123124
124125
// For loader requests, return the loader data as a JavaScript module

packages/one/src/vite/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,7 @@ export namespace One {
549549
}
550550

551551
export type BuildInfo = {
552+
outDir?: string
552553
constants: {
553554
CACHE_KEY: string
554555
}

0 commit comments

Comments
 (0)