From 4807def1651c587152e41e9896c1b4b466123448 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 28 Dec 2022 10:01:14 +0100 Subject: [PATCH 1/7] feat: move excluded_patterns into function_config --- node/bundler.test.ts | 4 ++-- node/manifest.test.ts | 8 ++++++-- node/manifest.ts | 12 ++++++++++-- node/validation/manifest/schema.ts | 17 +++++++++++++++++ 4 files changed, 35 insertions(+), 6 deletions(-) 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/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..703860f0 100644 --- a/node/manifest.ts +++ b/node/manifest.ts @@ -24,7 +24,9 @@ interface Route { function: string name?: string pattern: string - excluded_pattern?: string +} +interface EdgeFunctionConfig { + excluded_patterns: string[] } interface Manifest { bundler_version: string @@ -33,7 +35,9 @@ interface Manifest { layers: { name: string; flag: string }[] routes: Route[] post_cache_routes: Route[] + function_config: Record } + /* eslint-enable camelcase */ interface Route { @@ -53,6 +57,9 @@ const generateManifest = ({ }: GenerateManifestOptions) => { const preCacheRoutes: Route[] = [] const postCacheRoutes: Route[] = [] + const functionConfig: Manifest['function_config'] = Object.fromEntries( + functions.map(({ name }) => [name, { excluded_patterns: [] }]), + ) declarations.forEach((declaration) => { const func = functions.find(({ name }) => declaration.function === name) @@ -69,7 +76,7 @@ const generateManifest = ({ } const excludedPattern = getExcludedRegularExpression(declaration) if (excludedPattern) { - route.excluded_pattern = serializePattern(excludedPattern) + functionConfig[func.name].excluded_patterns.push(serializePattern(excludedPattern)) } if (declaration.cache === Cache.Manual) { @@ -89,6 +96,7 @@ const generateManifest = ({ bundler_version: getPackageVersion(), layers, import_map: importMap, + function_config: functionConfig, } return manifest diff --git a/node/validation/manifest/schema.ts b/node/validation/manifest/schema.ts index 2fa5d09d..bd55b8bc 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: ['excluded_patterns'], + 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, } From 5f8c135c4cee7909613290d9eb272c06210e051c Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 28 Dec 2022 10:45:09 +0100 Subject: [PATCH 2/7] feat: isc-defined excluded_patterns --- node/bundler.ts | 1 + node/config.test.ts | 14 +++-- node/config.ts | 1 + node/declaration.ts | 9 ++-- node/manifest.ts | 52 +++++++++---------- .../netlify/edge-functions/user-func5.ts | 6 +++ 6 files changed, 47 insertions(+), 36 deletions(-) create mode 100644 test/fixtures/with_config/netlify/edge-functions/user-func5.ts 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..b85b2bb2 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(7) + 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.ts b/node/manifest.ts index 703860f0..e122c09d 100644 --- a/node/manifest.ts +++ b/node/manifest.ts @@ -4,21 +4,13 @@ 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 @@ -40,6 +32,15 @@ interface Manifest { /* eslint-enable camelcase */ +interface GenerateManifestOptions { + bundles?: Bundle[] + declarations?: Declaration[] + functions: EdgeFunction[] + functionConfig?: Record + importMap?: string + layers?: Layer[] +} + interface Route { function: string name?: string @@ -52,15 +53,24 @@ const generateManifest = ({ bundles = [], declarations = [], functions, + functionConfig = {}, importMap, layers = [], }: GenerateManifestOptions) => { const preCacheRoutes: Route[] = [] const postCacheRoutes: Route[] = [] - const functionConfig: Manifest['function_config'] = Object.fromEntries( + 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) @@ -76,7 +86,7 @@ const generateManifest = ({ } const excludedPattern = getExcludedRegularExpression(declaration) if (excludedPattern) { - functionConfig[func.name].excluded_patterns.push(serializePattern(excludedPattern)) + manifestFunctionConfig[func.name].excluded_patterns.push(serializePattern(excludedPattern)) } if (declaration.cache === Cache.Manual) { @@ -96,7 +106,7 @@ const generateManifest = ({ bundler_version: getPackageVersion(), layers, import_map: importMap, - function_config: functionConfig, + function_config: manifestFunctionConfig, } return manifest @@ -133,24 +143,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/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', +} From 0fa20a9faee3ed274554d47db8cf298c2f8bbd3a Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Tue, 3 Jan 2023 16:48:57 +0100 Subject: [PATCH 3/7] feat: remove "excluded_patterns" from required fields --- node/validation/manifest/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/validation/manifest/schema.ts b/node/validation/manifest/schema.ts index bd55b8bc..6ecf2e59 100644 --- a/node/validation/manifest/schema.ts +++ b/node/validation/manifest/schema.ts @@ -26,7 +26,7 @@ const routesSchema = { const functionConfigSchema = { type: 'object', - required: ['excluded_patterns'], + required: [], properties: { excluded_patterns: { type: 'array', From 7c497533073b937262212acc4bf423685f1df562 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Tue, 3 Jan 2023 18:16:24 +0100 Subject: [PATCH 4/7] feat: only write functionConfig for functions that need it --- node/config.test.ts | 2 +- node/manifest.ts | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/node/config.test.ts b/node/config.test.ts index b85b2bb2..62124392 100644 --- a/node/config.test.ts +++ b/node/config.test.ts @@ -187,7 +187,7 @@ test('Loads function paths from the in-source `config` function', async () => { expect(postCacheRoutes.length).toBe(1) expect(postCacheRoutes[0]).toEqual({ function: 'user-func4', pattern: '^/user-func4/?$' }) - expect(Object.keys(functionConfig)).toHaveLength(7) + expect(Object.keys(functionConfig)).toHaveLength(1) expect(functionConfig['user-func5']).toEqual({ excluded_patterns: ['^/user-func5/excluded/?$'], }) diff --git a/node/manifest.ts b/node/manifest.ts index e122c09d..00da59f6 100644 --- a/node/manifest.ts +++ b/node/manifest.ts @@ -59,15 +59,18 @@ const generateManifest = ({ }: GenerateManifestOptions) => { const preCacheRoutes: Route[] = [] const postCacheRoutes: Route[] = [] - const manifestFunctionConfig: Manifest['function_config'] = Object.fromEntries( - functions.map(({ name }) => [name, { excluded_patterns: [] }]), - ) + const manifestFunctionConfig: Manifest['function_config'] = {} + + const getFunctionConfig = (name: string) => { + if (!manifestFunctionConfig[name]) manifestFunctionConfig[name] = { excluded_patterns: [] } + return manifestFunctionConfig[name] + } 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) + getFunctionConfig(name).excluded_patterns.push(...excludedPatterns) } } @@ -86,7 +89,7 @@ const generateManifest = ({ } const excludedPattern = getExcludedRegularExpression(declaration) if (excludedPattern) { - manifestFunctionConfig[func.name].excluded_patterns.push(serializePattern(excludedPattern)) + getFunctionConfig(func.name).excluded_patterns.push(serializePattern(excludedPattern)) } if (declaration.cache === Cache.Manual) { From 520a241a0dc85cc6f58231cc973f75a9be5b452c Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Thu, 5 Jan 2023 11:54:54 +0100 Subject: [PATCH 5/7] refactor: sanitize function config before saving --- node/manifest.ts | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/node/manifest.ts b/node/manifest.ts index 00da59f6..daa15dd3 100644 --- a/node/manifest.ts +++ b/node/manifest.ts @@ -49,6 +49,18 @@ interface Route { const serializePattern = (regex: RegExp) => regex.source.replace(/\\\//g, '/') +const sanitizeEdgeFunctionConfig = (config: Record): Record => { + const newConfig = { ...config } + + for (const [name, functionConfig] of Object.entries(newConfig)) { + if (functionConfig.excluded_patterns.length === 0) { + delete newConfig[name] + } + } + + return newConfig +} + const generateManifest = ({ bundles = [], declarations = [], @@ -59,18 +71,15 @@ const generateManifest = ({ }: GenerateManifestOptions) => { const preCacheRoutes: Route[] = [] const postCacheRoutes: Route[] = [] - const manifestFunctionConfig: Manifest['function_config'] = {} - - const getFunctionConfig = (name: string) => { - if (!manifestFunctionConfig[name]) manifestFunctionConfig[name] = { excluded_patterns: [] } - return manifestFunctionConfig[name] - } + const manifestFunctionConfig: Manifest['function_config'] = Object.fromEntries( + Object.keys(functionConfig).map((key) => [key, { 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) - getFunctionConfig(name).excluded_patterns.push(...excludedPatterns) + manifestFunctionConfig[name].excluded_patterns.push(...excludedPatterns) } } @@ -89,7 +98,7 @@ const generateManifest = ({ } const excludedPattern = getExcludedRegularExpression(declaration) if (excludedPattern) { - getFunctionConfig(func.name).excluded_patterns.push(serializePattern(excludedPattern)) + manifestFunctionConfig[func.name].excluded_patterns.push(serializePattern(excludedPattern)) } if (declaration.cache === Cache.Manual) { @@ -109,7 +118,7 @@ const generateManifest = ({ bundler_version: getPackageVersion(), layers, import_map: importMap, - function_config: manifestFunctionConfig, + function_config: sanitizeEdgeFunctionConfig(manifestFunctionConfig), } return manifest From cc1aa186f63b7fa4ea0134bf86c770340f6e4445 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Thu, 5 Jan 2023 12:01:40 +0100 Subject: [PATCH 6/7] fix: use right object to build config map --- node/manifest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/manifest.ts b/node/manifest.ts index daa15dd3..07fa9940 100644 --- a/node/manifest.ts +++ b/node/manifest.ts @@ -72,7 +72,7 @@ const generateManifest = ({ const preCacheRoutes: Route[] = [] const postCacheRoutes: Route[] = [] const manifestFunctionConfig: Manifest['function_config'] = Object.fromEntries( - Object.keys(functionConfig).map((key) => [key, { excluded_patterns: [] }]), + functions.map(({ name }) => [name, { excluded_patterns: [] }]), ) for (const [name, { excludedPath }] of Object.entries(functionConfig)) { From 699a05367ae86b26b3ddf4390a0d103fcad9eba6 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 6 Jan 2023 10:24:47 +0100 Subject: [PATCH 7/7] refactor: add instead of delete --- node/manifest.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/node/manifest.ts b/node/manifest.ts index 07fa9940..eb0a3159 100644 --- a/node/manifest.ts +++ b/node/manifest.ts @@ -50,11 +50,11 @@ interface Route { const serializePattern = (regex: RegExp) => regex.source.replace(/\\\//g, '/') const sanitizeEdgeFunctionConfig = (config: Record): Record => { - const newConfig = { ...config } + const newConfig: Record = {} - for (const [name, functionConfig] of Object.entries(newConfig)) { - if (functionConfig.excluded_patterns.length === 0) { - delete newConfig[name] + for (const [name, functionConfig] of Object.entries(config)) { + if (functionConfig.excluded_patterns.length !== 0) { + newConfig[name] = functionConfig } }