diff --git a/src/build/functions/edge.ts b/src/build/functions/edge.ts index f3f534c35b..a7668cbf00 100644 --- a/src/build/functions/edge.ts +++ b/src/build/functions/edge.ts @@ -1,5 +1,5 @@ -import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' -import { dirname, join } from 'node:path' +import { cp, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises' +import { dirname, join, parse as parsePath } from 'node:path' import type { Manifest, ManifestFunction } from '@netlify/edge-functions' import { glob } from 'fast-glob' @@ -8,9 +8,21 @@ import { pathToRegexp } from 'path-to-regexp' import { EDGE_HANDLER_NAME, PluginContext } from '../plugin-context.js' +type ManifestFunctionWithGenerator = ManifestFunction & { generator?: string } + +const getEdgeManifestPath = (ctx: PluginContext) => join(ctx.edgeFunctionsDir, 'manifest.json') + const writeEdgeManifest = async (ctx: PluginContext, manifest: Manifest) => { await mkdir(ctx.edgeFunctionsDir, { recursive: true }) - await writeFile(join(ctx.edgeFunctionsDir, 'manifest.json'), JSON.stringify(manifest, null, 2)) + await writeFile(getEdgeManifestPath(ctx), JSON.stringify(manifest, null, 2)) +} + +const readEdgeManifest = async (ctx: PluginContext) => { + try { + return JSON.parse(await readFile(getEdgeManifestPath(ctx), 'utf-8')) as Manifest + } catch { + return null + } } const copyRuntime = async (ctx: PluginContext, handlerDirectory: string): Promise => { @@ -145,7 +157,7 @@ const getHandlerName = ({ name }: Pick): string => const buildHandlerDefinition = ( ctx: PluginContext, { name, matchers, page }: NextDefinition, -): Array => { +): Array => { const fun = getHandlerName({ name }) const funName = name.endsWith('middleware') ? 'Next.js Middleware Handler' @@ -162,8 +174,35 @@ const buildHandlerDefinition = ( })) } +const clearStaleEdgeHandlers = async (ctx: PluginContext) => { + const previousManifest = await readEdgeManifest(ctx) + if (!previousManifest) { + return [] + } + + const uniqueNextRuntimeFunctions = new Set() + const nonNextRuntimeFunctions: ManifestFunctionWithGenerator[] = [] + + for (const fn of previousManifest.functions as ManifestFunctionWithGenerator[]) { + if (fn?.generator?.startsWith(ctx.pluginName)) { + uniqueNextRuntimeFunctions.add(fn.function) + } else { + nonNextRuntimeFunctions.push(fn) + } + } + + for (const fileOrDir of await readdir(ctx.edgeFunctionsDir, { withFileTypes: true })) { + const nameWithoutExtension = parsePath(fileOrDir.name).name + + if (uniqueNextRuntimeFunctions.has(nameWithoutExtension)) { + await rm(join(ctx.edgeFunctionsDir, fileOrDir.name), { recursive: true, force: true }) + } + } + return nonNextRuntimeFunctions +} + export const createEdgeHandlers = async (ctx: PluginContext) => { - await rm(ctx.edgeFunctionsDir, { recursive: true, force: true }) + const nonNextRuntimeFunctions = await clearStaleEdgeHandlers(ctx) const nextManifest = await ctx.getMiddlewareManifest() const nextDefinitions = [ @@ -175,7 +214,7 @@ export const createEdgeHandlers = async (ctx: PluginContext) => { const netlifyDefinitions = nextDefinitions.flatMap((def) => buildHandlerDefinition(ctx, def)) const netlifyManifest: Manifest = { version: 1, - functions: netlifyDefinitions, + functions: [...nonNextRuntimeFunctions, ...netlifyDefinitions], } await writeEdgeManifest(ctx, netlifyManifest) } diff --git a/src/build/functions/server.ts b/src/build/functions/server.ts index e34997ee06..726d98e99b 100644 --- a/src/build/functions/server.ts +++ b/src/build/functions/server.ts @@ -1,5 +1,5 @@ -import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' -import { join, relative } from 'node:path' +import { cp, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises' +import { join, parse as parsePath, relative } from 'node:path' import { join as posixJoin } from 'node:path/posix' import { trace } from '@opentelemetry/api' @@ -127,12 +127,48 @@ const writeHandlerFile = async (ctx: PluginContext) => { await writeFile(join(ctx.serverHandlerRootDir, `${SERVER_HANDLER_NAME}.mjs`), handler) } +const clearStaleServerHandlers = async (ctx: PluginContext) => { + const potentialServerlessFunctionConfigFiles = await glob('**/*.json', { + deep: 2, + cwd: ctx.serverFunctionsDir, + }) + + const toRemove = new Set() + + for (const potentialServerlessFunctionConfigFile of potentialServerlessFunctionConfigFiles) { + try { + const functionConfig = JSON.parse( + await readFile( + join(ctx.serverFunctionsDir, potentialServerlessFunctionConfigFile), + 'utf-8', + ), + ) + + if (functionConfig?.config?.generator?.startsWith(ctx.pluginName)) { + const parsedPath = parsePath(potentialServerlessFunctionConfigFile) + + toRemove.add(parsedPath.dir || parsedPath.name) + } + } catch { + // this might be malformatted json or json that doesn't represent function configuration + // so we just skip it in case of errors + } + } + + for (const fileOrDir of await readdir(ctx.serverFunctionsDir, { withFileTypes: true })) { + const nameWithoutExtension = parsePath(fileOrDir.name).name + + if (toRemove.has(nameWithoutExtension)) { + await rm(join(ctx.serverFunctionsDir, fileOrDir.name), { recursive: true, force: true }) + } + } +} /** * Create a Netlify function to run the Next.js server */ export const createServerHandler = async (ctx: PluginContext) => { await tracer.withActiveSpan('createServerHandler', async () => { - await rm(ctx.serverFunctionsDir, { recursive: true, force: true }) + await clearStaleServerHandlers(ctx) await mkdir(join(ctx.serverHandlerDir, '.netlify'), { recursive: true }) await copyNextServerCode(ctx)