Skip to content
Merged
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
2 changes: 2 additions & 0 deletions docs/guide/api-environment-runtimes.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,8 @@ function createWorkerEnvironment(name, config, context) {
const handlerToWorkerListener = new WeakMap()

const workerHotChannel = {
// Worker threads post messages are not exposed over the network, skip server.fs checks
skipFsCheck: true,
send: (data) => worker.postMessage(data),
on: (event, handler) => {
if (event === 'connection') return
Expand Down
1 change: 1 addition & 0 deletions packages/vite/src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ function defaultCreateClientDevEnvironment(
return new DevEnvironment(name, config, {
hot: true,
transport: context.ws,
disableFetchModule: true,
})
}

Expand Down
20 changes: 17 additions & 3 deletions packages/vite/src/node/server/environment.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { FetchFunctionOptions, FetchResult } from 'vite/module-runner'
import type { FSWatcher } from 'dep-types/chokidar'
import colors from 'picocolors'
import type { FetchFunctionOptions, FetchResult } from 'vite/module-runner'
import {
BaseEnvironment,
getDefaultResolvedEnvironmentOptions,
Expand Down Expand Up @@ -45,6 +45,8 @@ export interface DevEnvironmentContext {
inlineSourceMap?: boolean
}
depsOptimizer?: DepsOptimizer
/** @internal used for client environment */
disableFetchModule?: boolean
}

export class DevEnvironment extends BaseEnvironment {
Expand All @@ -56,6 +58,10 @@ export class DevEnvironment extends BaseEnvironment {
* @internal
*/
_remoteRunnerOptions: DevEnvironmentContext['remoteRunner']
/**
* @internal
*/
_skipFsCheck: boolean

get pluginContainer(): EnvironmentPluginContainer {
if (!this._pluginContainer)
Expand Down Expand Up @@ -121,6 +127,11 @@ export class DevEnvironment extends BaseEnvironment {
this._crawlEndFinder = setupOnCrawlEnd()

this._remoteRunnerOptions = context.remoteRunner ?? {}
this._skipFsCheck = !!(
context.transport &&
!(isWebSocketServer in context.transport) &&
context.transport.skipFsCheck
)

this.hot = context.transport
? isWebSocketServer in context.transport
Expand All @@ -130,6 +141,9 @@ export class DevEnvironment extends BaseEnvironment {

this.hot.setInvokeHandler({
fetchModule: (id, importer, options) => {
if (context.disableFetchModule) {
throw new Error('fetchModule is disabled in this environment')
}
return this.fetchModule(id, importer, options)
},
})
Expand Down Expand Up @@ -210,12 +224,12 @@ export class DevEnvironment extends BaseEnvironment {
}

transformRequest(url: string): Promise<TransformResult | null> {
return transformRequest(this, url)
return transformRequest(this, url, { skipFsCheck: this._skipFsCheck })
}

async warmupRequest(url: string): Promise<void> {
try {
await this.transformRequest(url)
await transformRequest(this, url, { skipFsCheck: true })
} catch (e) {
if (
e?.code === ERR_OUTDATED_OPTIMIZED_DEP ||
Expand Down
6 changes: 6 additions & 0 deletions packages/vite/src/node/server/hmr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ export type HotChannelListener<T extends string = string> = (
) => void

export interface HotChannel<Api = any> {
/**
* When true, the fs access check is skipped in fetchModule.
* Set this for transports that is not exposed over the network.
*/
skipFsCheck?: boolean
/**
* Broadcast events to all clients
*/
Expand Down Expand Up @@ -1132,6 +1137,7 @@ export function createServerHotChannel(): ServerHotChannel {
const outsideEmitter = new EventEmitter()

return {
skipFsCheck: true,
send(payload: HotPayload) {
outsideEmitter.emit('send', payload)
},
Expand Down
41 changes: 35 additions & 6 deletions packages/vite/src/node/server/middlewares/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ import {
ERR_OUTDATED_OPTIMIZED_DEP,
NULL_BYTE_PLACEHOLDER,
} from '../../../shared/constants'
import { checkServingAccess, respondWithAccessDenied } from './static'
import type { ResolvedConfig } from '../../config'
import {
checkLoadingAccess,
checkServingAccess,
respondWithAccessDenied,
} from './static'

const debugCache = createDebugger('vite:cache')

Expand Down Expand Up @@ -80,6 +85,16 @@ function deniedServingAccessForTransform(
return false
}

export function isServerAccessDeniedForTransform(
config: ResolvedConfig,
id: string,
): boolean {
if (rawRE.test(id) || urlRE.test(id) || inlineRE.test(id) || svgRE.test(id)) {
return checkLoadingAccess(config, id) !== 'allowed'
}
return false
}

/**
* A middleware that short-circuits the middleware chain to serve cached transformed modules
*/
Expand Down Expand Up @@ -266,9 +281,7 @@ export function transformMiddleware(
// resolve, load and transform using the plugin container
const result = await transformRequest(environment, url, {
html: req.headers.accept?.includes('text/html'),
allowId(id) {
return !deniedServingAccessForTransform(id, server, res, next)
},
skipFsCheck: environment._skipFsCheck,
})
if (result) {
const depsOptimizer = environment.depsOptimizer
Expand Down Expand Up @@ -340,8 +353,24 @@ export function transformMiddleware(
return next()
}
if (e?.code === ERR_DENIED_ID) {
// next() is called in ensureServingAccess
return
const id: string = e.id
let servingAccessResult = checkLoadingAccess(
server.config,
cleanUrl(id),
)
if (servingAccessResult === 'allowed') {
servingAccessResult = checkLoadingAccess(server.config, id)
}
if (servingAccessResult === 'denied') {
respondWithAccessDenied(id, server, res)
return true
}
if (servingAccessResult === 'fallback') {
next()
return true
}
servingAccessResult satisfies 'allowed'
throw new Error(`Unexpected access result for id ${id}`)
}
return next(e)
}
Expand Down
17 changes: 12 additions & 5 deletions packages/vite/src/node/server/transformRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
} from '../utils'
import { ssrTransform } from '../ssr/ssrTransform'
import { checkPublicFile } from '../publicDir'
import { cleanUrl, unwrapId } from '../../shared/utils'
import { cleanUrl, slash, unwrapId } from '../../shared/utils'
import {
applySourcemapIgnoreList,
extractSourcemapFromFile,
Expand All @@ -29,6 +29,7 @@ import {
import { isFileLoadingAllowed } from './middlewares/static'
import { throwClosedServerError } from './pluginContainer'
import type { DevEnvironment } from './environment'
import { isServerAccessDeniedForTransform } from './middlewares/transform'

export const ERR_LOAD_URL = 'ERR_LOAD_URL'
export const ERR_LOAD_PUBLIC_URL = 'ERR_LOAD_PUBLIC_URL'
Expand Down Expand Up @@ -57,9 +58,10 @@ export interface TransformOptions {
*/
html?: boolean
/**
* Whether to skip the `server.fs` check.
* @internal
*/
allowId?: (id: string) => boolean
skipFsCheck?: boolean
}

// TODO: This function could be moved to the DevEnvironment class.
Expand Down Expand Up @@ -253,8 +255,13 @@ async function loadAndTransform(

const moduleGraph = environment.moduleGraph

if (options.allowId && !options.allowId(id)) {
if (
!options.skipFsCheck &&
id[0] !== '\0' &&
isServerAccessDeniedForTransform(config, id)
) {
const err: any = new Error(`Denied ID ${id}`)
err.id = id
err.code = ERR_DENIED_ID
throw err
}
Expand All @@ -280,8 +287,8 @@ async function loadAndTransform(
// only try the fallback if access is allowed, skip for out of root url
// like /service-worker.js or /api/users
if (
environment.config.consumer === 'server' ||
isFileLoadingAllowed(environment.getTopLevelConfig(), file)
options.skipFsCheck ||
isFileLoadingAllowed(environment.getTopLevelConfig(), slash(file))
) {
try {
code = await fsp.readFile(file, 'utf-8')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'ok'
23 changes: 23 additions & 0 deletions packages/vite/src/node/ssr/__tests__/ssrLoadModule.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,3 +376,26 @@ test('buildStart before transform', async () => {
]
`)
})

test('server.fs check is not applied to ssrLoadModule', async () => {
const server = await createServer({
configFile: false,
root,
logLevel: 'silent',
optimizeDeps: {
noDiscovery: true,
},
server: {
fs: {
allow: [
path.resolve(import.meta.dirname, './fixtures/named-overwrite-all'),
],
},
},
})
onTestFinished(() => server.close())
await server.environments.ssr.pluginContainer.buildStart({})

const mod = await server.ssrLoadModule('/fixtures/basic/file.js')
expect(mod.default).toBe('ok')
})
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'error'
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { existsSync, readdirSync } from 'node:fs'
import { posix, win32 } from 'node:path'
import { posix, resolve, win32 } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, vi } from 'vitest'
import { isWindows } from '../../../../shared/utils'
Expand Down Expand Up @@ -481,3 +481,18 @@ describe('virtual module hmr', async () => {
})
})
})

describe('server.fs check', async () => {
const it = await createModuleRunnerTester({
server: {
fs: {
allow: [resolve(import.meta.dirname, './fixtures/circular')],
},
},
})

it('it is not applied to the server module runner', async ({ runner }) => {
const mod = await runner.import('/fixtures/basic.js')
expect(mod.name).toBe('basic')
})
})
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BroadcastChannel, Worker } from 'node:worker_threads'
import path from 'node:path'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import type { BirpcReturn } from 'birpc'
import { createBirpc } from 'birpc'
Expand Down Expand Up @@ -34,6 +35,9 @@ describe('running module runner inside a worker and using the ModuleRunnerTransp
hmr: {
port: 9610,
},
fs: {
allow: [path.resolve(import.meta.dirname, './fixtures')],
},
},
environments: {
worker: {
Expand Down Expand Up @@ -99,4 +103,13 @@ describe('running module runner inside a worker and using the ModuleRunnerTransp
expect(output).not.toHaveProperty('result')
expect(output.error).toContain('Error: Unknown invoke error')
})

it('server.fs check is applied to the custom transport by default', async () => {
handleInvoke = (data: any) =>
server.environments.worker.hot.handleInvoke(data)

const output = await run('./fixture-outside.js')
expect(output).toHaveProperty('error')
expect(output.error).toContain('Failed to load url')
})
})
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BroadcastChannel, Worker } from 'node:worker_threads'
import path from 'node:path'
import { describe, expect, it, onTestFinished } from 'vitest'
import type { HotChannel, HotChannelListener, HotPayload } from 'vite'
import { DevEnvironment } from '../../..'
Expand Down Expand Up @@ -95,4 +96,48 @@ describe('running module runner inside a worker', () => {
channel.postMessage({ id: './fixtures/default-string.ts' })
})
})

it('server.fs check is applied to the custom transport by default', async () => {
const worker = new Worker(
new URL('./fixtures/worker.mjs', import.meta.url),
{ stdout: true },
)
await new Promise<void>((resolve, reject) => {
worker.on('message', () => resolve())
worker.on('error', reject)
})
const server = await createServer({
root: import.meta.dirname,
logLevel: 'error',
server: {
middlewareMode: true,
watch: null,
hmr: {
port: 9609,
},
fs: {
allow: [path.resolve(import.meta.dirname, './fixtures')],
},
},
environments: {
worker: {
dev: {
createEnvironment: (name, config) => {
return new DevEnvironment(name, config, {
hot: false,
transport: createWorkerTransport(worker),
})
},
},
},
},
})
onTestFinished(async () => {
await Promise.allSettled([server.close(), worker.terminate()])
})

await expect(
server.environments.worker.transformRequest('./fixture-outside.js'),
).rejects.toThrow('Failed to load url')
})
})
4 changes: 2 additions & 2 deletions packages/vite/src/node/ssr/runtime/serverModuleRunner.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { existsSync, readFileSync } from 'node:fs'
import { ModuleRunner } from 'vite/module-runner'
import type { HotPayload } from 'types/hmrPayload'
import type {
ModuleEvaluator,
ModuleRunnerHmr,
ModuleRunnerOptions,
} from 'vite/module-runner'
import type { HotPayload } from 'types/hmrPayload'
import { ModuleRunner } from 'vite/module-runner'
import type { DevEnvironment } from '../../server/environment'
import type {
HotChannelClient,
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/ssr/ssrModuleLoader.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import colors from 'picocolors'
import type { EvaluatedModuleNode } from 'vite/module-runner'
import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner'
import type { EvaluatedModuleNode } from 'vite/module-runner'
import type { ViteDevServer } from '../server'
import { unwrapId } from '../../shared/utils'
import type { DevEnvironment } from '../server/environment'
Expand Down
Loading
Loading