diff --git a/packages/runtime/src/helpers/edge.ts b/packages/runtime/src/helpers/edge.ts index 59b9b85cb0..6a139ced05 100644 --- a/packages/runtime/src/helpers/edge.ts +++ b/packages/runtime/src/helpers/edge.ts @@ -13,9 +13,9 @@ import { outdent } from 'outdent' import { IMAGE_FUNCTION_NAME } from '../constants' import { getRequiredServerFiles, NextConfig } from './config' +import { getPluginVersion } from './functionsMetaData' import { makeLocaleOptional, stripLookahead, transformCaptureGroups } from './matchers' import { RoutesManifest } from './types' - // This is the format as of next@12.2 interface EdgeFunctionDefinitionV1 { env: string[] @@ -57,12 +57,14 @@ export interface FunctionManifest { name?: string path: string cache?: 'manual' + generator: string } | { function: string name?: string pattern: string cache?: 'manual' + generator: string } > layers?: Array<{ name: `https://${string}/mod.ts`; flag: string }> @@ -221,15 +223,17 @@ const generateEdgeFunctionMiddlewareMatchers = ({ const middlewareMatcherToEdgeFunctionDefinition = ( matcher: MiddlewareMatcher, name: string, + generator: string, cache?: 'manual', ): { function: string name?: string pattern: string cache?: 'manual' + generator: string } => { const pattern = transformCaptureGroups(stripLookahead(matcher.regexp)) - return { function: name, pattern, name, cache } + return { function: name, pattern, name, cache, generator } } export const cleanupEdgeFunctions = ({ @@ -239,12 +243,14 @@ export const cleanupEdgeFunctions = ({ export const writeDevEdgeFunction = async ({ INTERNAL_EDGE_FUNCTIONS_SRC = '.netlify/edge-functions', }: NetlifyPluginConstants) => { + const generator = await getPluginVersion() const manifest: FunctionManifest = { functions: [ { function: 'next-dev', name: 'netlify dev handler', path: '/*', + generator, }, ], version: 1, @@ -270,6 +276,7 @@ export const generateRscDataEdgeManifest = async ({ prerenderManifest?: PrerenderManifest appPathRoutesManifest?: Record }): Promise => { + const generator = await getPluginVersion() if (!prerenderManifest || !appPathRoutesManifest) { return [] } @@ -300,11 +307,13 @@ export const generateRscDataEdgeManifest = async ({ function: 'rsc-data', name: 'RSC data routing', path, + generator, })), ...dynamicAppDirRoutes.map((pattern) => ({ function: 'rsc-data', name: 'RSC data routing', pattern, + generator, })), ] } @@ -344,6 +353,8 @@ export const writeEdgeFunctions = async ({ netlifyConfig: NetlifyConfig routesManifest: RoutesManifest }) => { + const generator = await getPluginVersion() + const manifest: FunctionManifest = { functions: [], layers: [], @@ -402,7 +413,7 @@ export const writeEdgeFunctions = async ({ }) manifest.functions.push( - ...matchers.map((matcher) => middlewareMatcherToEdgeFunctionDefinition(matcher, functionName)), + ...matchers.map((matcher) => middlewareMatcherToEdgeFunctionDefinition(matcher, functionName, generator)), ) } // Functions (i.e. not middleware, but edge SSR and API routes) @@ -442,6 +453,7 @@ export const writeEdgeFunctions = async ({ pattern, // cache: "manual" is currently experimental, so we restrict it to sites that use experimental appDir cache: usesAppDir ? 'manual' : undefined, + generator, }) // pages-dir page routes also have a data route. If there's a match, add an entry mapping that to the function too const dataRoute = dataRoutesMap.get(edgeFunctionDefinition.page) @@ -451,6 +463,7 @@ export const writeEdgeFunctions = async ({ name: edgeFunctionDefinition.name, pattern: dataRoute, cache: usesAppDir ? 'manual' : undefined, + generator, }) } } @@ -476,6 +489,7 @@ export const writeEdgeFunctions = async ({ function: 'ipx', name: 'next/image handler', path: nextConfig.images.path || '/_next/image', + generator, }) manifest.layers.push({ @@ -494,6 +508,5 @@ export const writeEdgeFunctions = async ({ This feature is in beta. Please share your feedback here: https://ntl.fyi/next-netlify-edge `) } - await writeJson(join(edgeFunctionRoot, 'manifest.json'), manifest) } diff --git a/packages/runtime/src/helpers/functionsMetaData.ts b/packages/runtime/src/helpers/functionsMetaData.ts index c8e0972ded..897de45f9d 100644 --- a/packages/runtime/src/helpers/functionsMetaData.ts +++ b/packages/runtime/src/helpers/functionsMetaData.ts @@ -15,6 +15,22 @@ const getNextRuntimeVersion = async (packageJsonPath: string, useNodeModulesPath return useNodeModulesPath ? packagePlugin.version : packagePlugin.dependencies[NEXT_PLUGIN] } +const PLUGIN_PACKAGE_PATH = '.netlify/plugins/package.json' + +const nextPluginVersion = async () => { + const moduleRoot = resolveModuleRoot(NEXT_PLUGIN) + const nodeModulesPath = moduleRoot ? join(moduleRoot, 'package.json') : null + + return ( + (await getNextRuntimeVersion(nodeModulesPath, true)) || + (await getNextRuntimeVersion(PLUGIN_PACKAGE_PATH, false)) || + // The runtime version should always be available, but if it's not, return 'unknown' + 'unknown' + ) +} + +export const getPluginVersion = async () => `${NEXT_PLUGIN_NAME}@${await nextPluginVersion()}` + // The information needed to create a function configuration file export interface FunctionInfo { // The name of the function, e.g. `___netlify-handler` @@ -34,20 +50,12 @@ export interface FunctionInfo { */ export const writeFunctionConfiguration = async (functionInfo: FunctionInfo) => { const { functionName, functionTitle, functionsDir } = functionInfo - const pluginPackagePath = '.netlify/plugins/package.json' - const moduleRoot = resolveModuleRoot(NEXT_PLUGIN) - const nodeModulesPath = moduleRoot ? join(moduleRoot, 'package.json') : null - - const nextPluginVersion = - (await getNextRuntimeVersion(nodeModulesPath, true)) || - (await getNextRuntimeVersion(pluginPackagePath, false)) || - // The runtime version should always be available, but if it's not, return 'unknown' - 'unknown' + const generator = await getPluginVersion() const metadata = { config: { name: functionTitle, - generator: `${NEXT_PLUGIN_NAME}@${nextPluginVersion}`, + generator, }, version: 1, } diff --git a/test/edge.spec.ts b/test/edge.spec.ts index 80d6d782ce..a3411714d3 100644 --- a/test/edge.spec.ts +++ b/test/edge.spec.ts @@ -1,6 +1,14 @@ import { generateRscDataEdgeManifest } from '../packages/runtime/src/helpers/edge' import type { PrerenderManifest } from 'next/dist/build' +jest.mock('../packages/runtime/src/helpers/functionsMetaData', () => { + const { NEXT_PLUGIN_NAME } = require('../packages/runtime/src/constants') + return { + ...jest.requireActual('../packages/runtime/src/helpers/functionsMetaData'), + getPluginVersion: async () => `${NEXT_PLUGIN_NAME}@1.0.0`, + } +}) + const basePrerenderManifest: PrerenderManifest = { version: 3, routes: {}, @@ -33,11 +41,13 @@ describe('generateRscDataEdgeManifest', () => { expect(edgeManifest).toEqual([ { function: 'rsc-data', + generator: "@netlify/next-runtime@1.0.0", name: 'RSC data routing', path: '/', }, { function: 'rsc-data', + generator: "@netlify/next-runtime@1.0.0", name: 'RSC data routing', path: '/index.rsc', }, @@ -84,11 +94,13 @@ describe('generateRscDataEdgeManifest', () => { expect(edgeManifest).toEqual([ { function: 'rsc-data', + generator: "@netlify/next-runtime@1.0.0", name: 'RSC data routing', pattern: '^/blog/([^/]+?)(?:/)?$', }, { function: 'rsc-data', + generator: "@netlify/next-runtime@1.0.0", name: 'RSC data routing', pattern: '^/blog/([^/]+?)\\.rsc$', }, diff --git a/test/functionsMetaData.spec.ts b/test/functionsMetaData.spec.ts index 9c130d454f..b69e067d82 100644 --- a/test/functionsMetaData.spec.ts +++ b/test/functionsMetaData.spec.ts @@ -102,4 +102,4 @@ describe('writeFunctionConfiguration', () => { expect(actual).toEqual(expected) }) -}) +}) \ No newline at end of file diff --git a/test/index.spec.js b/test/index.spec.js index 06a22330e8..a188544851 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -9,6 +9,14 @@ jest.mock('../packages/runtime/src/helpers/utils', () => { } }) +jest.mock('../packages/runtime/src/helpers/functionsMetaData', () => { + const { NEXT_PLUGIN_NAME } = require('../packages/runtime/src/constants') + return { + ...jest.requireActual('../packages/runtime/src/helpers/functionsMetaData'), + getPluginVersion: async () => `${NEXT_PLUGIN_NAME}@1.0.0`, + } +}) + const Chance = require('chance') const { writeJSON, @@ -730,6 +738,44 @@ describe('onBuild()', () => { expect(existsSync(path.join('.netlify', 'edge-functions', 'ipx', 'index.ts'))).toBeTruthy() }) + test('generates edge-functions manifest', async () => { + await moveNextDist() + await nextRuntime.onBuild(defaultArgs) + expect(existsSync(path.join('.netlify', 'edge-functions', 'manifest.json'))).toBeTruthy() + }) + + test('generates generator field within the edge-functions manifest', async () => { + await moveNextDist() + await nextRuntime.onBuild(defaultArgs) + const manifestPath = await readJson(path.resolve('.netlify/edge-functions/manifest.json')) + const manifest = manifestPath.functions + + expect(manifest).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + generator: '@netlify/next-runtime@1.0.0' + }) + ]) + ) + }) + + + test('generates generator field within the edge-functions manifest includes IPX', async () => { + process.env.NEXT_FORCE_EDGE_IMAGES = '1' + await moveNextDist() + await nextRuntime.onBuild(defaultArgs) + const manifestPath = await readJson(path.resolve('.netlify/edge-functions/manifest.json')) + const manifest = manifestPath.functions + + expect(manifest).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + generator: '@netlify/next-runtime@1.0.0' + }) + ]) + ) + }) + test('does not generate an ipx function when DISABLE_IPX is set', async () => { process.env.DISABLE_IPX = '1' await moveNextDist()