diff --git a/packages/devframe/src/utils/serve-static.test.ts b/packages/devframe/src/utils/serve-static.test.ts index f2797be5..24ed9b08 100644 --- a/packages/devframe/src/utils/serve-static.test.ts +++ b/packages/devframe/src/utils/serve-static.test.ts @@ -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 @@ -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 diff --git a/packages/devframe/src/utils/serve-static.ts b/packages/devframe/src/utils/serve-static.ts index a555e906..2b71a589 100644 --- a/packages/devframe/src/utils/serve-static.ts +++ b/packages/devframe/src/utils/serve-static.ts @@ -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' @@ -168,13 +168,11 @@ 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, @@ -182,13 +180,9 @@ export function mountStaticHandler( 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) } /**