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
51 changes: 50 additions & 1 deletion packages/devframe/src/utils/serve-static.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { H3, toNodeHandler } from 'h3'
import { afterEach, describe, expect, it } from 'vitest'
import { serveStaticHandler, serveStaticNodeMiddleware } from './serve-static'
import { mountStaticHandler, serveStaticHandler, serveStaticNodeMiddleware } from './serve-static'

interface Fixture {
dir: string
Expand Down Expand Up @@ -167,6 +167,55 @@ describe('serveStaticHandler', () => {
})
})

describe('mountStaticHandler', () => {
it('serves files when mounted at the root', async () => {
const dir = makeTmp('devframe-serve-root-')
writeFileSync(join(dir, 'index.html'), 'root-index', 'utf-8')
writeFileSync(join(dir, 'app.js'), 'root-app', 'utf-8')

const app = new H3()
mountStaticHandler(app, '/', dir)

const indexResponse = await app.request('/')
const assetResponse = await app.request('/app.js')
const missingResponse = await app.request('/missing.js')

expect(indexResponse.status).toBe(200)
expect(await indexResponse.text()).toBe('root-index')
expect(assetResponse.status).toBe(200)
expect(await assetResponse.text()).toBe('root-app')
expect(missingResponse.status).toBe(404)
})

it('keeps static bases with overlapping prefixes isolated', async () => {
const viteDir = makeTmp('devframe-serve-vite-')
const vitestDir = makeTmp('devframe-serve-vitest-')
writeFileSync(join(viteDir, 'index.html'), 'vite-index', 'utf-8')
writeFileSync(join(viteDir, 'favicon.svg'), 'vite', 'utf-8')
writeFileSync(join(vitestDir, 'favicon.svg'), 'vitest', 'utf-8')

const app = new H3()
mountStaticHandler(app, '/__devtools-vite/', viteDir)
mountStaticHandler(app, '/__devtools-vitest/', vitestDir)

const viteBaseResponse = await app.request('/__devtools-vite')
const viteBaseSlashResponse = await app.request('/__devtools-vite/')
const viteResponse = await app.request('/__devtools-vite/favicon.svg')
const vitestResponse = await app.request('/__devtools-vitest/favicon.svg')
const adjacentResponse = await app.request('/__devtools-vite-extra/favicon.svg')

expect(viteBaseResponse.status).toBe(200)
expect(await viteBaseResponse.text()).toBe('vite-index')
expect(viteBaseSlashResponse.status).toBe(200)
expect(await viteBaseSlashResponse.text()).toBe('vite-index')
expect(viteResponse.status).toBe(200)
expect(await viteResponse.text()).toBe('vite')
expect(vitestResponse.status).toBe(200)
expect(await vitestResponse.text()).toBe('vitest')
expect(adjacentResponse.status).toBe(404)
})
})

describe('serveStaticNodeMiddleware', () => {
let fx: Fixture | undefined

Expand Down
24 changes: 9 additions & 15 deletions packages/devframe/src/utils/serve-static.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { EventHandler, H3 } from 'h3'
import type { EventHandler } from 'h3'
import type { IncomingMessage, ServerResponse } from 'node:http'
import { createReadStream } from 'node:fs'
import { stat } from 'node:fs/promises'
import { Readable } from 'node:stream'
import { defineHandler, withBase } from 'h3'
import { defineHandler, H3 } from 'h3'
import { lookup } from 'mrmime'
import { extname, join, normalize, resolve, sep } from 'pathe'

Expand Down Expand Up @@ -168,27 +168,21 @@ export function serveStaticHandler(
}

/**
* Mount {@link serveStaticHandler} on an h3 app at `base`, handling the
* route pattern and prefix-stripping required by h3 v2.
* Mount {@link serveStaticHandler} on an h3 app at `base`.
*
* h3 v2's `app.use(base, handler)` only matches the exact `base` path and
* does not strip the prefix from `event.url.pathname`. Static serving
* needs both subpath matching (`/base/**`) and the URL stripped so the
* file resolver sees paths relative to `dir` — this helper bundles both.
* h3's sub-app mount provides segment-boundary matching and strips `base`
* from `event.url.pathname`, so the file resolver sees paths relative to
* `dir`.
*/
export function mountStaticHandler(
app: H3,
base: string,
dir: string,
options?: ServeStaticOptions,
): void {
const trimmed = base.replace(/\/$/, '')
const handler = serveStaticHandler(dir, options)
if (trimmed === '') {
app.use('/**', handler)
return
}
app.use(`${trimmed}/**`, withBase(trimmed, handler))
const staticApp = new H3()
staticApp.use(serveStaticHandler(dir, options))
app.mount(base.replace(/\/$/, ''), staticApp)
}

/**
Expand Down
Loading