Skip to content

Commit e0cad28

Browse files
fix: Windows path bugs across build/server/dev/native paths
Static audit of every `path.*` / `await import()` / `JSON.stringify(path)` / chokidar / regex-anchored-on-`/` site surfaced a class of Windows-only bugs where native-separator producers feed POSIX-expecting consumers. Same defect class as #640 / commit 61302c5 (closed PR #695). Each fix is at the producer. packages/one — seed bug + audit follow-ups: - cli/build.ts mints serverJsPath via path.join (native sep). Downstream oneServe.ts runs .includes('${outDir}/server') (forward-slash literal); the check misses on Windows, the prefix is prepended a second time, and `dist\server\dist\server\...` is passed to await import() — production SSR returns 200 but loaderData is undefined. Fix at producer with posix.join: build.ts:616 (builtMiddlewares), :1031 (serverJsPath), oneServe.ts:320 (apiFile). The regex at build.ts:1366 and the literal replace at vercel/build/generate/createSsrServerlessFunction.ts:130 start matching on Windows for free. - utils/toServerOutputPath.ts (new): idempotent prefix-or-keep helper for the symmetric oneServe.ts call sites (lines 168 and 334). posix.join + startsWith with a trailing slash; backslash-input boundary conversion; no false-positive on substrings. 7 unit tests. - vite/plugins/virtualEntryPlugin.ts:97: pathToFileURL().href for the setupFile import specifier. JSON.stringify of a Windows-backslash absolute path is not a canonical ESM specifier shape. Mirrors the createNativeDevEngine fix below. Direct repro is currently blocked by an unrelated rolldown rust panic on Windows; this is preventive correctness. - vite/plugins/fileSystemRouterPlugin.tsx:559: posix.join for optimizeDeps.entries (tinyglobby needs forward-slash patterns). Also dropped the hardcoded './app' for routerRoot — pre-existing bug for users with router.root or ONE_ROUTER_ROOT overrides. - metro-config/getViteMetroPluginOptions.ts:244,264: normalizePath on path.relative results inlined by babel into metro-entry.js. ONE_SETUP_FILE_NATIVE truly needs forward-slash on Windows (Metro's isRelativeImport regex rejects `..\foo`); ONE_ROUTER_APP_ROOT_RELATIVE_TO_ENTRY is hygiene. - cli/build.ts:559,566: posix.join in rolldown chunkFileNames/assetFileNames. Latent for flat chunks; diverges for nested API routes. - cli/build.ts:1480: normalizePath on wranglerInputConfig.main. Latent today (relative() returns bare filename); preventive against future source-layout changes. - cli/buildPage.ts:67: clientJsPath was minted via join(clientDir, ...) → backslash on Windows. Currently only used by readFile (accepts both), but stored in dist/buildInfo.json with backslashes — cross-platform manifest hygiene fix matching serverJsPath. normalizePath wrap. - vite/one.ts:255: SSR symlink-dedup compares Vite's POSIX resolved.id against native realpathSync(nmPkgDir) on Windows; startsWith misses and dedup never fires for pnpm symlinks (allows duplicate React copies in SSR bundle). normalizePath both sides. Only active when ssr.dedupeSymlinkedModules: true (opt-in). packages/compiler: - transformSWC.ts:14: ignoreId regex used native path.sep (`\\` on Windows) but Vite plugin context hands the function a POSIX id on every platform, so the regex never matched on Windows — Vite-internal `.vite/deps/...` files got pushed through full SWC parse-and-transform instead of being short-circuited. Performance regression, not functional break. The function's other caller (vxrn/utils/patches.ts:274) hands native paths. Fix: POSIX-only regex (`/node_modules\/(\.vite|vite)\//`) plus normalize id once at the top of transformSWC so both callers agree on the format. Also fixes an HMR cache-key inconsistency in the same line — `id.replace (process.cwd(), '')` no longer no-ops on Windows when called from the Vite plugin path. packages/vxrn: - utils/getVitePath.ts:81: normalizePath(id) before endsWith('/react/...') sentinel. realpath returns native on Windows. Function has no in-tree caller; preventive for external consumers. - exports/dev.ts:148-157: normalize chokidar's native path and process.cwd() before stripping; viteServer.transformRequest expects POSIX URLs. Also collapsed the dual `'/dist/' || '\\dist\\'` check. Vite-native HMR codepath (Metro HMR uses its own pipeline, unaffected). - utils/createNativeDevEngine.ts:568,587: normalizePath() for the rolldown module id; pathToFileURL().href for the JSON.stringify-embedded setupFile import specifier. packages/vite-plugin-metro: - plugins/expoManifestRequestHandlerPlugin.ts:36: resolve(server.config.root) for the Vite→Metro/Expo boundary. Commit 61302c5 applied the same fix at metroPlugin.ts:88; this sibling boundary was missed. Test-only fixes (cross-platform hygiene): - vite/plugins/sourceInspectorPlugin.ts: normalizePath wrap on resolveEditorFilePath output; mirrors sibling getSourceInspectorPath. Production behavior unchanged. - packages/resolve/src/index.test.ts: construct expected paths via path.join (resolvePath returns native paths in production). - packages/vxrn/src/utils/patches.test.ts: symlink type 'dir' → 'junction'. Per Node fs docs the type arg is ignored on POSIX; on Windows junctions don't require Administrator. Single codepath. 4 fail → 8/8 pass. - packages/vxrn/src/plugins/serverExtensions.test.ts: source was refactored from FSExtra.pathExists to node:fs.existsSync without updating mocks; two config-extension tests also called plugin.config() with zero args while source destructures { command } from arg 2. Switch to vi.mock('node:fs', factory) (ESM module-namespace exports aren't spy-able) and pass the required SERVE_HOOK_OPTS. 4 fail → 7/7 pass on every platform. - packages/vxrn/package.json: add `"test": "vitest run --dir src"` script so turbo picks vxrn up in `bun run test` and CI now runs the 33 vxrn cases. Previously vxrn was typecheck-only on CI, which is how the serverExtensions stale mocks and patches.test EPERM sat broken without anyone noticing. Mirrors @vxrn/resolve. End-to-end verified on Windows host: SSR loaders return real data; web HMR fires; APK builds via Gradle; native HMR fires on Android emulator (Metro bundler). Cross-platform tests identical on Windows and Linux Docker (402 packages/one + 9 packages/resolve + 33 packages/vxrn/src + 20 helper/ posix tests, 0 fail). Vite-native bundler (`native.bundler: 'vite'`) still crashes on Windows with an unrelated rolldown rust panic (`crates/fft/src/hparser/convert.rs:120`); the createNativeDevEngine + dev.ts fixes target that codepath but cannot be directly exercised until rolldown ships a fix.
1 parent a677174 commit e0cad28

23 files changed

Lines changed: 303 additions & 62 deletions

packages/compiler/src/transformSWC.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { extname, sep } from 'node:path'
1+
import { extname } from 'node:path'
22
import {
33
type Output,
44
type ParserConfig,
@@ -7,27 +7,28 @@ import {
77
transform,
88
} from '@swc/core'
99
import { merge } from 'ts-deepmerge'
10+
import { normalizePath } from 'vite'
1011
import { configuration } from './configure'
1112
import { asyncGeneratorRegex, debug, parsers, runtimePublicPath } from './constants'
1213
import type { Options } from './types'
1314

14-
const ignoreId = new RegExp(`node_modules\\${sep}(\\.vite|vite)\\${sep}`)
15+
// POSIX-only sentinel — Vite plugin context hands us forward-slash ids; the
16+
// other caller (patches.ts) hands native paths, so we normalize id below.
17+
const ignoreId = /node_modules\/(\.vite|vite)\//
1518

1619
export async function transformSWC(
1720
id: string,
1821
code: string,
1922
options: Options & { es5?: boolean },
2023
swcOptions?: SWCOptions
2124
) {
25+
// unify caller contracts (Vite plugin: POSIX id; patches.ts: native id)
26+
id = normalizePath(id.split('?')[0]).replace(normalizePath(process.cwd()), '')
27+
2228
if (ignoreId.test(id)) {
2329
return
2430
}
2531

26-
id = id
27-
.split('?')[0]
28-
// fixes hmr
29-
.replace(process.cwd(), '')
30-
3132
if (id === runtimePublicPath) {
3233
return
3334
}

packages/one/src/cli/build.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createRequire } from 'node:module'
22
import { cpus } from 'node:os'
3-
import Path, { join, relative, resolve } from 'node:path'
3+
import Path, { join, posix, relative, resolve } from 'node:path'
44
import { resolvePath } from '@vxrn/resolve'
55
import FSExtra from 'fs-extra'
66
import MicroMatch from 'micromatch'
@@ -282,7 +282,7 @@ process.on('uncaughtException', (err) => {
282282
process.on('unhandledRejection', (reason) => {
283283
try {
284284
process.stderr.write(
285-
`[one build] unhandled rejection\n${formatErrorSafely(reason)}\n`,
285+
`[one build] unhandled rejection\n${formatErrorSafely(reason)}\n`
286286
)
287287
} catch {}
288288
process.exit(1)
@@ -547,14 +547,15 @@ export async function build(args: {
547547
chunkInfo.name,
548548
Path.extname(chunkInfo.name)
549549
)
550-
return Path.join(dir, `${name}-[hash].cjs`)
550+
// posix so nested-chunk fileNames stay forward-slash on Windows
551+
return posix.join(dir, `${name}-[hash].cjs`)
551552
},
552553
assetFileNames: (assetInfo) => {
553554
const name = assetInfo.name ?? ''
554555
const dir = Path.dirname(name)
555556
const baseName = Path.basename(name, Path.extname(name))
556557
const ext = Path.extname(name)
557-
return Path.join(dir, `${baseName}-[hash]${ext}`)
558+
return posix.join(dir, `${baseName}-[hash]${ext}`)
558559
},
559560
}),
560561
},
@@ -601,7 +602,12 @@ export async function build(args: {
601602
const outChunks = middlewareBuildInfo.output.filter((x) => x.type === 'chunk')
602603
const chunk = outChunks.find((x) => x.facadeModuleId === fullPath)
603604
if (!chunk) throw new Error(`internal err finding middleware`)
604-
builtMiddlewares[middleware.file] = join(outDir, 'middlewares', chunk.fileName)
605+
// posix so manifest entry stays forward-slash on Windows
606+
builtMiddlewares[middleware.file] = posix.join(
607+
outDir,
608+
'middlewares',
609+
chunk.fileName
610+
)
605611
}
606612
}
607613

@@ -1008,7 +1014,8 @@ export async function build(args: {
10081014
})
10091015
}
10101016

1011-
const serverJsPath = join(`${outDir}/server`, serverFileName)
1017+
// posix so downstream `.includes('${outDir}/server')` substring checks match on Windows
1018+
const serverJsPath = posix.join(outDir, 'server', serverFileName)
10121019

10131020
let exported
10141021
try {
@@ -1452,7 +1459,10 @@ export default {
14521459
projectName,
14531460
userWranglerConfig?.config
14541461
)
1455-
wranglerInputConfig.main = relative(join(options.root, outDir), workerSrcPath)
1462+
// serialized wrangler config is diffed cross-platform; keep forward-slash
1463+
wranglerInputConfig.main = normalizePath(
1464+
relative(join(options.root, outDir), workerSrcPath)
1465+
)
14561466

14571467
const wranglerInputPath = join(options.root, outDir, '_wrangler.input.jsonc')
14581468
await FSExtra.writeFile(

packages/one/src/cli/buildPage.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { join } from 'node:path'
22
import FSExtra from 'fs-extra'
3+
import { normalizePath } from 'vite'
34
import * as constants from '../constants'
45
import { LOADER_JS_POSTFIX_UNCACHED } from '../constants'
56
import type { LoaderProps } from '../types'
@@ -64,8 +65,9 @@ export async function buildPage(
6465
recordTiming('getRender', performance.now() - t0)
6566

6667
const htmlPath = `${path.endsWith('/') ? `${removeTrailingSlash(path)}/index` : path}.html`
68+
// forward-slash for cross-platform manifest hygiene (matches serverJsPath)
6769
const clientJsPath = clientManifestEntry
68-
? join(clientDir, clientManifestEntry.file)
70+
? normalizePath(join(clientDir, clientManifestEntry.file))
6971
: ''
7072
const htmlOutPath = toAbsolute(join(staticDir, htmlPath))
7173
const preloadPath = getPreloadPath(path)

packages/one/src/metro-config/getViteMetroPluginOptions.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path from 'node:path'
33
import type { metroPlugin } from '@vxrn/vite-plugin-metro'
44
import mm from 'micromatch'
55
import tsconfigPaths from 'tsconfig-paths'
6+
import { normalizePath } from 'vite'
67
import {
78
API_ROUTE_GLOB_PATTERN,
89
ROUTE_NATIVE_EXCLUSION_GLOB_PATTERNS,
@@ -232,9 +233,13 @@ export function getViteMetroPluginOptions({
232233
[
233234
'one/babel-plugin-one-router-metro',
234235
{
235-
ONE_ROUTER_APP_ROOT_RELATIVE_TO_ENTRY: path.relative(
236-
path.dirname(metroEntryPath),
237-
path.join(projectRoot, relativeRouterRoot)
236+
// both values are inlined by babel into metro-entry.js as JS module
237+
// specifiers — Metro's isRelativeImport regex rejects `..\foo` on Windows
238+
ONE_ROUTER_APP_ROOT_RELATIVE_TO_ENTRY: normalizePath(
239+
path.relative(
240+
path.dirname(metroEntryPath),
241+
path.join(projectRoot, relativeRouterRoot)
242+
)
238243
),
239244
ONE_ROUTER_ROOT_FOLDER_NAME: relativeRouterRoot,
240245
ONE_ROUTER_REQUIRE_CONTEXT_REGEX_STRING: routerRequireContextRegexString,
@@ -247,10 +252,11 @@ export function getViteMetroPluginOptions({
247252
? setupFile
248253
: setupFile.native || setupFile.ios || setupFile.android
249254
if (!nativeSetupFile) return undefined
250-
// Return path relative to metro entry
251-
return path.relative(
252-
path.dirname(metroEntryPath),
253-
path.join(projectRoot, nativeSetupFile)
255+
return normalizePath(
256+
path.relative(
257+
path.dirname(metroEntryPath),
258+
path.join(projectRoot, nativeSetupFile)
259+
)
254260
)
255261
})(),
256262
},

packages/one/src/server/oneServe.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Hono, MiddlewareHandler } from 'hono'
22
import type { BlankEnv } from 'hono/types'
33
import { readFile } from 'node:fs/promises'
4-
import { join } from 'node:path'
4+
import { join, posix } from 'node:path'
55
import {
66
CSS_PRELOAD_JS_POSTFIX,
77
LOADER_JS_POSTFIX_UNCACHED,
@@ -17,6 +17,7 @@ import {
1717
import type { RenderAppProps } from '../types'
1818
import { getPathFromLoaderPath } from '../utils/cleanUrl'
1919
import { toAbsoluteUrl } from '../utils/toAbsolute'
20+
import { toServerOutputPath } from '../utils/toServerOutputPath'
2021
import type { One } from '../vite/types'
2122
import type { RouteInfoCompiled } from './createRoutesManifest'
2223
import { setSSRLoaderData } from './ssrLoaderData'
@@ -164,9 +165,7 @@ export async function oneServe(
164165
// cold path - async import
165166
return (async () => {
166167
const pathToResolve = serverPath || lazyKey || ''
167-
const resolvedPath = pathToResolve.includes(`${outDir}/server`)
168-
? pathToResolve
169-
: join('./', `${outDir}/server`, pathToResolve)
168+
const resolvedPath = toServerOutputPath(pathToResolve, outDir)
170169

171170
let routeExported: any
172171
if (moduleImportCache.has(cacheKey)) {
@@ -314,7 +313,8 @@ export async function oneServe(
314313
}
315314
// both vite and rolldown-vite replace brackets with underscores in output filenames
316315
const fileName = route.page.slice(1).replace(/\[/g, '_').replace(/\]/g, '_')
317-
const apiFile = join(outDir, 'api', fileName + (apiCJS ? '.cjs' : '.js'))
316+
// posix matches the serverJsPath/builtMiddlewares producer convention
317+
const apiFile = posix.join(outDir, 'api', fileName + (apiCJS ? '.cjs' : '.js'))
318318
return await import(toAbsoluteUrl(apiFile))
319319
},
320320

@@ -328,9 +328,7 @@ export async function oneServe(
328328

329329
async handleLoader({ route, loaderProps }) {
330330
const routeFile = (route as any).routeFile || route.file
331-
const serverPath = route.file.includes(`${outDir}/server`)
332-
? route.file
333-
: join('./', `${outDir}/server`, route.file)
331+
const serverPath = toServerOutputPath(route.file, outDir)
334332

335333
let loader: Function | null
336334
try {
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// POSIX path contract tests — locks in the cross-platform behavior of the
2+
// Node primitives this PR's fixes depend on. If a future Node version
3+
// changes any of these, the failing test surfaces the regression before
4+
// the framework breaks on Windows again.
5+
6+
import { posix } from 'node:path'
7+
import { pathToFileURL } from 'node:url'
8+
import { describe, expect, it } from 'vitest'
9+
import { normalizePath } from 'vite'
10+
import { toServerOutputPath } from './toServerOutputPath'
11+
12+
describe('path.posix.join — forward-slash output on every platform', () => {
13+
it('joins server-output paths', () => {
14+
expect(posix.join('dist', 'server', 'foo.js')).toBe('dist/server/foo.js')
15+
})
16+
17+
it('joins middleware-output paths', () => {
18+
expect(posix.join('dist', 'middlewares', 'mw-hash.js')).toBe(
19+
'dist/middlewares/mw-hash.js'
20+
)
21+
})
22+
23+
it('strips redundant `./` segments', () => {
24+
expect(posix.join('./app', './_layout.tsx')).toBe('app/_layout.tsx')
25+
})
26+
27+
it('produces a bare filename for a `.` directory component', () => {
28+
expect(posix.join('.', 'foo-[hash].cjs')).toBe('foo-[hash].cjs')
29+
})
30+
31+
it('preserves nested-chunk subdirectories', () => {
32+
expect(posix.join('subdir', 'foo-[hash].cjs')).toBe('subdir/foo-[hash].cjs')
33+
})
34+
})
35+
36+
describe('pathToFileURL — canonical file:// URL specifier', () => {
37+
it('produces a forward-slash href on every platform', () => {
38+
const href = pathToFileURL('/proj/src/setup.ts').href
39+
expect(href.startsWith('file://')).toBe(true)
40+
expect(href.includes('\\')).toBe(false)
41+
})
42+
43+
it('round-trips through JSON.stringify without backslashes leaking in', () => {
44+
// The motivating defect: emitting `import "${absolutePath}"` puts
45+
// backslashes into the generated JS source on Windows. A file:// URL
46+
// does not.
47+
const href = pathToFileURL('/proj/src/setup.ts').href
48+
const stringified = JSON.stringify(href)
49+
expect(stringified.includes('\\\\')).toBe(false)
50+
})
51+
})
52+
53+
describe('Vite normalizePath — converts on Windows, no-op on POSIX', () => {
54+
it('preserves a forward-slash path unchanged', () => {
55+
expect(normalizePath('/proj/src/foo.tsx')).toBe('/proj/src/foo.tsx')
56+
})
57+
58+
it('converts backslashes (Windows-shaped input)', () => {
59+
// normalizePath only converts on Windows hosts. We assert the contract
60+
// shape — on Windows: backslashes converted; on POSIX: passthrough
61+
// because backslash is a valid filename character.
62+
const input = String.raw`C:\proj\src\foo.tsx`
63+
const out = normalizePath(input)
64+
if (process.platform === 'win32') {
65+
expect(out).toBe('C:/proj/src/foo.tsx')
66+
} else {
67+
expect(out).toBe(input)
68+
}
69+
})
70+
})
71+
72+
describe('toServerOutputPath — cross-platform parity', () => {
73+
// toServerOutputPath uses path.posix.join + replace(/\\/g, '/') so the
74+
// output must be byte-identical across platforms. The fact that we
75+
// converted from Vite's normalizePath to this implementation was driven
76+
// by a Linux Docker test failure during initial review.
77+
const cases: Array<[string, string, string]> = [
78+
['foo.js', 'dist', 'dist/server/foo.js'],
79+
['subdir/bar.js', 'dist', 'dist/server/subdir/bar.js'],
80+
['dist/server/foo.js', 'dist', 'dist/server/foo.js'],
81+
[String.raw`dist\server\foo.js`, 'dist', 'dist/server/foo.js'],
82+
[String.raw`subdir\bar.js`, 'dist', 'dist/server/subdir/bar.js'],
83+
['foo/dist/server/bar.js', 'dist', 'dist/server/foo/dist/server/bar.js'],
84+
['dist/server', 'dist', 'dist/server'],
85+
['baz.js', 'build', 'build/server/baz.js'],
86+
]
87+
88+
for (const [input, outDir, expected] of cases) {
89+
it(`("${input}", "${outDir}") → "${expected}"`, () => {
90+
expect(toServerOutputPath(input, outDir)).toBe(expected)
91+
})
92+
}
93+
})
94+
95+
describe('seed bug regression — SSR loader path doubling on Windows', () => {
96+
// Direct regression test for the specific shape that hit production:
97+
// build.ts:1031 minted `dist\server\assets\time_ssr-XXX.js` (native sep);
98+
// oneServe.ts:168 then ran `.includes('dist/server')` on it (forward slash);
99+
// the check missed; `join('./','dist/server','dist\\server\\...')` was
100+
// called; result was `dist\server\dist\server\...` — passed to await import().
101+
it('does not double-prefix backslash-shaped server-output input', () => {
102+
const windowsShapedInput = String.raw`dist\server\assets\time_ssr-COqAsxju.js`
103+
expect(toServerOutputPath(windowsShapedInput, 'dist')).toBe(
104+
'dist/server/assets/time_ssr-COqAsxju.js'
105+
)
106+
})
107+
108+
it('does not double-prefix already-forward-slashed input', () => {
109+
const posixInput = 'dist/server/assets/time_ssr-COqAsxju.js'
110+
expect(toServerOutputPath(posixInput, 'dist')).toBe(posixInput)
111+
})
112+
})
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import { toServerOutputPath } from './toServerOutputPath'
4+
5+
describe('toServerOutputPath', () => {
6+
it('prepends ${outDir}/server/ to bare filenames from Vite output', () => {
7+
expect(toServerOutputPath('assets/loader-abc.js', 'dist')).toBe(
8+
'dist/server/assets/loader-abc.js'
9+
)
10+
})
11+
12+
it('prepends ${outDir}/server/ to project-relative paths from the route tree', () => {
13+
expect(toServerOutputPath('./pages/index.tsx', 'dist')).toBe(
14+
'dist/server/pages/index.tsx'
15+
)
16+
})
17+
18+
it('returns input unchanged when it is already rooted under ${outDir}/server/', () => {
19+
expect(toServerOutputPath('dist/server/assets/loader-abc.js', 'dist')).toBe(
20+
'dist/server/assets/loader-abc.js'
21+
)
22+
})
23+
24+
it('treats backslashes as separators on every platform (the Windows path-doubling case)', () => {
25+
// Simulates what `path.join` produces on Windows after the first prefix
26+
// has been added by an earlier caller. The function must convert
27+
// backslashes to forward slashes here regardless of host OS so the
28+
// sentinel check matches and the prefix isn't doubled.
29+
expect(toServerOutputPath('dist\\server\\assets\\loader-abc.js', 'dist')).toBe(
30+
'dist/server/assets/loader-abc.js'
31+
)
32+
})
33+
34+
it('is idempotent: feeding the output back in returns the same value', () => {
35+
const first = toServerOutputPath('assets/loader-abc.js', 'dist')
36+
const second = toServerOutputPath(first, 'dist')
37+
expect(second).toBe(first)
38+
})
39+
40+
it('honors a custom outDir', () => {
41+
expect(toServerOutputPath('assets/loader.js', 'build')).toBe(
42+
'build/server/assets/loader.js'
43+
)
44+
})
45+
46+
it('does not false-positive on inputs that contain ${outDir}/server as a substring but are not rooted under it', () => {
47+
// Without startsWith, `.includes('dist/server')` would match this input
48+
// and skip the prefix — incorrectly treating it as already-prefixed.
49+
expect(toServerOutputPath('foo/dist/server/bar.js', 'dist')).toBe(
50+
'dist/server/foo/dist/server/bar.js'
51+
)
52+
})
53+
})

0 commit comments

Comments
 (0)