diff --git a/node/bundler.test.ts b/node/bundler.test.ts index 3353d0c4..b5f9530b 100644 --- a/node/bundler.test.ts +++ b/node/bundler.test.ts @@ -359,14 +359,14 @@ test('Loads declarations and import maps from the deploy configuration', async ( const manifestFile = await fs.readFile(resolve(distPath, 'manifest.json'), 'utf8') const manifest = JSON.parse(manifestFile) - const { bundles, routes } = manifest + const { bundles, function_config: functionConfig } = manifest expect(bundles.length).toBe(1) expect(bundles[0].format).toBe('eszip2') expect(generatedFiles.includes(bundles[0].asset)).toBe(true) // respects excludedPath from deploy config - expect(routes[1].excluded_pattern).toEqual('^/func2/skip/?$') + expect(functionConfig.func2).toEqual({ excluded_patterns: ['^/func2/skip/?$'] }) await cleanup() }) diff --git a/node/bundler.ts b/node/bundler.ts index 7c3fac25..692b1ba3 100644 --- a/node/bundler.ts +++ b/node/bundler.ts @@ -128,6 +128,7 @@ const bundle = async ( declarations, distDirectory, functions, + functionConfig: functionsWithConfig, importMap: importMapSpecifier, layers: deployConfig.layers, }) diff --git a/node/config.test.ts b/node/config.test.ts index b98f76b2..62124392 100644 --- a/node/config.test.ts +++ b/node/config.test.ts @@ -127,7 +127,7 @@ test('Ignores function paths from the in-source `config` function if the feature }) const generatedFiles = await fs.readdir(distPath) - expect(result.functions.length).toBe(6) + expect(result.functions.length).toBe(7) expect(generatedFiles.length).toBe(2) const manifestFile = await fs.readFile(resolve(distPath, 'manifest.json'), 'utf8') @@ -165,27 +165,33 @@ test('Loads function paths from the in-source `config` function', async () => { }) const generatedFiles = await fs.readdir(distPath) - expect(result.functions.length).toBe(6) + expect(result.functions.length).toBe(7) expect(generatedFiles.length).toBe(2) const manifestFile = await fs.readFile(resolve(distPath, 'manifest.json'), 'utf8') const manifest = JSON.parse(manifestFile) - const { bundles, routes, post_cache_routes: postCacheRoutes } = manifest + const { bundles, routes, post_cache_routes: postCacheRoutes, function_config: functionConfig } = manifest expect(bundles.length).toBe(1) expect(bundles[0].format).toBe('eszip2') expect(generatedFiles.includes(bundles[0].asset)).toBe(true) - expect(routes.length).toBe(5) + expect(routes.length).toBe(6) expect(routes[0]).toEqual({ function: 'framework-func2', pattern: '^/framework-func2/?$' }) expect(routes[1]).toEqual({ function: 'user-func2', pattern: '^/user-func2/?$' }) expect(routes[2]).toEqual({ function: 'framework-func1', pattern: '^/framework-func1/?$' }) expect(routes[3]).toEqual({ function: 'user-func1', pattern: '^/user-func1/?$' }) expect(routes[4]).toEqual({ function: 'user-func3', pattern: '^/user-func3/?$' }) + expect(routes[5]).toEqual({ function: 'user-func5', pattern: '^/user-func5/.*/?$' }) expect(postCacheRoutes.length).toBe(1) expect(postCacheRoutes[0]).toEqual({ function: 'user-func4', pattern: '^/user-func4/?$' }) + expect(Object.keys(functionConfig)).toHaveLength(1) + expect(functionConfig['user-func5']).toEqual({ + excluded_patterns: ['^/user-func5/excluded/?$'], + }) + await cleanup() }) diff --git a/node/config.ts b/node/config.ts index 41b1cec2..b3919f54 100644 --- a/node/config.ts +++ b/node/config.ts @@ -29,6 +29,7 @@ export const enum Cache { export interface FunctionConfig { cache?: Cache path?: string | string[] + excludedPath?: string | string[] } const getConfigExtractor = () => { diff --git a/node/declaration.ts b/node/declaration.ts index db65abe3..041e533e 100644 --- a/node/declaration.ts +++ b/node/declaration.ts @@ -44,13 +44,13 @@ export const getDeclarationsFromConfig = ( const paths = Array.isArray(config.path) ? config.path : [config.path] paths.forEach((path) => { - declarations.push({ ...declaration, ...config, path }) + declarations.push({ ...declaration, cache: config.cache, path }) }) // With an in-source config without a path, add the config to the declaration } else { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { path, ...rest } = config + const { path, excludedPath, ...rest } = config declarations.push({ ...declaration, ...rest }) } @@ -60,15 +60,14 @@ export const getDeclarationsFromConfig = ( // Finally, we must create declarations for functions that are not declared // in the TOML at all. for (const name in functionsConfig) { - const { ...config } = functionsConfig[name] - const { path } = functionsConfig[name] + const { cache, path } = functionsConfig[name] // If we have path specified create a declaration for each path if (!functionsVisited.has(name) && path) { const paths = Array.isArray(path) ? path : [path] paths.forEach((singlePath) => { - declarations.push({ ...config, function: name, path: singlePath }) + declarations.push({ cache, function: name, path: singlePath }) }) } } diff --git a/node/manifest.test.ts b/node/manifest.test.ts index a1ba2157..675002b6 100644 --- a/node/manifest.test.ts +++ b/node/manifest.test.ts @@ -64,11 +64,15 @@ test('Generates a manifest with excluded paths and patterns', () => { const manifest = generateManifest({ bundles: [], declarations, functions }) const expectedRoutes = [ - { function: 'func-1', name: 'Display Name', pattern: '^/f1/.*/?$', excluded_pattern: '^/f1/exclude/?$' }, - { function: 'func-2', pattern: '^/f2/.*/?$', excluded_pattern: '^/f2/exclude$' }, + { function: 'func-1', name: 'Display Name', pattern: '^/f1/.*/?$' }, + { function: 'func-2', pattern: '^/f2/.*/?$' }, ] expect(manifest.routes).toEqual(expectedRoutes) + expect(manifest.function_config).toEqual({ + 'func-1': { excluded_patterns: ['^/f1/exclude/?$'] }, + 'func-2': { excluded_patterns: ['^/f2/exclude$'] }, + }) expect(manifest.bundler_version).toBe(env.npm_package_version as string) }) diff --git a/node/manifest.ts b/node/manifest.ts index cfbcf346..eb0a3159 100644 --- a/node/manifest.ts +++ b/node/manifest.ts @@ -4,27 +4,21 @@ import { join } from 'path' import globToRegExp from 'glob-to-regexp' import type { Bundle } from './bundle.js' -import { Cache } from './config.js' +import { Cache, FunctionConfig } from './config.js' import type { Declaration } from './declaration.js' import { EdgeFunction } from './edge_function.js' import { Layer } from './layer.js' import { getPackageVersion } from './package_json.js' import { nonNullable } from './utils/non_nullable.js' -interface GenerateManifestOptions { - bundles?: Bundle[] - declarations?: Declaration[] - functions: EdgeFunction[] - importMap?: string - layers?: Layer[] -} - /* eslint-disable camelcase */ interface Route { function: string name?: string pattern: string - excluded_pattern?: string +} +interface EdgeFunctionConfig { + excluded_patterns: string[] } interface Manifest { bundler_version: string @@ -33,9 +27,20 @@ interface Manifest { layers: { name: string; flag: string }[] routes: Route[] post_cache_routes: Route[] + function_config: Record } + /* eslint-enable camelcase */ +interface GenerateManifestOptions { + bundles?: Bundle[] + declarations?: Declaration[] + functions: EdgeFunction[] + functionConfig?: Record + importMap?: string + layers?: Layer[] +} + interface Route { function: string name?: string @@ -44,15 +49,39 @@ interface Route { const serializePattern = (regex: RegExp) => regex.source.replace(/\\\//g, '/') +const sanitizeEdgeFunctionConfig = (config: Record): Record => { + const newConfig: Record = {} + + for (const [name, functionConfig] of Object.entries(config)) { + if (functionConfig.excluded_patterns.length !== 0) { + newConfig[name] = functionConfig + } + } + + return newConfig +} + const generateManifest = ({ bundles = [], declarations = [], functions, + functionConfig = {}, importMap, layers = [], }: GenerateManifestOptions) => { const preCacheRoutes: Route[] = [] const postCacheRoutes: Route[] = [] + const manifestFunctionConfig: Manifest['function_config'] = Object.fromEntries( + functions.map(({ name }) => [name, { excluded_patterns: [] }]), + ) + + for (const [name, { excludedPath }] of Object.entries(functionConfig)) { + if (excludedPath) { + const paths = Array.isArray(excludedPath) ? excludedPath : [excludedPath] + const excludedPatterns = paths.map(pathToRegularExpression).map(serializePattern) + manifestFunctionConfig[name].excluded_patterns.push(...excludedPatterns) + } + } declarations.forEach((declaration) => { const func = functions.find(({ name }) => declaration.function === name) @@ -69,7 +98,7 @@ const generateManifest = ({ } const excludedPattern = getExcludedRegularExpression(declaration) if (excludedPattern) { - route.excluded_pattern = serializePattern(excludedPattern) + manifestFunctionConfig[func.name].excluded_patterns.push(serializePattern(excludedPattern)) } if (declaration.cache === Cache.Manual) { @@ -89,6 +118,7 @@ const generateManifest = ({ bundler_version: getPackageVersion(), layers, import_map: importMap, + function_config: sanitizeEdgeFunctionConfig(manifestFunctionConfig), } return manifest @@ -125,24 +155,12 @@ const getExcludedRegularExpression = (declaration: Declaration) => { } } -interface WriteManifestOptions { - bundles: Bundle[] - declarations: Declaration[] +interface WriteManifestOptions extends GenerateManifestOptions { distDirectory: string - functions: EdgeFunction[] - importMap?: string - layers?: Layer[] } -const writeManifest = async ({ - bundles, - declarations = [], - distDirectory, - functions, - importMap, - layers, -}: WriteManifestOptions) => { - const manifest = generateManifest({ bundles, declarations, functions, importMap, layers }) +const writeManifest = async ({ distDirectory, ...rest }: WriteManifestOptions) => { + const manifest = generateManifest(rest) const manifestPath = join(distDirectory, 'manifest.json') await fs.writeFile(manifestPath, JSON.stringify(manifest)) diff --git a/node/validation/manifest/schema.ts b/node/validation/manifest/schema.ts index 2fa5d09d..6ecf2e59 100644 --- a/node/validation/manifest/schema.ts +++ b/node/validation/manifest/schema.ts @@ -24,6 +24,22 @@ const routesSchema = { additionalProperties: false, } +const functionConfigSchema = { + type: 'object', + required: [], + properties: { + excluded_patterns: { + type: 'array', + items: { + type: 'string', + format: 'regexPattern', + errorMessage: + 'excluded_patterns needs to be an array of regex that starts with ^ and ends with $ without any additional slashes before and afterwards', + }, + }, + }, +} + const layersSchema = { type: 'object', required: ['flag', 'name'], @@ -57,6 +73,7 @@ const edgeManifestSchema = { }, import_map: { type: 'string' }, bundler_version: { type: 'string' }, + function_config: { type: 'object', items: functionConfigSchema }, }, additionalProperties: false, } diff --git a/test/fixtures/with_config/netlify/edge-functions/user-func5.ts b/test/fixtures/with_config/netlify/edge-functions/user-func5.ts new file mode 100644 index 00000000..f129f9a1 --- /dev/null +++ b/test/fixtures/with_config/netlify/edge-functions/user-func5.ts @@ -0,0 +1,6 @@ +export default async () => new Response('Hello from user function 5.') + +export const config = { + path: '/user-func5/*', + excludedPath: '/user-func5/excluded', +}