Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions packages/one/src/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,22 @@ async function serveWithCluster(args: Parameters<typeof serve>[0], numWorkers: n
}

async function startWorker(args: Parameters<typeof serve>[0]) {
const outDir =
args?.outDir || (FSExtra.existsSync('buildInfo.json') ? '.' : null) || 'dist'
// Resolve outDir with the same precedence chain the `build` command uses
// (matching cli/build.ts:370 — `viteLoadedConfig?.config?.build?.outDir ?? 'dist'`):
// 1. --outDir CLI flag
// 2. cwd has buildInfo.json — preserves the "cd into output dir then run" UX
// 3. vite.config's build.outDir
// 4. 'dist' fallback
// Step (3) only runs when (1)-(2) miss; loading vite.config is non-trivial
// and not worth it for the common case where the cwd already has buildInfo.json.
let outDir = args?.outDir
if (!outDir && FSExtra.existsSync('buildInfo.json')) {
outDir = '.'
}
// DIAG #703-A: bypass loadViteBuildOutDir entirely to isolate whether this
// call (even with IS_VXRN_CLI + globalThis isolation) is what's breaking the
// spa-shell-routing CI test.
outDir = outDir || 'dist'
const buildInfo = (await FSExtra.readJSON(`${outDir}/buildInfo.json`)) as One.BuildInfo
const { oneOptions } = buildInfo

Expand Down
2 changes: 2 additions & 0 deletions packages/one/src/vite/__fixtures__/with-outdir/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { defineConfig } from 'vite'
export default defineConfig({ build: { outDir: 'build-out' } })
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { defineConfig } from 'vite'
export default defineConfig({})
37 changes: 37 additions & 0 deletions packages/one/src/vite/loadConfig.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { loadViteBuildOutDir } from './loadConfig'

const here = dirname(fileURLToPath(import.meta.url))
const fixture = (name: string) => join(here, '__fixtures__', name)

describe('loadViteBuildOutDir', () => {
it("returns vite.config's build.outDir when set", async () => {
expect(await loadViteBuildOutDir(fixture('with-outdir'))).toBe('build-out')
})

it('returns undefined when build.outDir is not set', async () => {
expect(await loadViteBuildOutDir(fixture('without-outdir'))).toBeUndefined()
})

// Regression guard for the side-effect bug that broke spa-shell-routing
// on CI: `loadConfigFromFile` runs the plugin chain, and `one()` has two
// branches keyed off `IS_VXRN_CLI`. Without the env-var + globalThis save/
// restore, the helper used to leak `__oneOptions` / `__vxrnPluginConfig__`
// into the next `vxrn/serve` startup. Verify the helper is a no-op on the
// ambient state.
it('restores process.env.IS_VXRN_CLI + __oneOptions globals after the call', async () => {
const previousEnv = process.env.IS_VXRN_CLI
const previousOneOptions = globalThis['__oneOptions']
const previousVxrnPluginConfig = globalThis['__vxrnPluginConfig__']
const previousVxrnMetroOptions = globalThis['__vxrnMetroOptions__']

await loadViteBuildOutDir(fixture('with-outdir'))

expect(process.env.IS_VXRN_CLI).toBe(previousEnv)
expect(globalThis['__oneOptions']).toBe(previousOneOptions)
expect(globalThis['__vxrnPluginConfig__']).toBe(previousVxrnPluginConfig)
expect(globalThis['__vxrnMetroOptions__']).toBe(previousVxrnMetroOptions)
})
})
53 changes: 53 additions & 0 deletions packages/one/src/vite/loadConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,59 @@ import { loadConfigFromFile } from 'vite'
import '../polyfills-server'
import type { One } from './types'

/// Read `build.outDir` from the user's `vite.config.ts` without instantiating
/// the full One plugin chain. Used by `serve.ts` to resolve where
/// `buildInfo.json` lives when `--outDir` and the cwd-`buildInfo.json` UX both
/// miss. Returns `undefined` when there's no vite.config or `build.outDir`
/// isn't set.
///
/// Why the `IS_VXRN_CLI` + `globalThis` isolation: `one()` (the plugin) has
/// two branches keyed off `process.env.IS_VXRN_CLI`. In the CLI branch (which
/// `one build` enters by setting that env var) the plugin just stashes user
/// options into globals and returns an empty plugin list. In the non-CLI
/// branch it pushes `vxrnVitePlugin` and runs the full Vite-native dev path —
/// `configResolved` hooks, file watchers, server middleware. `one serve` does
/// NOT set `IS_VXRN_CLI`, so a naïve `loadConfigFromFile` call inside `serve`
/// would instantiate the full plugin chain, leak watchers / globals, and break
/// the subsequent `vxrn/serve` startup (manifests as flaking SPA navigation
/// tests in CI). Mirror the isolation `loadUserOneOptions` already uses: set
/// the env var + clear stale globals before the call, restore in `finally`.
///
/// The optional `configRoot` lets tests target a fixture directory without
/// having to `chdir` the whole vitest worker. Defaults to `process.cwd()`,
/// matching the production call site in `serve.ts`.
export async function loadViteBuildOutDir(
configRoot?: string
): Promise<string | undefined> {
const previousIsVxrnCli = process.env.IS_VXRN_CLI
const previousOneOptions = globalThis['__oneOptions']
const previousVxrnPluginConfig = globalThis['__vxrnPluginConfig__']
const previousVxrnMetroOptions = globalThis['__vxrnMetroOptions__']

try {
process.env.IS_VXRN_CLI = 'true'
delete globalThis['__oneOptions']
delete globalThis['__vxrnPluginConfig__']
delete globalThis['__vxrnMetroOptions__']

const loaded = await loadConfigFromFile(
{ command: 'serve', mode: 'production' },
undefined,
configRoot
)
return loaded?.config?.build?.outDir as string | undefined
} finally {
if (previousIsVxrnCli === undefined) {
delete process.env.IS_VXRN_CLI
} else {
process.env.IS_VXRN_CLI = previousIsVxrnCli
}
globalThis['__oneOptions'] = previousOneOptions
globalThis['__vxrnPluginConfig__'] = previousVxrnPluginConfig
globalThis['__vxrnMetroOptions__'] = previousVxrnMetroOptions
}
}

// globalThis, otherwise we get issues with duplicates due to however vite calls loadConfigFromFile

export function setOneOptions(next: One.PluginOptions) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare const _default: import("vite").UserConfig;
export default _default;
//# sourceMappingURL=vite.config.d.ts.map
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare const _default: import("vite").UserConfig;
export default _default;
//# sourceMappingURL=vite.config.d.ts.map
1 change: 1 addition & 0 deletions packages/one/types/vite/loadConfig.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import '../polyfills-server';
import type { One } from './types';
export declare function loadViteBuildOutDir(configRoot?: string): Promise<string | undefined>;
export declare function setOneOptions(next: One.PluginOptions): void;
export declare function loadUserOneOptions(command: 'serve' | 'build', silent?: boolean): Promise<{
config: {
Expand Down
2 changes: 2 additions & 0 deletions packages/one/types/vite/loadConfig.test.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=loadConfig.test.d.ts.map
Loading