Skip to content
Open
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
21 changes: 11 additions & 10 deletions packages/one/src/cli/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { resolvePath } from '@vxrn/resolve'
import FSExtra from 'fs-extra'
import MicroMatch from 'micromatch'
import type { OutputAsset, RolldownOutput } from 'rolldown'
import { type InlineConfig, mergeConfig, build as viteBuild } from 'vite'
import { type InlineConfig, mergeConfig, normalizePath, build as viteBuild } from 'vite'
import {
type ClientManifestEntry,
fillOptions,
Expand All @@ -19,7 +19,7 @@ import { setServerGlobals } from '../server/setServerGlobals'
import { getPathnameFromFilePath } from '../utils/getPathnameFromFilePath'
import { getRouterRootFromOneOptions } from '../utils/getRouterRootFromOneOptions'
import { isRolldown } from '../utils/isRolldown'
import { toAbsolute } from '../utils/toAbsolute'
import { toAbsolute, toAbsoluteUrl } from '../utils/toAbsolute'
import { buildVercelOutputDirectory } from '../vercel/build/buildVercelOutputDirectory'
import { getManifest } from '../vite/getManifest'
import { loadUserOneOptions } from '../vite/loadConfig'
Expand Down Expand Up @@ -347,7 +347,7 @@ export async function build(args: {
if (middlewareBuildInfo) {
for (const middleware of manifest.middlewareRoutes) {
const absoluteRoot = resolve(process.cwd(), options.root)
const fullPath = join(absoluteRoot, routerRoot, middleware.file)
const fullPath = normalizePath(join(absoluteRoot, routerRoot, middleware.file))
const outChunks = middlewareBuildInfo.output.filter((x) => x.type === 'chunk')
const chunk = outChunks.find((x) => x.facadeModuleId === fullPath)
if (!chunk) throw new Error(`internal err finding middleware`)
Expand All @@ -356,7 +356,7 @@ export async function build(args: {
}

// for the require Sitemap in getRoutes
globalThis['require'] = createRequire(join(import.meta.url, '..'))
globalThis['require'] = createRequire(import.meta.dirname + '/')

const assets: OutputAsset[] = []

Expand Down Expand Up @@ -426,7 +426,10 @@ export async function build(args: {
// layout files start with _layout
if (file.startsWith('_layout') && id.includes(`/${routerRoot}/`)) {
// contextKey format is "./_layout.tsx" or "./subdir/_layout.tsx"
const relativePath = relative(process.cwd(), id).replace(`${routerRoot}/`, '')
const relativePath = normalizePath(relative(process.cwd(), id)).replace(
`${routerRoot}/`,
''
)
const contextKey = `./${relativePath}`
layoutServerPaths.set(contextKey, output.fileName)
}
Expand Down Expand Up @@ -469,10 +472,8 @@ export async function build(args: {
}

// resolve the full module path for this route
const routeModulePath = join(
resolve(process.cwd(), options.root),
routerRoot,
foundRoute.file.slice(2)
const routeModulePath = normalizePath(
join(resolve(process.cwd(), options.root), routerRoot, foundRoute.file.slice(2))
)

// find the server chunk containing this route
Expand Down Expand Up @@ -761,7 +762,7 @@ export async function build(args: {

let exported
try {
exported = await import(toAbsolute(serverJsPath))
exported = await import(toAbsoluteUrl(serverJsPath))
} catch (err) {
console.error(`Error importing page (original error)`, err)
// err cause not showing in vite or something
Expand Down
8 changes: 4 additions & 4 deletions packages/one/src/cli/buildPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { LOADER_JS_POSTFIX_UNCACHED } from '../constants'
import type { LoaderProps } from '../types'
import { getLoaderPath, getPreloadCSSPath, getPreloadPath } from '../utils/cleanUrl'
import { isResponse } from '../utils/isResponse'
import { toAbsolute } from '../utils/toAbsolute'
import { toAbsolute, toAbsoluteUrl } from '../utils/toAbsolute'
import { replaceLoader } from '../vite/replaceLoader'
import type { One, RouteInfo } from '../vite/types'

Expand Down Expand Up @@ -174,7 +174,7 @@ prefetchCSS()
recordTiming('writeCSSPreload', performance.now() - t0)

t0 = performance.now()
const exported = await import(toAbsolute(serverJsPath))
const exported = await import(toAbsoluteUrl(serverJsPath))
recordTiming('importServerModule', performance.now() - t0)

const loaderProps: LoaderProps = { path, params }
Expand All @@ -195,7 +195,7 @@ prefetchCSS()
// derive server dir from clientDir (e.g. dist/client -> dist/server)
const serverDir = join(clientDir, '..', 'server')
const layoutExported = await import(
toAbsolute(join(serverDir, layoutServerPath))
toAbsoluteUrl(join(serverDir, layoutServerPath))
)
const layoutLoaderData = await layoutExported?.loader?.(loaderProps)
return { contextKey: layout.contextKey, loaderData: layoutLoaderData }
Expand Down Expand Up @@ -478,7 +478,7 @@ params:\n\n${JSON.stringify(params || null, null, 2)}`

async function getRender(serverEntry: string) {
try {
const serverImport = await import(serverEntry)
const serverImport = await import(toAbsoluteUrl(serverEntry))

const render =
serverImport.default.render ||
Expand Down
34 changes: 34 additions & 0 deletions packages/one/src/metro-config/getViteMetroPluginOptions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest'
import { normalizeReSource } from './getViteMetroPluginOptions'

// String.raw avoids double-escaping: String.raw`[\\/]` is the 5-char string [ \ \ / ]
// which is exactly what regex.source produces for /[\\/]/

describe('normalizeReSource', () => {
it(String.raw`[\\/] to \/`, () => {
expect(normalizeReSource(String.raw`[\\/]`)).toBe(String.raw`\/`)
})

it(String.raw`[^\\/] to [^/]`, () => {
expect(normalizeReSource(String.raw`[^\\/]`)).toBe('[^/]')
})

it('full Windows micromatch regex', () => {
// micromatch.makeRe('**/*.web.(ts|tsx)').source on Windows (picomatch 2.x)
const windowsSource = String.raw`^(?:(?:^|[\\/]|(?:(?:(?!(?:^|[\\/])\.).)*?)[\\/])(?!\.)(?=.)[^\\/]*?\.web\.(ts|tsx))$`
const posixSource = String.raw`^(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.web\.(ts|tsx))$`

expect(normalizeReSource(windowsSource)).toBe(posixSource)
})

it('no-op for POSIX regex', () => {
const posixSource = String.raw`^(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.web\.(ts|tsx))$`

expect(normalizeReSource(posixSource)).toBe(posixSource)
})

it('multiple groups in one source', () => {
const source = String.raw`a[\\/]b[\\/]c[^\\/]d`
expect(normalizeReSource(source)).toBe(String.raw`a\/b\/c[^/]d`)
})
})
25 changes: 14 additions & 11 deletions packages/one/src/metro-config/getViteMetroPluginOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ import {
ROUTE_NATIVE_EXCLUSION_GLOB_PATTERNS,
} from '../router/glob-patterns'

/**
* On Windows, micromatch.makeRe() produces regex patterns with `[\\/]` or `[^\\/]`
* instead of `\/` and `[^/]`. Normalize them so the startsWith check works.
*/
export function normalizeReSource(source: string): string {
return source.replace(/\[\\\\\/\]/g, '\\/').replace(/\[\^\\\\\/\]/g, '[^/]')
}

export function getViteMetroPluginOptions({
projectRoot,
relativeRouterRoot,
Expand Down Expand Up @@ -56,7 +64,7 @@ export function getViteMetroPluginOptions({
* ^(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\+api\.(ts|tsx))$
* ```
*/
const reSource = re.source
const reSource = normalizeReSource(re.source)

if (
!(
Expand Down Expand Up @@ -112,13 +120,7 @@ export function getViteMetroPluginOptions({
// .filter((i): i is NonNullable<typeof i> => !!i)
// ),
},
nodeModulesPaths: tsconfigPathsConfigLoadResult.absoluteBaseUrl
? [
// "vite-tsconfig-paths" for Metro
tsconfigPathsConfigLoadResult.absoluteBaseUrl,
...(defaultConfig?.resolver?.nodeModulesPaths || []),
]
: defaultConfig?.resolver?.nodeModulesPaths,
nodeModulesPaths: defaultConfig?.resolver?.nodeModulesPaths,
resolveRequest: (context, moduleName, platform) => {
if (moduleName.endsWith('.css')) {
return {
Expand Down Expand Up @@ -153,12 +155,13 @@ export function getViteMetroPluginOptions({
const defaultResolveRequest =
defaultConfig?.resolver?.resolveRequest || context.resolveRequest
const res = defaultResolveRequest(context, moduleName, platform)
if (res && 'filePath' in res && res.filePath.includes('/src/index.ts')) {
const svgSrcSuffix = `${path.sep}src${path.sep}index.ts`
if (res && 'filePath' in res && res.filePath.includes(svgSrcSuffix)) {
return {
...res,
filePath: res.filePath.replace(
'/src/index.ts',
'/lib/commonjs/index.js'
svgSrcSuffix,
`${path.sep}lib${path.sep}commonjs${path.sep}index.js`
),
}
}
Expand Down
22 changes: 8 additions & 14 deletions packages/one/src/server/oneServe.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Hono, MiddlewareHandler } from 'hono'
import type { BlankEnv } from 'hono/types'
import { readFile } from 'node:fs/promises'
import { join, resolve } from 'node:path'
import { join } from 'node:path'
import {
CSS_PRELOAD_JS_POSTFIX,
LOADER_JS_POSTFIX_UNCACHED,
Expand All @@ -16,7 +16,7 @@ import {
} from '../createHandleRequest'
import type { RenderAppProps } from '../types'
import { getPathFromLoaderPath } from '../utils/cleanUrl'
import { toAbsolute } from '../utils/toAbsolute'
import { toAbsoluteUrl } from '../utils/toAbsolute'
import type { One } from '../vite/types'
import type { RouteInfoCompiled } from './createRoutesManifest'
import { setSSRLoaderData } from './ssrLoaderData'
Expand Down Expand Up @@ -160,8 +160,8 @@ export async function oneServe(
routeExported = lazyKey
? options?.lazyRoutes?.pages?.[lazyKey]
? await options.lazyRoutes.pages[lazyKey]()
: await import(toAbsolute(resolvedPath))
: await import(toAbsolute(serverPath!))
: await import(toAbsoluteUrl(resolvedPath))
: await import(toAbsoluteUrl(serverPath!))
moduleImportCache.set(cacheKey, routeExported)
}

Expand Down Expand Up @@ -264,8 +264,7 @@ export async function oneServe(
const entry = options?.lazyRoutes?.serverEntry
? await options.lazyRoutes.serverEntry()
: await import(
resolve(
process.cwd(),
toAbsoluteUrl(
`${serverOptions.root}/${outDir}/server/_virtual_one-entry.${typeof oneOptions.build?.server === 'object' && oneOptions.build.server.outputFormat === 'cjs' ? 'c' : ''}js`
)
)
Expand Down Expand Up @@ -300,21 +299,16 @@ export async function oneServe(
}
// both vite and rolldown-vite replace brackets with underscores in output filenames
const fileName = route.page.slice(1).replace(/\[/g, '_').replace(/\]/g, '_')
const apiFile = join(
process.cwd(),
outDir,
'api',
fileName + (apiCJS ? '.cjs' : '.js')
)
return await import(apiFile)
const apiFile = join(outDir, 'api', fileName + (apiCJS ? '.cjs' : '.js'))
return await import(toAbsoluteUrl(apiFile))
},

async loadMiddleware(route) {
// Use lazy import if available (workers), otherwise dynamic import (Node.js)
if (options?.lazyRoutes?.middlewares?.[route.contextKey]) {
return await options.lazyRoutes.middlewares[route.contextKey]()
}
return await import(toAbsolute(route.contextKey))
return await import(toAbsoluteUrl(route.contextKey))
},

async handleLoader({ route, loaderProps }) {
Expand Down
5 changes: 5 additions & 0 deletions packages/one/src/utils/toAbsolute.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'

/** Resolve to native filesystem path — for fs operations (readFile, writeFile, join). */
export const toAbsolute = (p: string) => resolve(process.cwd(), p)

/** Resolve to file:// URL — for dynamic import() which requires URLs on Windows. */
export const toAbsoluteUrl = (p: string) => pathToFileURL(resolve(process.cwd(), p)).href
4 changes: 2 additions & 2 deletions packages/one/src/utils/workerImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*
* Usage: Pass the caller's import.meta.url to resolve relative paths correctly.
*/
import { fileURLToPath } from 'node:url'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { dirname, resolve } from 'node:path'

type ModuleCache = Map<string, any>
Expand Down Expand Up @@ -33,7 +33,7 @@ export async function workerImport<T = any>(
const mjsPath = absolutePath.endsWith('.mjs') ? absolutePath : `${absolutePath}.mjs`

// @ts-ignore - runtime needs .mjs extension for proper ESM resolution
const mod = await import(mjsPath)
const mod = await import(pathToFileURL(mjsPath).href)
cache.set(cacheKey, mod)
return mod
}
Loading