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
12 changes: 12 additions & 0 deletions packages/edge-bundler/node/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,18 @@ test('Loads function paths from the in-source `config` function', async () => {
pattern: '^/user-func1/?$',
excluded_patterns: [],
path: '/user-func1',
headers: {
'x-must-be-there': {
style: 'exists',
},
'x-must-match': {
pattern: '^(foo|bar)$',
style: 'regex',
},
'x-must-not-be-there': {
style: 'missing',
},
},
})
expect(routes[7]).toEqual({
function: 'user-func3',
Expand Down
3 changes: 3 additions & 0 deletions packages/edge-bundler/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,11 @@ export const isValidOnError = (value: unknown): value is OnError => {
return value === 'fail' || value === 'bypass' || value.startsWith('/')
}

export type HeadersConfig = Record<string, boolean | string>

interface BaseFunctionConfig {
cache?: Cache
header?: HeadersConfig
onError?: OnError
name?: string
generator?: string
Expand Down
43 changes: 40 additions & 3 deletions packages/edge-bundler/node/declaration.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { FunctionConfig, FunctionConfigWithAllPossibleFields, HTTPMethod, Path } from './config.js'
import { BundleError } from './bundle_error.js'
import { FunctionConfig, FunctionConfigWithAllPossibleFields, HeadersConfig, HTTPMethod, Path } from './config.js'
import { FeatureFlags } from './feature_flags.js'

export type HeaderMatch = { pattern: string; style: 'regex' } | { style: 'exists' | 'missing' }
type HeaderMatchers = Record<string, HeaderMatch>

interface BaseDeclaration {
cache?: string
function: string
header?: HeadersConfig
method?: HTTPMethod | HTTPMethod[]
// todo: remove these two after a while and only support in-source config for non-route related configs
name?: string
Expand Down Expand Up @@ -104,7 +109,7 @@ const createDeclarationsFromFunctionConfigs = (
if (!functionsVisited.has(name)) {
// If we have a pattern specified, create a declaration for each pattern.
if ('pattern' in functionConfig && functionConfig.pattern) {
const { pattern, excludedPattern } = functionConfig
const { header, pattern, excludedPattern } = functionConfig
const patterns = Array.isArray(pattern) ? pattern : [pattern]
patterns.forEach((singlePattern) => {
const declaration: Declaration = { function: name, pattern: singlePattern }
Expand All @@ -117,12 +122,15 @@ const createDeclarationsFromFunctionConfigs = (
if (excludedPattern) {
declaration.excludedPattern = excludedPattern
}
if (header) {
declaration.header = header
}
declarations.push(declaration)
})
}
// If we don't have a pattern but we have a path specified, create a declaration for each path.
else if ('path' in functionConfig && functionConfig.path) {
const { path, excludedPath } = functionConfig
const { header, path, excludedPath } = functionConfig
const paths = Array.isArray(path) ? path : [path]

paths.forEach((singlePath) => {
Expand All @@ -136,6 +144,9 @@ const createDeclarationsFromFunctionConfigs = (
if (excludedPath) {
declaration.excludedPath = excludedPath
}
if (header) {
declaration.header = header
}
declarations.push(declaration)
})
}
Expand Down Expand Up @@ -165,3 +176,29 @@ export const normalizePattern = (pattern: string) => {
// Strip leading and forward slashes.
return regexp.toString().slice(1, -1)
}

const headerConfigError = `The 'header' configuration property must be an object where keys are strings and values are either booleans or strings (e.g. { "x-header-present": true, "x-header-absent": false, "x-header-matching": "^prefix" }).`

export const getHeaderMatchers = (headers?: HeadersConfig): HeaderMatchers => {
const matchers: HeaderMatchers = {}

if (!headers) {
return matchers
}

if (Object.getPrototypeOf(headers) !== Object.prototype) {
throw new BundleError(new Error(headerConfigError))
}

for (const header in headers) {
if (typeof headers[header] === 'boolean') {
matchers[header] = { style: headers[header] ? 'exists' : 'missing' }
} else if (typeof headers[header] === 'string') {
matchers[header] = { style: 'regex', pattern: normalizePattern(headers[header]) }
} else {
throw new BundleError(new Error(headerConfigError))
}
}

return matchers
}
96 changes: 95 additions & 1 deletion packages/edge-bundler/node/manifest.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test, expect } from 'vitest'
import { describe, test, expect } from 'vitest'

// @ts-expect-error current tsconfig.json doesn't allow this, but I don't want to change it
import { version } from '../package.json' assert { type: 'json' }
Expand Down Expand Up @@ -572,3 +572,97 @@ test('Generates a manifest with rewrite config', () => {
expect(manifest.routes).toEqual(expectedRoutes)
expect(manifest.function_config).toEqual(expectedFunctionConfig)
})

describe('Header matching', () => {
test('Throws a bundling error if the type is incorrect', () => {
const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }]

expect(() =>
generateManifest({
bundles: [],

// @ts-expect-error Incorrect type
declarations: [{ function: 'func-1', path: '/f1/*', header: 'foo' }],
functions,
}),
).toThrowError(BundleError)

expect(() =>
generateManifest({
bundles: [],

declarations: [
{
function: 'func-1',
path: '/f1/*',
header: {
'x-correct': true,

// @ts-expect-error Incorrect type
'x-not-correct': {
problem: true,
},
},
},
],
functions,
}),
).toThrowError(BundleError)
})

test('Writes header matching rules to the manifest', () => {
const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }]
const declarations: Declaration[] = [
{
function: 'func-1',
path: '/f1/*',
header: {
'x-present': true,
'x-also-present': true,
'x-absent': false,
'x-match-prefix': '^prefix(.*)',
'x-match-exact': 'exact',
'x-match-suffix': '(.*)suffix$',
},
},
]
const { manifest } = generateManifest({
bundles: [],
declarations,
functions,
})

const expectedRoutes = [
{
function: 'func-1',
pattern: '^/f1(?:/(.*))/?$',
excluded_patterns: [],
path: '/f1/*',
headers: {
'x-absent': {
style: 'missing',
},
'x-also-present': {
style: 'exists',
},
'x-match-exact': {
pattern: '^exact$',
style: 'regex',
},
'x-match-prefix': {
pattern: '^prefix(.*)$',
style: 'regex',
},
'x-match-suffix': {
pattern: '^(.*)suffix$',
style: 'regex',
},
'x-present': {
style: 'exists',
},
},
},
]
expect(manifest.routes).toEqual(expectedRoutes)
})
})
7 changes: 6 additions & 1 deletion packages/edge-bundler/node/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { join } from 'path'
import type { Bundle } from './bundle.js'
import { wrapBundleError } from './bundle_error.js'
import { Cache, FunctionConfig, FunctionConfigWithAllPossibleFields, Path } from './config.js'
import { Declaration, normalizePattern } from './declaration.js'
import { Declaration, type HeaderMatch, getHeaderMatchers, normalizePattern } from './declaration.js'
import { EdgeFunction } from './edge_function.js'
import { FeatureFlags } from './feature_flags.js'
import { Layer } from './layer.js'
Expand All @@ -15,6 +15,7 @@ import { ExtendedURLPattern } from './utils/urlpattern.js'

interface Route {
function: string
headers?: Record<string, HeaderMatch>
pattern: string
excluded_patterns: string[]
path?: string
Expand Down Expand Up @@ -232,6 +233,10 @@ const generateManifest = ({
route.methods = normalizeMethods(declaration.method, func.name)
}

if ('header' in declaration) {
route.headers = getHeaderMatchers(declaration.header)
}

if ('path' in declaration) {
route.path = declaration.path
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,10 @@ export default async () => {
}

export const config: Config = {
header: {
"x-must-be-there": true,
"x-must-not-be-there": false,
"x-must-match": "^(foo|bar)$",
},
path: '/user-func1',
}
Loading