From 5981a35880065a0cf1cd4b9467d47852a6089d6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Thu, 20 Jun 2024 10:55:51 +0100 Subject: [PATCH] feat: support `excludedPath` in functions (#6717) --- src/lib/functions/netlify-function.ts | 40 ++++++++++++++----- .../functions/runtimes/js/builders/zisi.ts | 13 +++++- src/utils/deploy/hash-fns.ts | 1 + .../functions/custom-path-excluded.js | 6 +++ tests/integration/commands/dev/v2-api.test.ts | 11 +++++ 5 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 tests/integration/__fixtures__/dev-server-with-v2-functions/functions/custom-path-excluded.js diff --git a/src/lib/functions/netlify-function.ts b/src/lib/functions/netlify-function.ts index 8cbc6dc7001..ad93da9b610 100644 --- a/src/lib/functions/netlify-function.ts +++ b/src/lib/functions/netlify-function.ts @@ -2,6 +2,7 @@ import { Buffer } from 'buffer' import { basename, extname } from 'path' import { version as nodeVersion } from 'process' +import type { ExtendedRoute, Route } from '@netlify/zip-it-and-ship-it' import CronParser from 'cron-parser' import semver from 'semver' @@ -267,19 +268,18 @@ export default class NetlifyFunction { let path = rawPath !== '/' && rawPath.endsWith('/') ? rawPath.slice(0, -1) : rawPath path = path.toLowerCase() - const { routes = [] } = this.buildData ?? {} - // @ts-expect-error TS(7031) FIXME: Binding element 'expression' implicitly has an 'an... Remove this comment to see the full error message - const route = routes.find(({ expression, literal, methods }) => { - if (methods.length !== 0 && !methods.includes(method)) { + const { excludedRoutes = [], routes = [] } = this.buildData ?? {} + const matchingRoute = routes.find((route: ExtendedRoute) => { + if (route.methods && route.methods.length !== 0 && !route.methods.includes(method)) { return false } - if (literal !== undefined) { - return path === literal + if ('literal' in route && route.literal !== undefined) { + return path === route.literal } - if (expression !== undefined) { - const regex = new RegExp(expression) + if ('expression' in route && route.expression !== undefined) { + const regex = new RegExp(route.expression) return regex.test(path) } @@ -287,15 +287,33 @@ export default class NetlifyFunction { return false }) - if (!route) { + if (!matchingRoute) { return } - if (route.prefer_static && (await hasStaticFile())) { + const isExcluded = excludedRoutes.some((excludedRoute: Route) => { + if ('literal' in excludedRoute && excludedRoute.literal !== undefined) { + return path === excludedRoute.literal + } + + if ('expression' in excludedRoute && excludedRoute.expression !== undefined) { + const regex = new RegExp(excludedRoute.expression) + + return regex.test(path) + } + + return false + }) + + if (isExcluded) { + return + } + + if (matchingRoute.prefer_static && (await hasStaticFile())) { return } - return route + return matchingRoute } get runtimeAPIVersion() { diff --git a/src/lib/functions/runtimes/js/builders/zisi.ts b/src/lib/functions/runtimes/js/builders/zisi.ts index e343e7f9bd1..9f3dd68deae 100644 --- a/src/lib/functions/runtimes/js/builders/zisi.ts +++ b/src/lib/functions/runtimes/js/builders/zisi.ts @@ -65,6 +65,7 @@ const buildFunction = async ({ const entryPath = functionDirectory === directory ? func.mainFile : functionDirectory const { entryFilename, + excludedRoutes, includedFiles, inputs, mainFile, @@ -96,7 +97,17 @@ const buildFunction = async ({ clearFunctionsCache(targetDirectory) - return { buildPath, includedFiles, outputModuleFormat, mainFile, routes, runtimeAPIVersion, srcFiles, schedule } + return { + buildPath, + excludedRoutes, + includedFiles, + outputModuleFormat, + mainFile, + routes, + runtimeAPIVersion, + srcFiles, + schedule, + } } /** diff --git a/src/utils/deploy/hash-fns.ts b/src/utils/deploy/hash-fns.ts index 9a06a28c63e..ce4b115ebc9 100644 --- a/src/utils/deploy/hash-fns.ts +++ b/src/utils/deploy/hash-fns.ts @@ -198,6 +198,7 @@ const hashFns = async ( ...funcs, [curr.name]: { display_name: curr.displayName, + excluded_routes: curr.excludedRoutes, generator: curr.generator, routes: curr.routes, build_data: curr.buildData, diff --git a/tests/integration/__fixtures__/dev-server-with-v2-functions/functions/custom-path-excluded.js b/tests/integration/__fixtures__/dev-server-with-v2-functions/functions/custom-path-excluded.js new file mode 100644 index 00000000000..f535cea64cb --- /dev/null +++ b/tests/integration/__fixtures__/dev-server-with-v2-functions/functions/custom-path-excluded.js @@ -0,0 +1,6 @@ +export default async (req, context) => new Response(`Your product: ${context.params.sku}`) + +export const config = { + path: '/custom-path-excluded/:sku', + excludedPath: ['/custom-path-excluded/jacket'], +} diff --git a/tests/integration/commands/dev/v2-api.test.ts b/tests/integration/commands/dev/v2-api.test.ts index 450ed634ff1..e0841387783 100644 --- a/tests/integration/commands/dev/v2-api.test.ts +++ b/tests/integration/commands/dev/v2-api.test.ts @@ -174,6 +174,17 @@ describe.runIf(gte(version, '18.13.0')).concurrent('v2 api', () => { expect(response.status).toBe(404) }) + test('respects excluded paths', async ({ devServer }) => { + const url1 = `http://localhost:${devServer.port}/custom-path-excluded/t-shirt` + const response1 = await fetch(url1) + expect(response1.status).toBe(200) + expect(await response1.text()).toBe(`Your product: t-shirt`) + + const url2 = `http://localhost:${devServer.port}/custom-path-excluded/jacket` + const response2 = await fetch(url2) + expect(response2.status).toBe(404) + }) + describe('handles rewrites to a function', () => { test('rewrite to legacy URL format with `force: true`', async ({ devServer }) => { const url = `http://localhost:${devServer.port}/v2-to-legacy-with-force`