From fc161399a6757c769326a08c27fb684c67061f1e Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Sat, 15 Oct 2022 16:29:41 +0200 Subject: [PATCH 1/7] refactor: normalize `cors` route options to header options --- src/options.ts | 13 +++++++++++++ src/presets/netlify.ts | 12 +----------- src/presets/vercel.ts | 10 ---------- src/runtime/route-options.ts | 11 ----------- 4 files changed, 14 insertions(+), 32 deletions(-) diff --git a/src/options.ts b/src/options.ts index 562ce49a77..7c21341de6 100644 --- a/src/options.ts +++ b/src/options.ts @@ -183,6 +183,19 @@ export async function loadOptions (configOverrides: NitroConfig = {}): Promise value.cors || value.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}`) + ...Object.entries({ ...value.headers }).map(([header, value]) => ` ${header}: ${value}`) ].join('\n') contents += headers + '\n' diff --git a/src/presets/vercel.ts b/src/presets/vercel.ts index 6d58bca796..0031449418 100644 --- a/src/presets/vercel.ts +++ b/src/presets/vercel.ts @@ -93,16 +93,6 @@ function generateBuildConfig (nitro: Nitro) { 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 diff --git a/src/runtime/route-options.ts b/src/runtime/route-options.ts index 74d7503e6f..150ffa5f35 100644 --- a/src/runtime/route-options.ts +++ b/src/runtime/route-options.ts @@ -10,17 +10,6 @@ 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) From 20d3df726a8368b90dc03b48354b97e6f0ecbf70 Mon Sep 17 00:00:00 2001 From: pooya parsa Date: Sat, 15 Oct 2022 16:31:31 +0200 Subject: [PATCH 2/7] Update src/options.ts Co-authored-by: Daniel Roe --- src/options.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/options.ts b/src/options.ts index 7c21341de6..0b76d2071b 100644 --- a/src/options.ts +++ b/src/options.ts @@ -187,11 +187,11 @@ export async function loadOptions (configOverrides: NitroConfig = {}): Promise Date: Sat, 15 Oct 2022 16:33:18 +0200 Subject: [PATCH 3/7] update test --- test/presets/vercel.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/presets/vercel.test.ts b/test/presets/vercel.test.ts index fabbcba362..05e9b282e3 100644 --- a/test/presets/vercel.test.ts +++ b/test/presets/vercel.test.ts @@ -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", From eeb05e3adfc9c5a165940cf60f50c49c25359ac4 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Sat, 15 Oct 2022 18:14:47 +0200 Subject: [PATCH 4/7] normalize more --- src/options.ts | 41 ++++++++++++++++++++++++++++++------ src/presets/netlify.ts | 22 +++++++++++-------- src/presets/vercel.ts | 35 +++++++++++++++--------------- src/runtime/cache.ts | 1 + src/runtime/route-options.ts | 19 ++++++----------- src/types/nitro.ts | 31 +++++++++++++++++++-------- test/presets/netlify.test.ts | 8 +++---- 7 files changed, 99 insertions(+), 58 deletions(-) diff --git a/src/options.ts b/src/options.ts index 0b76d2071b..31149cf3d4 100644 --- a/src/options.ts +++ b/src/options.ts @@ -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' @@ -183,18 +183,47 @@ export async function loadOptions (configOverrides: NitroConfig = {}): Promise 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 = { + 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', - ...rule.headers + ...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, { diff --git a/src/presets/netlify.ts b/src/presets/netlify.ts index f8916a2839..9d8882659a 100644 --- a/src/presets/netlify.ts +++ b/src/presets/netlify.ts @@ -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)) { @@ -109,10 +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.headers }).map(([header, value]) => ` ${header}: ${value}`) + path.replace('/**', '/*'), + ...Object.entries({ ...routeOptions.headers }).map(([header, value]) => ` ${header}: ${value}`) ].join('\n') contents += headers + '\n' diff --git a/src/presets/vercel.ts b/src/presets/vercel.ts index 0031449418..69025c15c7 100644 --- a/src/presets/vercel.ts +++ b/src/presets/vercel.ts @@ -82,24 +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.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) diff --git a/src/runtime/cache.ts b/src/runtime/cache.ts index 312e7a90e8..428c046863 100644 --- a/src/runtime/cache.ts +++ b/src/runtime/cache.ts @@ -18,6 +18,7 @@ export interface CacheOptions { group?: string; integrity?: any; maxAge?: number; + static?: boolean; // TODO swr?: boolean; staleMaxAge?: number; base?: string; diff --git a/src/runtime/route-options.ts b/src/runtime/route-options.ts index 150ffa5f35..5ec795e3ed 100644 --- a/src/runtime/route-options.ts +++ b/src/runtime/route-options.ts @@ -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 })) @@ -16,15 +17,13 @@ export function createRouteOptionsHandler () { } // 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) + // @ts-ignore + 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 @@ -33,10 +32,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()) } diff --git a/src/types/nitro.ts b/src/types/nitro.ts index fabd6a7aa1..2e970859b7 100644 --- a/src/types/nitro.ts +++ b/src/types/nitro.ts @@ -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' @@ -62,16 +63,9 @@ type DeepPartial = T extends Record ? { [P in keyof T]?: DeepPar export type NitroPreset = NitroConfig | (() => NitroConfig) -export interface NitroConfig extends DeepPartial { +export interface NitroConfig extends DeepPartial> { extends?: string | string[] | NitroPreset -} - -export interface NitroRouteOptions { - swr?: boolean | number - static?: boolean - redirect?: string | { to: string, statusCode?: 301 | 302 | 307 | 308 } - headers?: Record - cors?: boolean + routes?: { [path: string]: NitroRouteConfig } } export interface PublicAssetDir { @@ -95,6 +89,25 @@ export interface CompressOptions { brotli?: boolean } +type Enumerate = Acc['length'] extends N ? Acc[number] : Enumerate +type IntRange = Exclude, Enumerate> +type HTTPStatusCode = IntRange<100, 600> + +export interface NitroRouteConfig { + cache?: Exclude + headers?: Record + redirect?: string | { to: string, statusCode?: HTTPStatusCode } + + // Shortcuts + cors?: boolean + swr?: boolean | number + static?: boolean | number +} + +export interface NitroRouteOptions extends Omit { + redirect?: { to: string, statusCode: HTTPStatusCode } +} + export interface NitroOptions extends PresetOptions { // Internal _config: NitroConfig diff --git a/test/presets/netlify.test.ts b/test/presets/netlify.test.ts index 8b53daa5e4..464b0bbaff 100644 --- a/test/presets/netlify.test.ts +++ b/test/presets/netlify.test.ts @@ -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" `) From 014f5e0c81a7d01c16efbc0ce4da481cb17f0aa5 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Sat, 15 Oct 2022 18:22:22 +0200 Subject: [PATCH 5/7] pass cache options --- src/runtime/app.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/runtime/app.ts b/src/runtime/app.ts index 2f72831c28..b356552711 100644 --- a/src/runtime/app.ts +++ b/src/runtime/app.ts @@ -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 }) } From 5edbbfaec31ce32548db47bdeb2ac12f11f7ed9f Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Sat, 15 Oct 2022 18:25:21 +0200 Subject: [PATCH 6/7] add default for to --- src/options.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/options.ts b/src/options.ts index 31149cf3d4..97b616cae0 100644 --- a/src/options.ts +++ b/src/options.ts @@ -194,6 +194,7 @@ export async function loadOptions (configOverrides: NitroConfig = {}): Promise Date: Sat, 15 Oct 2022 18:26:24 +0200 Subject: [PATCH 7/7] remove ts-ignore --- src/runtime/route-options.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/runtime/route-options.ts b/src/runtime/route-options.ts index 5ec795e3ed..869308d0c2 100644 --- a/src/runtime/route-options.ts +++ b/src/runtime/route-options.ts @@ -17,7 +17,6 @@ export function createRouteOptionsHandler () { } // Apply redirect options if (routeOptions.redirect) { - // @ts-ignore return sendRedirect(event, routeOptions.redirect.to, routeOptions.redirect.statusCode) } })