Skip to content

feat: add generator meta data for framework generated Netlify Functions #1999

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Mar 22, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"jest-extended": "^3.2.0",
"jest-fetch-mock": "^3.0.3",
"jest-junit": "^14.0.1",
"mock-fs": "^5.2.0",
"netlify-plugin-cypress": "^2.2.1",
"npm-run-all": "^4.1.5",
"playwright-chromium": "^1.26.1",
Expand Down
6 changes: 5 additions & 1 deletion packages/runtime/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
export const HANDLER_FUNCTION_NAME = '___netlify-handler'
export const ODB_FUNCTION_NAME = '___netlify-odb-handler'
export const IMAGE_FUNCTION_NAME = '_ipx'

export const NEXT_PLUGIN_NAME = '@netlify/next-runtime'
export const NEXT_PLUGIN = '@netlify/plugin-nextjs'
export const HANDLER_FUNCTION_TITLE = 'Next.js SSR handler'
export const ODB_FUNCTION_TITLE = 'Next.js ISR handler'
export const IMAGE_FUNCTION_TITLE = 'next/image handler'
// These are paths in .next that shouldn't be publicly accessible
export const HIDDEN_PATHS = [
'/cache/*',
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/src/helpers/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const updateRequiredServerFiles = async (publish: string, modifiedConfig:
await writeJSON(configFile, modifiedConfig)
}

const resolveModuleRoot = (moduleName) => {
export const resolveModuleRoot = (moduleName) => {
try {
return dirname(relative(process.cwd(), require.resolve(`${moduleName}/package.json`, { paths: [process.cwd()] })))
} catch {
Expand Down
19 changes: 15 additions & 4 deletions packages/runtime/src/helpers/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,22 @@ import type { ImageConfigComplete, RemotePattern } from 'next/dist/shared/lib/im
import { outdent } from 'outdent'
import { join, relative, resolve } from 'pathe'

import { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME, IMAGE_FUNCTION_NAME, DEFAULT_FUNCTIONS_SRC } from '../constants'
import {
HANDLER_FUNCTION_NAME,
ODB_FUNCTION_NAME,
IMAGE_FUNCTION_NAME,
DEFAULT_FUNCTIONS_SRC,
HANDLER_FUNCTION_TITLE,
ODB_FUNCTION_TITLE,
IMAGE_FUNCTION_TITLE,
} from '../constants'
import { getApiHandler } from '../templates/getApiHandler'
import { getHandler } from '../templates/getHandler'
import { getResolverForPages, getResolverForSourceFiles } from '../templates/getPageResolver'

import { ApiConfig, ApiRouteType, extractConfigFromFile } from './analysis'
import { getSourceFileForPage } from './files'
import { writeFunctionConfiguration } from './functionsMetaData'
import { getFunctionNameForPage } from './utils'

export interface ApiRouteConfig {
Expand Down Expand Up @@ -62,7 +71,7 @@ export const generateFunctions = async (
await writeFile(join(functionsDir, functionName, 'pages.js'), resolverSource)
}

const writeHandler = async (functionName: string, isODB: boolean) => {
const writeHandler = async (functionName: string, functionTitle: string, isODB: boolean) => {
const handlerSource = await getHandler({ isODB, publishDir, appDir: relative(functionDir, appDir) })
await ensureDir(join(functionsDir, functionName))
await writeFile(join(functionsDir, functionName, `${functionName}.js`), handlerSource)
Expand All @@ -71,10 +80,11 @@ export const generateFunctions = async (
join(__dirname, '..', '..', 'lib', 'templates', 'handlerUtils.js'),
join(functionsDir, functionName, 'handlerUtils.js'),
)
writeFunctionConfiguration(functionName, functionTitle, functionsDir)
}

await writeHandler(HANDLER_FUNCTION_NAME, false)
await writeHandler(ODB_FUNCTION_NAME, true)
await writeHandler(HANDLER_FUNCTION_NAME, HANDLER_FUNCTION_TITLE, false)
await writeHandler(ODB_FUNCTION_NAME, ODB_FUNCTION_TITLE, true)
}

/**
Expand Down Expand Up @@ -138,6 +148,7 @@ export const setupImageFunction = async ({
})

await copyFile(join(__dirname, '..', '..', 'lib', 'templates', 'ipx.js'), join(functionDirectory, functionName))
writeFunctionConfiguration(functionName.replace('.js', ''), IMAGE_FUNCTION_TITLE, functionsPath)

// If we have edge functions then the request will have already been rewritten
// so this won't match. This is matched if edge is disabled or unavailable.
Expand Down
44 changes: 44 additions & 0 deletions packages/runtime/src/helpers/functionsMetaData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { existsSync, readJSON, writeFile } from 'fs-extra'
import { join } from 'pathe'

import { NEXT_PLUGIN, NEXT_PLUGIN_NAME } from '../constants'

import { resolveModuleRoot } from './config'

const checkForPackage = async (packageDir: string, nodeModule: boolean) => {
const packagePlugin = existsSync(packageDir) ? await readJSON(packageDir) : null
let nextPlugin
if (!nodeModule && packagePlugin) {
nextPlugin = packagePlugin.dependencies[NEXT_PLUGIN] ? packagePlugin.dependencies[NEXT_PLUGIN] : null
} else if (nodeModule && packagePlugin) {
nextPlugin = packagePlugin.version ? packagePlugin.version : null
}

return nextPlugin
}

/**
* Creates a function configuration file for the given function
*
* @param functionName The name of the function, e.g. `___netlify-handler`
* @param functionTitle The name of the function that will be displayed in logs, e.g. `Next.js SSR handler`
* @param functionsDir The directory where the function is located, e.g. `.netlify/functions`
*/
export const writeFunctionConfiguration = async (functionName: string, functionTitle: string, functionsDir: string) => {
const pluginPackagePath = '.netlify/plugins/package.json'
const ProjDir = resolveModuleRoot(NEXT_PLUGIN)
const nodeModulesPath = `${ProjDir}/package.json`

const nextPluginVersion =
(await checkForPackage(nodeModulesPath, true)) || (await checkForPackage(pluginPackagePath, false))

const metadata = {
config: {
name: functionTitle,
generator: `${NEXT_PLUGIN_NAME}@${nextPluginVersion || 'version-not-found'}`,
},
version: 1,
}

await writeFile(join(functionsDir, functionName, `${functionName}.json`), JSON.stringify(metadata))
}
105 changes: 105 additions & 0 deletions test/functionsMetaData.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { readJSON } from 'fs-extra'
import mock from 'mock-fs'
import { join } from 'pathe'
import { NEXT_PLUGIN_NAME } from '../packages/runtime/src/constants'
import { writeFunctionConfiguration } from '../packages/runtime/src/helpers/functionsMetaData'

describe('writeFunctionConfiguration', () => {
afterEach(() => {
mock.restore()
})

it('should write the configuration for a function using node modules version of @netlify/plugin-nextjs', async () => {
const nextRuntimeVersion = '23.4.5'

mock({
'.netlify/plugins/package.json': JSON.stringify({
name: 'test',
version: '1.0.0',
dependencies: {
'@netlify/plugin-nextjs': '29.3.4',
},
}),
'node_modules/@netlify/plugin-nextjs/package.json': JSON.stringify({
name: '@netlify/plugin-nextjs',
version: nextRuntimeVersion,
}),
'.netlify/functions/some-folder/someFunctionName': {},
})

const functionName = 'someFunctionName'
const functionTitle = 'some function title'
const functionsDir = '.netlify/functions/some-folder'

const expected = {
config: {
name: functionTitle,
generator: `${NEXT_PLUGIN_NAME}@${nextRuntimeVersion}`,
},
version: 1,
}

const filePathToSaveTo = join(functionsDir, functionName, `${functionName}.json`)
await writeFunctionConfiguration(functionName, functionTitle, functionsDir)
const actual = await readJSON(filePathToSaveTo)

expect(actual).toEqual(expected)
})

it('should write the configuration for a function using version of @netlify/plugin-nextjs in package.json', async () => {
const nextRuntimeVersion = '23.4.5'

mock({
'.netlify/plugins/package.json': JSON.stringify({
name: 'test',
version: '1.0.0',
dependencies: {
'@netlify/plugin-nextjs': nextRuntimeVersion,
},
}),
'.netlify/functions/some-folder/someFunctionName': {},
})

const functionName = 'someFunctionName'
const functionTitle = 'some function title'
const functionsDir = '.netlify/functions/some-folder'

const expected = {
config: {
name: functionTitle,
generator: `${NEXT_PLUGIN_NAME}@${nextRuntimeVersion}`,
},
version: 1,
}

const filePathToSaveTo = join(functionsDir, functionName, `${functionName}.json`)
await writeFunctionConfiguration(functionName, functionTitle, functionsDir)
const actual = await readJSON(filePathToSaveTo)

expect(actual).toEqual(expected)
})

it('should write the configuration for a function with runtime version not found', async () => {
mock({
'.netlify/functions/some-folder/someFunctionName': {},
})

const functionName = 'someFunctionName'
const functionTitle = 'some function title'
const functionsDir = '.netlify/functions/some-folder'

const expected = {
config: {
name: functionTitle,
generator: '@netlify/next-runtime@version-not-found',
},
version: 1,
}

const filePathToSaveTo = join(functionsDir, functionName, `${functionName}.json`)
await writeFunctionConfiguration(functionName, functionTitle, functionsDir)
const actual = await readJSON(filePathToSaveTo)

expect(actual).toEqual(expected)
})
})