From dabc56063f4a4cf3b1449b5515bd5a7279e68747 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Fri, 24 Oct 2025 13:38:47 +0200 Subject: [PATCH 1/2] test: adjust EF deploy tests to not add netlifyConfig configuration and use inline config, also add cases for internal and framework functions --- .../commands/deploy/deploy.test.ts | 197 ++++++++++++++---- 1 file changed, 155 insertions(+), 42 deletions(-) diff --git a/tests/integration/commands/deploy/deploy.test.ts b/tests/integration/commands/deploy/deploy.test.ts index 03c3caf8ac8..f6b0b8754ad 100644 --- a/tests/integration/commands/deploy/deploy.test.ts +++ b/tests/integration/commands/deploy/deploy.test.ts @@ -167,69 +167,182 @@ describe.skipIf(process.env.NETLIFY_TEST_DISABLE_LIVE === 'true').concurrent('co }) }) - test('should deploy Edge Functions when directory exists', async (t) => { - await withSiteBuilder(t, async (builder) => { - const content = 'Edge Function works NOT' - builder - .withContentFile({ - path: 'public/index.html', - content, - }) - .withNetlifyToml({ - config: { - build: { publish: 'public', command: 'echo "no op"' }, - edge_functions: [{ function: 'edge', path: '/*' }], - }, + for (const { variant, shouldRunBuildBeforeDeploy } of [ + { + variant: 'after running a build', + shouldRunBuildBeforeDeploy: true, + }, + { + variant: 'without running a build', + shouldRunBuildBeforeDeploy: false, + }, + ]) { + test(`should deploy Edge Functions when directory exists ${variant}`, async (t) => { + await withSiteBuilder(t, async (builder) => { + const content = 'Edge Function works NOT' + builder + .withContentFile({ + path: 'public/index.html', + content, + }) + .withNetlifyToml({ + config: { + build: { publish: 'public', command: 'echo "no op"' }, + }, + }) + .withEdgeFunction({ + handler: async () => new Response('Edge Function works'), + config: { + path: '/*', + }, + name: 'edge', + }) + + await builder.build() + + const options = { + cwd: builder.directory, + env: { NETLIFY_SITE_ID: context.siteId }, + } + + if (shouldRunBuildBeforeDeploy) { + await callCli(['build'], options) + } + const deploy = await callCli(['deploy', '--json', '--no-build'], options).then((output: string) => + JSON.parse(output), + ) + + // give edge functions manifest a couple ticks to propagate + await pause(500) + + await validateDeploy({ + deploy, + siteName: SITE_NAME, + content: 'Edge Function works', + contentMessage: 'Edge function did not execute correctly or was not deployed correctly', }) - .withEdgeFunction({ - handler: async () => new Response('Edge Function works'), - name: 'edge', + }) + }) + + test(`should deploy Edge Functions with custom cwd when directory exists ${variant}`, async (t) => { + await withSiteBuilder(t, async (builder) => { + const content = 'Edge Function works NOT' + const pathPrefix = 'app/cool' + builder + .withContentFile({ + path: 'app/cool/public/index.html', + content, + }) + .withNetlifyToml({ + config: { + build: { publish: 'public', command: 'echo "no op"' }, + }, + pathPrefix, + }) + .withEdgeFunction({ + handler: async () => new Response('Edge Function works'), + name: 'edge', + config: { + path: '/*', + }, + pathPrefix, + }) + + await builder.build() + + const options = { + cwd: builder.directory, + env: { NETLIFY_SITE_ID: context.siteId }, + } + + if (shouldRunBuildBeforeDeploy) { + await callCli(['build', '--cwd', pathPrefix], options) + } + const deploy = await callCli(['deploy', '--json', '--no-build', '--cwd', pathPrefix], options).then( + (output: string) => JSON.parse(output), + ) + + // give edge functions manifest a couple ticks to propagate + await pause(500) + + await validateDeploy({ + deploy, + siteName: SITE_NAME, + content: 'Edge Function works', + contentMessage: 'Edge function did not execute correctly or was not deployed correctly', }) + }) + }) - await builder.build() + test(`should deploy integrations Edge Functions when directory exists ${variant}`, async (t) => { + await withSiteBuilder(t, async (builder) => { + const content = 'Edge Function works NOT' + builder + .withContentFile({ + path: 'public/index.html', + content, + }) + .withNetlifyToml({ + config: { + build: { publish: 'public', command: 'echo "no op"' }, + }, + }) + .withEdgeFunction({ + handler: async () => new Response('Edge Function works'), + config: { + path: '/*', + }, + name: 'edge', + path: '.netlify/edge-functions', + }) - const options = { - cwd: builder.directory, - env: { NETLIFY_SITE_ID: context.siteId }, - } + await builder.build() - await callCli(['build'], options) - const deploy = await callCli(['deploy', '--json', '--no-build'], options).then((output: string) => - JSON.parse(output), - ) + const options = { + cwd: builder.directory, + env: { NETLIFY_SITE_ID: context.siteId }, + } - // give edge functions manifest a couple ticks to propagate - await pause(500) + if (shouldRunBuildBeforeDeploy) { + await callCli(['build'], options) + } + const deploy = await callCli(['deploy', '--json', '--no-build'], options).then((output: string) => + JSON.parse(output), + ) - await validateDeploy({ - deploy, - siteName: SITE_NAME, - content: 'Edge Function works', - contentMessage: 'Edge function did not execute correctly or was not deployed correctly', + // give edge functions manifest a couple ticks to propagate + await pause(500) + + await validateDeploy({ + deploy, + siteName: SITE_NAME, + content: 'Edge Function works', + contentMessage: 'Edge function did not execute correctly or was not deployed correctly', + }) }) }) - }) + } - test('should deploy Edge Functions with custom cwd when directory exists', async (t) => { + test('should deploy framework Edge Functions when directory exists without running a build', async (t) => { await withSiteBuilder(t, async (builder) => { const content = 'Edge Function works NOT' - const pathPrefix = 'app/cool' builder .withContentFile({ - path: 'app/cool/public/index.html', + path: 'public/index.html', content, }) .withNetlifyToml({ config: { build: { publish: 'public', command: 'echo "no op"' }, - edge_functions: [{ function: 'edge', path: '/*' }], }, - pathPrefix, }) .withEdgeFunction({ handler: async () => new Response('Edge Function works'), + config: { + path: '/*', + }, name: 'edge', - pathPrefix, + path: '.netlify/v1/edge-functions', }) await builder.build() @@ -239,9 +352,9 @@ describe.skipIf(process.env.NETLIFY_TEST_DISABLE_LIVE === 'true').concurrent('co env: { NETLIFY_SITE_ID: context.siteId }, } - await callCli(['build', '--cwd', pathPrefix], options) - const deploy = await callCli(['deploy', '--json', '--no-build', '--cwd', pathPrefix], options).then( - (output: string) => JSON.parse(output), + // skipping running build here, because it cleans up frameworks API directories + const deploy = await callCli(['deploy', '--json', '--no-build'], options).then((output: string) => + JSON.parse(output), ) // give edge functions manifest a couple ticks to propagate From 4fe4444df7a6c4f027b6a3f1f77b0e6209f07c23 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Fri, 24 Oct 2025 13:00:28 +0200 Subject: [PATCH 2/2] fix: bundle edge functions if they exist on deploy --no-build --- src/commands/deploy/deploy.ts | 15 +++++++---- src/lib/edge-functions/get-directories.ts | 32 +++++++++++++++++++++++ src/lib/edge-functions/proxy.ts | 5 ---- src/lib/edge-functions/registry.ts | 26 +++++++++--------- 4 files changed, 54 insertions(+), 24 deletions(-) create mode 100644 src/lib/edge-functions/get-directories.ts diff --git a/src/commands/deploy/deploy.ts b/src/commands/deploy/deploy.ts index d3a13193fc4..5c2b27e190d 100644 --- a/src/commands/deploy/deploy.ts +++ b/src/commands/deploy/deploy.ts @@ -53,6 +53,7 @@ import { SiteInfo } from '../../utils/types.js' import type { DeployOptionValues } from './option_values.js' import boxen from 'boxen' import terminalLink from 'terminal-link' +import { anyEdgeFunctionsDirectoryExists } from '../../lib/edge-functions/get-directories.js' const triggerDeploy = async ({ api, @@ -727,8 +728,11 @@ const bundleEdgeFunctions = async (options: DeployOptionValues, command: BaseCom // We log our own progress so we don't want this as well. Plus, this logs much of the same // information as the build that (likely) came before this as part of the deploy build. quiet: options.debug ?? true, - // @ts-expect-error FIXME(serhalp): This is missing from the `runCoreSteps` type in @netlify/build + // (cachedConfig type error hides this one, but it still is valid) @ts-expect-error FIXME(serhalp): This is missing from the `runCoreSteps` type in @netlify/build edgeFunctionsBootstrapURL: await getBootstrapURL(), + // @ts-expect-error 'CachedConfig' is not assignable to type 'Record'. + // Index signature for type 'string' is missing in type 'CachedConfig'. + cachedConfig: command.netlify.cachedConfig, }) if (!success) { @@ -867,10 +871,11 @@ const prepAndRunDeploy = async ({ const functionsFolder = getFunctionsFolder({ workingDir, options, config, site, siteData }) const { configPath } = site - const edgeFunctionsConfig = command.netlify.config.edge_functions - - // build flag wasn't used and edge functions exist - if (!options.build && edgeFunctionsConfig && edgeFunctionsConfig.length !== 0) { + // build flag wasn't used and edge functions directories exist + if (!options.build && (await anyEdgeFunctionsDirectoryExists(command))) { + // for the case of directories existing but not containing any edge functions, + // there is early bail in edge functions bundling after scanning for edge functions + // for this case and to avoid replicating scanning logic here, we defer to the bundling step await bundleEdgeFunctions(options, command) } diff --git a/src/lib/edge-functions/get-directories.ts b/src/lib/edge-functions/get-directories.ts new file mode 100644 index 00000000000..7220c198a83 --- /dev/null +++ b/src/lib/edge-functions/get-directories.ts @@ -0,0 +1,32 @@ +import { join } from 'path' + +import { getPathInProject } from '../settings.js' +import { INTERNAL_EDGE_FUNCTIONS_FOLDER } from './consts.js' +import BaseCommand from '../../commands/base-command.js' +import { fileExistsAsync } from '../fs.js' + +export const getUserEdgeFunctionsDirectory = (command: BaseCommand): string | undefined => { + return command.netlify.config.build.edge_functions +} + +export const getInternalEdgeFunctionsDirectory = (command: BaseCommand): string => { + return join(command.workingDir, getPathInProject([INTERNAL_EDGE_FUNCTIONS_FOLDER])) +} + +export const getFrameworkEdgeFunctionsDirectory = (command: BaseCommand): string => { + return command.netlify.frameworksAPIPaths.edgeFunctions.path +} + +const getAllEdgeFunctionsDirectories = (command: BaseCommand) => { + return [ + getUserEdgeFunctionsDirectory(command), + getInternalEdgeFunctionsDirectory(command), + getFrameworkEdgeFunctionsDirectory(command), + ].filter(Boolean) as string[] +} + +export const anyEdgeFunctionsDirectoryExists = async (command: BaseCommand): Promise => { + const directoriesToCheck = getAllEdgeFunctionsDirectories(command) + + return (await Promise.all(directoriesToCheck.map(fileExistsAsync))).some(Boolean) +} diff --git a/src/lib/edge-functions/proxy.ts b/src/lib/edge-functions/proxy.ts index d2a9b0ef23f..4880e81d598 100644 --- a/src/lib/edge-functions/proxy.ts +++ b/src/lib/edge-functions/proxy.ts @@ -117,7 +117,6 @@ export const initializeProxy = async ({ siteInfo: $TSFixMe state: LocalState }) => { - const userFunctionsPath = config.build.edge_functions const isolatePort = await getAvailablePort() const runtimeFeatureFlags = ['edge_functions_bootstrap_failure_mode', 'edge_functions_bootstrap_populate_environment'] const protocol = settings.https ? 'https' : 'http' @@ -132,7 +131,6 @@ export const initializeProxy = async ({ config, configPath, debug, - directory: userFunctionsPath, env: configEnv, featureFlags: buildFeatureFlags, getUpdatedConfig, @@ -207,7 +205,6 @@ const prepareServer = async ({ config, configPath, debug, - directory, env: configEnv, featureFlags, getUpdatedConfig, @@ -221,7 +218,6 @@ const prepareServer = async ({ config: NormalizedCachedConfigConfig configPath: string debug: boolean - directory?: string env: Record featureFlags: FeatureFlags getUpdatedConfig: () => Promise @@ -261,7 +257,6 @@ const prepareServer = async ({ config, configPath, debug, - directories: directory ? [directory] : [], env: configEnv, featureFlags, getUpdatedConfig, diff --git a/src/lib/edge-functions/registry.ts b/src/lib/edge-functions/registry.ts index c830e4a984c..5482c69f57b 100644 --- a/src/lib/edge-functions/registry.ts +++ b/src/lib/edge-functions/registry.ts @@ -22,7 +22,12 @@ import type { FeatureFlags } from '../../utils/feature-flags.js' import { MultiMap } from '../../utils/multimap.js' import { getPathInProject } from '../settings.js' -import { DIST_IMPORT_MAP_PATH, INTERNAL_EDGE_FUNCTIONS_FOLDER } from './consts.js' +import { DIST_IMPORT_MAP_PATH } from './consts.js' +import { + getFrameworkEdgeFunctionsDirectory, + getInternalEdgeFunctionsDirectory, + getUserEdgeFunctionsDirectory, +} from './get-directories.js' type DependencyCache = Record type EdgeFunctionEvent = 'buildError' | 'loaded' | 'reloaded' | 'reloading' | 'removed' @@ -38,7 +43,6 @@ interface EdgeFunctionsRegistryOptions { config: NormalizedCachedConfigConfig configPath: string debug: boolean - directories: string[] env: Record featureFlags: FeatureFlags getUpdatedConfig: () => Promise @@ -105,7 +109,6 @@ export class EdgeFunctionsRegistry { // Mapping file URLs to names of functions that use them as dependencies. private dependencyPaths = new MultiMap() - private directories: string[] private directoryWatchers = new Map() private env: Record private featureFlags: FeatureFlags @@ -133,7 +136,6 @@ export class EdgeFunctionsRegistry { command, config, configPath, - directories, env, featureFlags, getUpdatedConfig, @@ -146,7 +148,6 @@ export class EdgeFunctionsRegistry { this.command = command this.bundler = bundler this.configPath = configPath - this.directories = directories this.featureFlags = featureFlags this.getUpdatedConfig = getUpdatedConfig this.runIsolate = runIsolate @@ -563,16 +564,12 @@ export class EdgeFunctionsRegistry { return { functionsConfig, graph, success } } - private get internalDirectory() { - return join(this.projectDir, getPathInProject([INTERNAL_EDGE_FUNCTIONS_FOLDER])) - } - private get internalImportMapPath() { return join(this.projectDir, getPathInProject([DIST_IMPORT_MAP_PATH])) } private async readDeployConfig() { - const manifestPath = join(this.internalDirectory, 'manifest.json') + const manifestPath = join(getInternalEdgeFunctionsDirectory(this.command), 'manifest.json') try { const contents = await readFile(manifestPath, 'utf8') const manifest = JSON.parse(contents) @@ -592,15 +589,16 @@ export class EdgeFunctionsRegistry { this.declarationsFromDeployConfig = deployConfig.functions this.importMapFromDeployConfig = deployConfig.import_map - ? join(this.internalDirectory, deployConfig.import_map) + ? join(getInternalEdgeFunctionsDirectory(this.command), deployConfig.import_map) : undefined } private async scanForFunctions() { + const userFunctionDirectory = getUserEdgeFunctionsDirectory(this.command) const [frameworkFunctions, integrationFunctions, userFunctions] = await Promise.all([ - this.usesFrameworksAPI ? this.bundler.find([this.command.netlify.frameworksAPIPaths.edgeFunctions.path]) : [], - this.bundler.find([this.internalDirectory]), - this.bundler.find(this.directories), + this.usesFrameworksAPI ? this.bundler.find([getFrameworkEdgeFunctionsDirectory(this.command)]) : [], + this.bundler.find([getInternalEdgeFunctionsDirectory(this.command)]), + userFunctionDirectory ? this.bundler.find([userFunctionDirectory]) : [], this.scanForDeployConfig(), ]) const internalFunctions = [...frameworkFunctions, ...integrationFunctions]