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
20 changes: 6 additions & 14 deletions examples/devframe-files-inspector/tests/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import {
createHostContext,
startHttpAndWs,
} from 'devframe/node'
import { serveStaticHandler } from 'devframe/utils/serve-static'
import { mountStaticHandler } from 'devframe/utils/serve-static'
import { getPort } from 'get-port-please'
import { createApp, eventHandler } from 'h3'
import { H3 } from 'h3'
import { resolve } from 'pathe'
import devframe from '../src/devframe'

Expand Down Expand Up @@ -66,30 +66,22 @@ export async function startInspectorServer(
const host = '127.0.0.1'
const port = await getPort({ host, random: true })

const app = createApp()
const app = new H3()
const origin = `http://${host}:${port}`
const h3Host = createH3DevToolsHost({
origin,
appName: devframe.id,
mount: (base, dir) => {
app.use(base, serveStaticHandler(dir))
mountStaticHandler(app, base, dir)
},
})

const ctx = await createHostContext({ cwd, mode: 'dev', host: h3Host })
await devframe.setup(ctx)

const metaPath = `${basePath}${DEVTOOLS_CONNECTION_META_FILENAME}`
app.use(
metaPath,
eventHandler((event) => {
event.node.res.setHeader('Content-Type', 'application/json')
return event.node.res.end(
JSON.stringify({ backend: 'websocket', websocket: port }),
)
}),
)
app.use(basePath, serveStaticHandler(resolve(distDir)))
app.use(metaPath, () => ({ backend: 'websocket', websocket: port }))
mountStaticHandler(app, basePath, resolve(distDir))

const server = await startHttpAndWs({
context: ctx,
Expand Down
10 changes: 5 additions & 5 deletions examples/devframe-files-inspector/tests/static-serve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { createServer } from 'node:http'
import os from 'node:os'
import path from 'node:path'
import { createBuild } from 'devframe/adapters/build'
import { serveStaticHandler } from 'devframe/utils/serve-static'
import { mountStaticHandler } from 'devframe/utils/serve-static'
import { getPort } from 'get-port-please'
import { createApp, toNodeListener } from 'h3'
import { H3, toNodeHandler } from 'h3'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import devframe from '../src/devframe'
import { assertClientBuilt, makeFixtureCwd } from './_utils'
Expand All @@ -20,9 +20,9 @@ interface StaticServer {
async function startStaticServer(outDir: string, mountBase: string): Promise<StaticServer> {
const host = '127.0.0.1'
const port = await getPort({ host, random: true })
const app = createApp()
app.use(mountBase, serveStaticHandler(outDir))
const httpServer = createServer(toNodeListener(app))
const app = new H3()
mountStaticHandler(app, mountBase, outDir)
const httpServer = createServer(toNodeHandler(app))
await new Promise<void>(r => httpServer.listen(port, host, () => r()))
return {
origin: `http://${host}:${port}`,
Expand Down
21 changes: 6 additions & 15 deletions examples/devframe-streaming-chat/tests/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import {
createHostContext,
startHttpAndWs,
} from 'devframe/node'
import { serveStaticHandler } from 'devframe/utils/serve-static'
import { mountStaticHandler } from 'devframe/utils/serve-static'
import { getPort } from 'get-port-please'
import { createApp, eventHandler } from 'h3'
import { H3 } from 'h3'
import { resolve } from 'pathe'
import devframe from '../src/devframe'

Expand All @@ -39,30 +39,21 @@ export async function startStreamingChatServer(): Promise<StartedServer & {
const host = '127.0.0.1'
const port = await getPort({ host, random: true })

const app = createApp()
const app = new H3()
const origin = `http://${host}:${port}`
const h3Host = createH3DevToolsHost({
origin,
appName: devframe.id,
mount: (base, dir) =>
app.use(base, serveStaticHandler(dir)),
mount: (base, dir) => mountStaticHandler(app, base, dir),
})

const ctx = await createHostContext({ cwd: process.cwd(), mode: 'dev', host: h3Host })
await devframe.setup(ctx)

const metaPath = `${basePath}${DEVTOOLS_CONNECTION_META_FILENAME}`
app.use(
metaPath,
eventHandler((event) => {
event.node.res.setHeader('Content-Type', 'application/json')
return event.node.res.end(
JSON.stringify({ backend: 'websocket', websocket: port }),
)
}),
)
app.use(metaPath, () => ({ backend: 'websocket', websocket: port }))
if (existsSync(path.join(resolve(distDir), 'index.html'))) {
app.use(basePath, serveStaticHandler(resolve(distDir)))
mountStaticHandler(app, basePath, resolve(distDir))
}

const server = await startHttpAndWs({
Expand Down
2 changes: 1 addition & 1 deletion packages/devframe/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "devframe",
"type": "module",
"version": "0.1.22",
"version": "0.2.0",
"description": "Framework for building generic DevTools",
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
"license": "MIT",
Expand Down
4 changes: 2 additions & 2 deletions packages/devframe/src/adapters/cli.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { CAC } from 'cac'
import type { App } from 'h3'
import type { H3 } from 'h3'
import type { DevframeDefinition } from '../types/devframe'
import process from 'node:process'
import cac from 'cac'
Expand All @@ -24,7 +24,7 @@ export interface CreateCliOptions {
* Called once the dev server is listening. Use this to print a
* startup banner or trigger side-effects that depend on the live URL.
*/
onReady?: (info: { origin: string, port: number, app: App }) => void | Promise<void>
onReady?: (info: { origin: string, port: number, app: H3 }) => void | Promise<void>
}

export interface CliHandle {
Expand Down
20 changes: 8 additions & 12 deletions packages/devframe/src/adapters/dev.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import type { App } from 'h3'
import type { StartedServer } from '../node/server'
import type { DevframeDefinition, DevframeSetupInfo } from '../types/devframe'
import process from 'node:process'
import { getPort } from 'get-port-please'
import { createApp, eventHandler } from 'h3'
import { H3 } from 'h3'
import { resolve } from 'pathe'
import { DEVTOOLS_CONNECTION_META_FILENAME } from '../constants'
import { createHostContext } from '../node/context'
import { createH3DevToolsHost } from '../node/host-h3'
import { startHttpAndWs } from '../node/server'
import { open } from '../utils/open'
import { serveStaticHandler } from '../utils/serve-static'
import { mountStaticHandler } from '../utils/serve-static'
import { normalizeBasePath, resolveBasePath } from './_shared'

const DEFAULT_PORT = 9999
Expand Down Expand Up @@ -49,7 +48,7 @@ export interface CreateDevServerOptions {
* middleware (auth, logging, extra static assets) before devframe's
* own handlers.
*/
app?: App
app?: H3
/**
* Auto-open the browser. When `undefined` the resolution falls
* through to `flags.open` (incl. string path) and finally
Expand All @@ -61,7 +60,7 @@ export interface CreateDevServerOptions {
* Called once the WS server is bound. Devframe stays headless
* otherwise — wire this if you want a startup banner.
*/
onReady?: (info: { origin: string, port: number, app: App }) => void | Promise<void>
onReady?: (info: { origin: string, port: number, app: H3 }) => void | Promise<void>
}

export interface ResolveDevServerPortOptions {
Expand Down Expand Up @@ -123,14 +122,14 @@ export async function createDevServer(
const port = options.port ?? await resolveDevServerPort(def, { host })
const flags = options.flags ?? {}
const basePath = options.basePath ? normalizeBasePath(options.basePath) : resolveBasePath(def, 'standalone')
const app = options.app ?? createApp()
const app = options.app ?? new H3()
const origin = `http://${host}:${port}`

const h3Host = createH3DevToolsHost({
origin,
appName: def.id,
mount: (base, dir) => {
app.use(base, serveStaticHandler(dir))
mountStaticHandler(app, base, dir)
},
})

Expand All @@ -148,13 +147,10 @@ export async function createDevServer(
// sits at the SPA root (next to index.html) so the deployed SPA can
// discover it via a relative `./__connection.json` fetch.
const connectionMetaPath = `${basePath}${DEVTOOLS_CONNECTION_META_FILENAME}`
app.use(connectionMetaPath, eventHandler((event) => {
event.node.res.setHeader('Content-Type', 'application/json')
return event.node.res.end(JSON.stringify({ backend: 'websocket', websocket: port }))
}))
app.use(connectionMetaPath, () => ({ backend: 'websocket', websocket: port }))

if (distDir)
app.use(basePath, serveStaticHandler(resolve(distDir)))
mountStaticHandler(app, basePath, resolve(distDir))

return startHttpAndWs({
context: ctx,
Expand Down
13 changes: 6 additions & 7 deletions packages/devframe/src/node/server.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import type { BirpcGroup } from 'birpc'
import type { DevToolsNodeContext, DevToolsNodeRpcSession, DevToolsRpcClientFunctions, DevToolsRpcServerFunctions } from 'devframe/types'
import type { App } from 'h3'
import type { WebSocketServer } from 'ws'
import type { RpcFunctionsHost } from './host-functions'
import { AsyncLocalStorage } from 'node:async_hooks'
import { createServer } from 'node:http'
import { createRpcServer } from 'devframe/rpc/server'
import { attachWsRpcTransport } from 'devframe/rpc/transports/ws-server'
import { createApp, toNodeListener } from 'h3'
import { H3, toNodeHandler } from 'h3'
import { WebSocketServer as WSServer } from 'ws'

export interface StartHttpAndWsOptions {
Expand All @@ -19,7 +18,7 @@ export interface StartHttpAndWsOptions {
* when provided, callers can add their own routes (static handlers,
* auth middleware, etc.) first.
*/
app?: App
app?: H3
/**
* When `false`, the RPC server is started without a trust handshake.
* Intended for single-user localhost tools where an auth round-trip
Expand All @@ -36,14 +35,14 @@ export interface StartHttpAndWsOptions {
* handlers whose origin depends on the resolved port, or print their
* own startup banner. Devframe does not print one itself.
*/
onReady?: (info: { origin: string, port: number, app: App }) => void | Promise<void>
onReady?: (info: { origin: string, port: number, app: H3 }) => void | Promise<void>
}

export interface StartedServer {
/** Listening origin, e.g. `http://localhost:9999`. */
origin: string
port: number
app: App
app: H3
wss: WebSocketServer
rpcGroup: BirpcGroup<DevToolsRpcClientFunctions, DevToolsRpcServerFunctions, false>
close: () => Promise<void>
Expand All @@ -57,8 +56,8 @@ export interface StartedServer {
export async function startHttpAndWs(options: StartHttpAndWsOptions): Promise<StartedServer> {
const { context, port } = options
const bindHost = options.host ?? 'localhost'
const app = options.app ?? createApp()
const httpServer = createServer(toNodeListener(app))
const app = options.app ?? new H3()
const httpServer = createServer(toNodeHandler(app))
const wss = new WSServer({ server: httpServer })
const rpcHost = context.rpc as unknown as RpcFunctionsHost

Expand Down
6 changes: 3 additions & 3 deletions packages/devframe/src/utils/serve-static.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
import { createServer } from 'node:http'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { createApp, toNodeListener } from 'h3'
import { H3, toNodeHandler } from 'h3'
import { afterEach, describe, expect, it } from 'vitest'
import { serveStaticHandler, serveStaticNodeMiddleware } from './serve-static'

Expand All @@ -19,9 +19,9 @@ function makeTmp(prefix = 'devframe-serve-'): string {
}

async function startH3(dir: string, options?: ServeStaticOptions): Promise<Fixture> {
const app = createApp()
const app = new H3()
app.use(serveStaticHandler(dir, options))
const server = createServer(toNodeListener(app))
const server = createServer(toNodeHandler(app))
await new Promise<void>(r => server.listen(0, '127.0.0.1', r))
const port = (server.address() as AddressInfo).port
return {
Expand Down
72 changes: 51 additions & 21 deletions packages/devframe/src/utils/serve-static.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { EventHandler, EventHandlerRequest } from 'h3'
import type { EventHandler, H3 } from 'h3'
import type { IncomingMessage, ServerResponse } from 'node:http'
import { createReadStream } from 'node:fs'
import { stat } from 'node:fs/promises'
import { defineEventHandler, sendStream, setResponseHeader, setResponseStatus } from 'h3'
import { Readable } from 'node:stream'
import { defineHandler, withBase } from 'h3'
import { lookup } from 'mrmime'
import { extname, join, normalize, resolve, sep } from 'pathe'

Expand Down Expand Up @@ -105,11 +106,18 @@ function contentTypeFor(abs: string): string {
return type
}

function setStaticHeaders(res: ServerResponse, file: ResolvedFile): void {
res.setHeader('Content-Type', contentTypeFor(file.abs))
res.setHeader('Content-Length', file.size)
res.setHeader('Last-Modified', file.mtime.toUTCString())
res.setHeader('Cache-Control', 'no-store')
function staticHeadersFor(file: ResolvedFile): Record<string, string> {
return {
'Content-Type': contentTypeFor(file.abs),
'Content-Length': String(file.size),
'Last-Modified': file.mtime.toUTCString(),
'Cache-Control': 'no-store',
}
}

function applyStaticHeadersToNode(res: ServerResponse, file: ResolvedFile): void {
for (const [k, v] of Object.entries(staticHeadersFor(file)))
res.setHeader(k, v)
}

interface NormalizedOptions {
Expand All @@ -136,31 +144,53 @@ function normalizeOptions(options: ServeStaticOptions | undefined): NormalizedOp
export function serveStaticHandler(
dir: string,
options?: ServeStaticOptions,
): EventHandler<EventHandlerRequest> {
): EventHandler {
const absDir = resolve(dir)
const opts = normalizeOptions(options)
return defineEventHandler(async (event) => {
const method = event.node.req.method
return defineHandler(async (event) => {
const method = event.req.method
if (method !== 'GET' && method !== 'HEAD') {
setResponseStatus(event, 405)
setResponseHeader(event, 'Allow', 'GET, HEAD')
event.res.status = 405
event.res.headers.set('Allow', 'GET, HEAD')
return ''
}
const url = event.node.req.url ?? '/'
const file = await resolveTarget(absDir, url, opts.indexNames, opts.single)
const file = await resolveTarget(absDir, event.url.pathname, opts.indexNames, opts.single)
if (!file) {
setResponseStatus(event, 404)
event.res.status = 404
return ''
}
setStaticHeaders(event.node.res, file)
if (method === 'HEAD') {
event.node.res.end()
for (const [k, v] of Object.entries(staticHeadersFor(file)))
event.res.headers.set(k, v)
if (method === 'HEAD')
return ''
}
return sendStream(event, createReadStream(file.abs))
return Readable.toWeb(createReadStream(file.abs)) as ReadableStream
})
}

/**
* Mount {@link serveStaticHandler} on an h3 app at `base`, handling the
* route pattern and prefix-stripping required by h3 v2.
*
* 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.
*/
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))
}

/**
* Connect/Express-style Node middleware variant of {@link serveStaticHandler}.
*
Expand Down Expand Up @@ -198,7 +228,7 @@ export function serveStaticNodeMiddleware(
res.end()
return
}
setStaticHeaders(res, file)
applyStaticHeadersToNode(res, file)
if (method === 'HEAD') {
res.end()
return
Expand Down
Loading
Loading