Skip to content

Commit

Permalink
feat: normalize route options and shortcuts (#583)
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 authored Oct 15, 2022
1 parent d5fa627 commit 3b7dc3f
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 87 deletions.
45 changes: 44 additions & 1 deletion src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { withLeadingSlash, withoutTrailingSlash, withTrailingSlash } from 'ufo'
import { isTest, isDebug } from 'std-env'
import { findWorkspaceDir } from 'pkg-types'
import { resolvePath, detectTarget } from './utils'
import type { NitroConfig, NitroOptions } from './types'
import type { NitroConfig, NitroOptions, NitroRouteConfig, NitroRouteOptions } from './types'
import { runtimeDir, pkgDir } from './dirs'
import * as PRESETS from './presets'
import { nitroImports } from './imports'
Expand Down Expand Up @@ -183,6 +183,49 @@ export async function loadOptions (configOverrides: NitroConfig = {}): Promise<N
})
}

// Normalize route rules (NitroRouteConfig => NitroRouteOptions)
const routes: { [p: string]: NitroRouteOptions } = {}
for (const path in options.routes) {
const routeConfig = options.routes[path] as NitroRouteConfig
const routeOptions: NitroRouteOptions = {
...routeConfig,
redirect: undefined
}
// Redirect
if (routeConfig.redirect) {
routeOptions.redirect = {
to: '/',
statusCode: 307,
...(typeof routeConfig.redirect === 'string' ? { to: routeConfig.redirect } : routeConfig.redirect)
}
}
// CORS
if (routeConfig.cors) {
routeOptions.headers = {
'access-control-allow-origin': '*',
'access-control-allowed-methods': '*',
'access-control-allow-headers': '*',
'access-control-max-age': '0',
...routeOptions.headers
}
}
// Cache: swr
if (routeConfig.swr) {
routeOptions.cache = routeOptions.cache || {}
routeOptions.cache.swr = true
if (typeof routeConfig.swr === 'number') {
routeOptions.cache.maxAge = routeConfig.swr
}
}
// Cache: static
if (routeConfig.static) {
routeOptions.cache = routeOptions.cache || {}
routeOptions.cache.static = true
}
routes[path] = routeOptions
}
options.routes = routes

options.baseURL = withLeadingSlash(withTrailingSlash(options.baseURL))
options.runtimeConfig = defu(options.runtimeConfig, {
app: {
Expand Down
32 changes: 13 additions & 19 deletions src/presets/netlify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,18 @@ async function writeRedirects (nitro: Nitro) {
const redirectsPath = join(nitro.options.output.publicDir, '_redirects')
let contents = '/* /.netlify/functions/server 200'

// Rewrite SWR and static paths to builder functions
for (const [key] of Object.entries(nitro.options.routes).filter(([_, value]) => value.swr || value.static)) {
// Rewrite static cached paths to builder functions
for (const [key] of Object.entries(nitro.options.routes)
.filter(([_, routeOptions]) => routeOptions.cache?.static || routeOptions.cache?.swr)
) {
contents = `${key.replace('/**', '/*')}\t/.netlify/builders/server 200\n` + contents
}

for (const [key, value] of Object.entries(nitro.options.routes).filter(([_, value]) => value.redirect)) {
const redirect = typeof value.redirect === 'string' ? { to: value.redirect } : value.redirect
// TODO: update to 307 when netlify support 307/308
contents = `${key.replace('/**', '/*')}\t${redirect.to}\t${redirect.statusCode || 301}\n` + contents
for (const [key, routeOptions] of Object.entries(nitro.options.routes).filter(([_, routeOptions]) => routeOptions.redirect)) {
// TODO: Remove map when netlify support 307/308
let code = routeOptions.redirect.statusCode
code = ({ 307: 302, 308: 301 })[code] || code
contents = `${key.replace('/**', '/*')}\t${routeOptions.redirect.to}\t${code}\n` + contents
}

if (existsSync(redirectsPath)) {
Expand All @@ -109,20 +112,11 @@ async function writeHeaders (nitro: Nitro) {
const headersPath = join(nitro.options.output.publicDir, '_headers')
let contents = ''

for (const [key, value] of Object.entries(nitro.options.routes).filter(([_, value]) => value.cors || value.headers)) {
for (const [path, routeOptions] of Object.entries(nitro.options.routes)
.filter(([_, routeOptions]) => routeOptions.headers)) {
const headers = [
key.replace('/**', '/*'),
...Object.entries({
...value.cors
? {
'access-control-allow-origin': '*',
'access-control-allowed-methods': '*',
'access-control-allow-headers': '*',
'access-control-max-age': '0'
}
: {},
...value.headers || {}
}).map(([header, value]) => ` ${header}: ${value}`)
path.replace('/**', '/*'),
...Object.entries({ ...routeOptions.headers }).map(([header, value]) => ` ${header}: ${value}`)
].join('\n')

contents += headers + '\n'
Expand Down
45 changes: 17 additions & 28 deletions src/presets/vercel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,34 +82,23 @@ function generateBuildConfig (nitro: Nitro) {
)
),
routes: [
...Object.entries(nitro.options.routes).filter(([_, value]) => value.redirect || value.headers || value.cors).map(([key, value]) => {
let route = {
src: key.replace('/**', '/.*')
}
if (value.redirect) {
const redirect = typeof value.redirect === 'string' ? { to: value.redirect } : value.redirect
route = defu(route, {
status: redirect.statusCode || 307,
headers: { Location: redirect.to }
})
}
if (value.cors) {
route = defu(route, {
headers: {
'access-control-allow-origin': '*',
'access-control-allowed-methods': '*',
'access-control-allow-headers': '*',
'access-control-max-age': '0'
}
})
}
if (value.headers) {
route = defu(route, {
headers: value.headers
})
}
return route
}),
...Object.entries(nitro.options.routes)
.filter(([_, routeOptions]) => routeOptions.redirect || routeOptions.headers)
.map(([path, routeOptions]) => {
let route = {
src: path.replace('/**', '/.*')
}
if (routeOptions.redirect) {
route = defu(route, {
status: routeOptions.redirect.statusCode,
headers: { Location: routeOptions.redirect.to }
})
}
if (routeOptions.headers) {
route = defu(route, { headers: routeOptions.headers })
}
return route
}),
...nitro.options.publicAssets
.filter(asset => !asset.fallthrough)
.map(asset => asset.baseURL)
Expand Down
5 changes: 3 additions & 2 deletions src/runtime/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ function createNitroApp (): NitroApp {

// Wrap matching handlers for caching route options
const routeOptions = getRouteOptionsForPath(h.route.replace(/:\w+|\*\*/g, '_'))
if (routeOptions.swr) {
if (routeOptions.cache) {
handler = cachedEventHandler(handler, {
group: 'nitro/routes'
group: 'nitro/routes',
...routeOptions.cache
})
}

Expand Down
1 change: 1 addition & 0 deletions src/runtime/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface CacheOptions<T = any> {
group?: string;
integrity?: any;
maxAge?: number;
static?: boolean; // TODO
swr?: boolean;
staleMaxAge?: number;
base?: string;
Expand Down
29 changes: 6 additions & 23 deletions src/runtime/route-options.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { eventHandler, H3Event, sendRedirect, setHeaders } from 'h3'
import defu from 'defu'
import { createRouter as createRadixRouter, toRouteMatcher } from 'radix3'
import { NitroRouteOptions } from '../types'
import { useRuntimeConfig } from './config'
import type { NitroRouteOptions } from 'nitropack'

const config = useRuntimeConfig()
const _routeOptionsMatcher = toRouteMatcher(createRadixRouter({ routes: config.nitro.routes }))
Expand All @@ -10,32 +11,18 @@ export function createRouteOptionsHandler () {
return eventHandler((event) => {
// Match route options against path
const routeOptions = getRouteOptions(event)

// Apply CORS options
if (routeOptions.cors) {
setHeaders(event, {
'access-control-allow-origin': '*',
'access-control-allowed-methods': '*',
'access-control-allow-headers': '*',
'access-control-max-age': '0'
})
}

// Apply headers options
if (routeOptions.headers) {
setHeaders(event, routeOptions.headers)
}
// Apply redirect options
if (routeOptions.redirect) {
if (typeof routeOptions.redirect === 'string') {
routeOptions.redirect = { to: routeOptions.redirect }
}
return sendRedirect(event, routeOptions.redirect.to, routeOptions.redirect.statusCode || 307)
return sendRedirect(event, routeOptions.redirect.to, routeOptions.redirect.statusCode)
}
})
}

export function getRouteOptions (event: H3Event) {
export function getRouteOptions (event: H3Event): NitroRouteOptions {
event.context._nitro = event.context._nitro || {}
if (!event.context._nitro.routeOptions) {
const path = new URL(event.req.url, 'http://localhost').pathname
Expand All @@ -44,10 +31,6 @@ export function getRouteOptions (event: H3Event) {
return event.context._nitro.routeOptions
}

export function getRouteOptionsForPath (path: string) {
const routeOptions: NitroRouteOptions = {}
for (const rule of _routeOptionsMatcher.matchAll(path)) {
Object.assign(routeOptions, rule)
}
return routeOptions
export function getRouteOptionsForPath (path: string): NitroRouteOptions {
return defu({}, ..._routeOptionsMatcher.matchAll(path).reverse())
}
31 changes: 22 additions & 9 deletions src/types/nitro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { Storage, BuiltinDriverName } from 'unstorage'
import type { NodeExternalsOptions } from '../rollup/plugins/externals'
import type { RollupConfig } from '../rollup/config'
import type { Options as EsbuildOptions } from '../rollup/plugins/esbuild'
import { CacheOptions } from '../runtime/types'
import type { NitroErrorHandler, NitroDevEventHandler, NitroEventHandler } from './handler'
import type { PresetOptions } from './presets'

Expand Down Expand Up @@ -62,16 +63,9 @@ type DeepPartial<T> = T extends Record<string, any> ? { [P in keyof T]?: DeepPar

export type NitroPreset = NitroConfig | (() => NitroConfig)

export interface NitroConfig extends DeepPartial<NitroOptions> {
export interface NitroConfig extends DeepPartial<Omit<NitroOptions, 'routes'>> {
extends?: string | string[] | NitroPreset
}

export interface NitroRouteOptions {
swr?: boolean | number
static?: boolean
redirect?: string | { to: string, statusCode?: 301 | 302 | 307 | 308 }
headers?: Record<string, string>
cors?: boolean
routes?: { [path: string]: NitroRouteConfig }
}

export interface PublicAssetDir {
Expand All @@ -95,6 +89,25 @@ export interface CompressOptions {
brotli?: boolean
}

type Enumerate<N extends number, Acc extends number[] = []> = Acc['length'] extends N ? Acc[number] : Enumerate<N, [...Acc, Acc['length']]>
type IntRange<F extends number, T extends number> = Exclude<Enumerate<T>, Enumerate<F>>
type HTTPStatusCode = IntRange<100, 600>

export interface NitroRouteConfig {
cache?: Exclude<CacheOptions, 'getKey' | 'transform'>
headers?: Record<string, string>
redirect?: string | { to: string, statusCode?: HTTPStatusCode }

// Shortcuts
cors?: boolean
swr?: boolean | number
static?: boolean | number
}

export interface NitroRouteOptions extends Omit<NitroRouteConfig, 'redirect' | 'cors' | 'swr' | 'static'> {
redirect?: { to: string, statusCode: HTTPStatusCode }
}

export interface NitroOptions extends PresetOptions {
// Internal
_config: NitroConfig
Expand Down
8 changes: 4 additions & 4 deletions test/presets/netlify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ describe('nitro:preset:netlify', async () => {
const redirects = await fsp.readFile(resolve(ctx.rootDir, 'dist/_redirects'), 'utf-8')
/* eslint-disable no-tabs */
expect(redirects).toMatchInlineSnapshot(`
"/rules/nested/override /other 301
/rules/nested/* /base 301
/rules/redirect/obj https://nitro.unjs.io/ 308
/rules/redirect /base 301
"/rules/nested/override /other 302
/rules/nested/* /base 302
/rules/redirect/obj https://nitro.unjs.io/ 301
/rules/redirect /base 302
/rules/static /.netlify/builders/server 200
/* /.netlify/functions/server 200"
`)
Expand Down
2 changes: 1 addition & 1 deletion test/presets/vercel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe('nitro:preset:vercel', async () => {
"headers": {
"access-control-allow-headers": "*",
"access-control-allow-origin": "*",
"access-control-allowed-methods": "*",
"access-control-allowed-methods": "GET",
"access-control-max-age": "0",
},
"src": "/rules/cors",
Expand Down

0 comments on commit 3b7dc3f

Please sign in to comment.