Skip to content

Commit

Permalink
feat(dev): check publicDir before app routes
Browse files Browse the repository at this point in the history
This is useful when an app route overlaps with a public file.
  • Loading branch information
aleclarson committed Aug 24, 2023
1 parent bfe374a commit 401e66b
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 39 deletions.
60 changes: 21 additions & 39 deletions src/bundle/runtime/bundle/server/servePublicDir.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,14 @@
import {
ServePublicFileOptions,
servePublicFile,
} from '@runtime/servePublicFile'
import etag from 'etag'
import fs from 'fs'
import { gray } from 'kleur/colors'
import * as mime from 'mrmime'
import path from 'path'
import runtimeConfig from '../config'
import { connect } from './connect'
import { debug } from './debug'

interface Options {
/** @default runtimeConfig.publicDir */
root?: string
/**
* When defined, only files matching this can be served
* by this middleware.
*/
include?: RegExp
/**
* When defined, files matching this cannot be served
* by this middleware.
*/
ignore?: RegExp
type Options = ServePublicFileOptions & {
/**
* Set the `max-age` Cache-Control directive. \
* Set to `Infinity` to use the `immutable` directive.
Expand All @@ -31,32 +20,25 @@ interface Options {

export function servePublicDir(options: Options = {}): connect.Middleware {
const cacheControl = resolveCacheControl(options)
const {
root: publicDir = runtimeConfig.publicDir,
include = /./,
ignore = /^$/,
} = options

return async function servePublicFile(req, res, next) {
const fileName = req.url.slice(runtimeConfig.base.length)
if (ignore.test(fileName) || !include.test(fileName)) {
return next()
}
try {
const content = fs.readFileSync(path.join(publicDir, fileName))
return async function servePublicDir(req, res, next) {
const file = servePublicFile(req.url, runtimeConfig, options)
if (file) {
debug(gray('read'), req.url)
res.writeHead(200, {
ETag: etag(content, { weak: true }),
'Content-Type': mime.lookup(req.url) || 'application/octet-stream',
'Cache-Control': cacheControl,
})
res.write(content)
res.end()
} catch (e: any) {
if (e.code == 'ENOENT' || e.code == 'EISDIR') {
return next()
const entityTag = etag(file.data, { weak: true })
if (req.headers['if-none-match'] == entityTag) {
res.statusCode = 304
} else {
res.writeHead(200, {
etag: entityTag,
'content-type': file.mime,
'cache-control': cacheControl,
})
res.write(file.data)
}
throw e
res.end()
} else {
next()
}
}
}
Expand Down
25 changes: 25 additions & 0 deletions src/dev/createDevApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import { renderErrorFallback } from '@runtime/app/errorFallback'
import { throttleRender } from '@runtime/app/throttleRender'
import { App, RenderPageOptions } from '@runtime/app/types'
import { RuntimeConfig } from '@runtime/config'
import { route } from '@runtime/routeHooks'
import { servePublicFile } from '@runtime/servePublicFile'
import { callPlugins } from '@utils/callPlugins'
import { throttle } from '@utils/throttle'
import { clearExports } from '@vm/moduleMap'
import createDebug from 'debug'
import etag from 'etag'
import os from 'os'
import path from 'path'
import { createHotReload } from './hotReload'
Expand Down Expand Up @@ -48,6 +51,7 @@ export async function createDevApp(
throttleRender({
onError: error => [null, error],
}),
servePublicDir(runtimeConfig),
]

return createApp(
Expand Down Expand Up @@ -187,3 +191,24 @@ function isolatePages(context: DevContext): App.Plugin {
}
}
}

/**
* If a custom route is configured to serve a pathname that overlaps a directory
* or file in the public directory, this plugin will prefer the public file.
*/
const servePublicDir =
(runtimeConfig: RuntimeConfig): App.Plugin =>
() => {
route(`:file`).get(async (req, headers) => {
const file = servePublicFile(req.path, runtimeConfig)
if (file) {
const entityTag = etag(file.data, { weak: true })
if (req.headers['if-none-match'] === entityTag) {
req.respondWith(304)
} else {
headers.etag(entityTag).contentType(file.mime)
req.respondWith(200, { buffer: file.data })
}
}
})
}
54 changes: 54 additions & 0 deletions src/runtime/servePublicFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import fs from 'fs'
import * as mime from 'mrmime'
import path from 'path'
import { RenderedFile } from './app'
import { RuntimeConfig } from './config'

export interface ServePublicFileOptions {
/** @default runtimeConfig.publicDir */
root?: string
/**
* When defined, only files matching this can be served
* by this middleware.
*/
include?: RegExp
/**
* When defined, files matching this cannot be served
* by this middleware.
*/
ignore?: RegExp
}

export type PublicFile = RenderedFile & {
data: Buffer
}

export function servePublicFile(
url: string,
runtimeConfig: RuntimeConfig,
options?: ServePublicFileOptions
): PublicFile | null {
const fileName = url.slice(runtimeConfig.base.length)
if (options?.ignore?.test(fileName)) {
return null
}
if (options?.include && !options.include.test(fileName)) {
return null
}
try {
const publicDir = options?.root || runtimeConfig.publicDir
const content = fs.readFileSync(path.join(publicDir, fileName))
return {
id: fileName,
data: content,
get mime() {
return mime.lookup(fileName) || 'application/octet-stream'
},
}
} catch (e: any) {
if (e.code == 'ENOENT' || e.code == 'EISDIR') {
return null
}
throw e
}
}

0 comments on commit 401e66b

Please sign in to comment.