diff --git a/src/runtimes/node/in_source_config/index.ts b/src/runtimes/node/in_source_config/index.ts index ca9792493..7dfa8c9ef 100644 --- a/src/runtimes/node/in_source_config/index.ts +++ b/src/runtimes/node/in_source_config/index.ts @@ -131,7 +131,12 @@ export const parseSource = ( result.methods = normalizeMethods(configExport.method, functionName) } - result.routes = getRoutes(configExport.path, functionName, result.methods ?? []) + result.routes = getRoutes({ + functionName, + methods: result.methods ?? [], + path: configExport.path, + preferStatic: configExport.preferStatic === true, + }) return result } diff --git a/src/utils/routes.ts b/src/utils/routes.ts index 68da9c2d8..402f8d8db 100644 --- a/src/utils/routes.ts +++ b/src/utils/routes.ts @@ -4,7 +4,10 @@ import { FunctionBundlingUserError } from './error.js' import { nonNullable } from './non_nullable.js' import { ExtendedURLPattern } from './urlpattern.js' -export type Route = { pattern: string; methods: string[] } & ({ literal: string } | { expression: string }) +export type Route = { pattern: string; methods: string[]; prefer_static?: boolean } & ( + | { literal: string } + | { expression: string } +) // Based on https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API. const isExpression = (part: string) => @@ -18,11 +21,18 @@ const isPathLiteral = (path: string) => { return parts.every((part) => !isExpression(part)) } +interface GetRouteOption { + functionName: string + methods: string[] + path: unknown + preferStatic: boolean +} + /** * Takes an element from a `path` declaration and returns a Route element that * represents it. */ -const getRoute = (path: unknown, functionName: string, methods: string[]): Route | undefined => { +const getRoute = ({ functionName, methods, path, preferStatic }: GetRouteOption): Route | undefined => { if (typeof path !== 'string') { throw new FunctionBundlingUserError(`'path' property must be a string, found '${JSON.stringify(path)}'`, { functionName, @@ -38,7 +48,7 @@ const getRoute = (path: unknown, functionName: string, methods: string[]): Route } if (isPathLiteral(path)) { - return { pattern: path, literal: path, methods } + return { pattern: path, literal: path, methods, prefer_static: preferStatic || undefined } } try { @@ -53,7 +63,7 @@ const getRoute = (path: unknown, functionName: string, methods: string[]): Route // for both `/foo` and `/foo/`. const normalizedRegex = `^${regex}\\/?$` - return { pattern: path, expression: normalizedRegex, methods } + return { pattern: path, expression: normalizedRegex, methods, prefer_static: preferStatic || undefined } } catch { throw new FunctionBundlingUserError(`'${path}' is not a valid path according to the URLPattern specification`, { functionName, @@ -62,17 +72,38 @@ const getRoute = (path: unknown, functionName: string, methods: string[]): Route } } +interface GetRoutesOptions { + functionName: string + methods: string[] + path: unknown + preferStatic?: boolean +} + /** * Takes a `path` declaration, normalizes it into an array, and processes the * individual elements to obtain an array of `Route` expressions. */ -export const getRoutes = (input: unknown, functionName: string, methods: string[]): Route[] => { - if (!input) { +export const getRoutes = ({ + functionName, + methods, + path: pathOrPaths, + preferStatic = false, +}: GetRoutesOptions): Route[] => { + if (!pathOrPaths) { return [] } - const paths = [...new Set(Array.isArray(input) ? input : [input])] - const routes = paths.map((path) => getRoute(path, functionName, methods ?? [])).filter(nonNullable) + const paths = [...new Set(Array.isArray(pathOrPaths) ? pathOrPaths : [pathOrPaths])] + const routes = paths + .map((path) => + getRoute({ + functionName, + methods, + path, + preferStatic, + }), + ) + .filter(nonNullable) return routes } diff --git a/tests/unit/runtimes/node/in_source_config.test.ts b/tests/unit/runtimes/node/in_source_config.test.ts index 718040da6..2e31155b4 100644 --- a/tests/unit/runtimes/node/in_source_config.test.ts +++ b/tests/unit/runtimes/node/in_source_config.test.ts @@ -593,4 +593,68 @@ describe('V2 API', () => { expect(routes).toEqual([{ pattern: '/products', literal: '/products', methods: [] }]) }) }) + + describe('`preferStatic` property', () => { + test('Sets a `prefer_static` property on a single route', () => { + const source = `export default async () => { + return new Response("Hello!") + } + + export const config = { + path: "/products", + preferStatic: true + }` + + const { routes } = parseSource(source, options) + + expect(routes).toEqual([{ pattern: '/products', literal: '/products', methods: [], prefer_static: true }]) + }) + + test('Sets a `prefer_static` property on all routes', () => { + const source = `export default async () => { + return new Response("Hello!") + } + + export const config = { + path: ["/items", "/products"], + preferStatic: true + }` + + const { routes } = parseSource(source, options) + + expect(routes).toEqual([ + { pattern: '/items', literal: '/items', methods: [], prefer_static: true }, + { pattern: '/products', literal: '/products', methods: [], prefer_static: true }, + ]) + }) + + test('Does not set a `prefer_static` property if `preferStatic` is not a boolean', () => { + const source = `export default async () => { + return new Response("Hello!") + } + + export const config = { + path: "/products", + preferStatic: "yep" + }` + + const { routes } = parseSource(source, options) + + expect(routes).toEqual([{ pattern: '/products', literal: '/products', methods: [] }]) + }) + + test('Does not set a `prefer_static` property if `preferStatic` is not set', () => { + const source = `export default async () => { + return new Response("Hello!") + } + + export const config = { + path: "/products" + }` + + const { routes } = parseSource(source, options) + + expect(routes).toEqual([{ pattern: '/products', literal: '/products', methods: [] }]) + }) + }) })