Skip to content

Commit e5ddcd2

Browse files
taty2010nickytonlinekodiakhq[bot]
authored
feat: add generator meta data for framework generated Netlify Functions (#1999)
* fix: moving functionmetadata over * chore: updated naming for file and when version not found * chore: prettier * Revert "chore: prettier" This reverts commit 75d46b4. * Revert updated naming for file and when version not found" reverts commit 6acb832. * chore: updating not found mssg + image func title * chore: refactor based on feedback * chore: refactored parameters for writeFunctionConfiguration * chore: small refactor and renaming * Update packages/runtime/src/helpers/functionsMetaData.ts * test: updated test when runtime version is unknown * chore: added a comment about returning an unknown version of the next runtime * chore: refactor based on PR feedback * chore: fixed an error causing tests to fail --------- Co-authored-by: Nick Taylor <[email protected]> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent fb93b54 commit e5ddcd2

File tree

7 files changed

+203
-6
lines changed

7 files changed

+203
-6
lines changed

package-lock.json

+16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"jest-extended": "^3.2.0",
7676
"jest-fetch-mock": "^3.0.3",
7777
"jest-junit": "^14.0.1",
78+
"mock-fs": "^5.2.0",
7879
"netlify-plugin-cypress": "^2.2.1",
7980
"npm-run-all": "^4.1.5",
8081
"playwright-chromium": "^1.26.1",

packages/runtime/src/constants.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
export const HANDLER_FUNCTION_NAME = '___netlify-handler'
22
export const ODB_FUNCTION_NAME = '___netlify-odb-handler'
33
export const IMAGE_FUNCTION_NAME = '_ipx'
4-
4+
export const NEXT_PLUGIN_NAME = '@netlify/next-runtime'
5+
export const NEXT_PLUGIN = '@netlify/plugin-nextjs'
6+
export const HANDLER_FUNCTION_TITLE = 'Next.js SSR handler'
7+
export const ODB_FUNCTION_TITLE = 'Next.js ISR handler'
8+
export const IMAGE_FUNCTION_TITLE = 'next/image handler'
59
// These are paths in .next that shouldn't be publicly accessible
610
export const HIDDEN_PATHS = [
711
'/cache/*',

packages/runtime/src/helpers/config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export const updateRequiredServerFiles = async (publish: string, modifiedConfig:
7171
await writeJSON(configFile, modifiedConfig)
7272
}
7373

74-
const resolveModuleRoot = (moduleName) => {
74+
export const resolveModuleRoot = (moduleName) => {
7575
try {
7676
return dirname(relative(process.cwd(), require.resolve(`${moduleName}/package.json`, { paths: [process.cwd()] })))
7777
} catch {

packages/runtime/src/helpers/functions.ts

+19-4
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,22 @@ import type { ImageConfigComplete, RemotePattern } from 'next/dist/shared/lib/im
77
import { outdent } from 'outdent'
88
import { join, relative, resolve } from 'pathe'
99

10-
import { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME, IMAGE_FUNCTION_NAME, DEFAULT_FUNCTIONS_SRC } from '../constants'
10+
import {
11+
HANDLER_FUNCTION_NAME,
12+
ODB_FUNCTION_NAME,
13+
IMAGE_FUNCTION_NAME,
14+
DEFAULT_FUNCTIONS_SRC,
15+
HANDLER_FUNCTION_TITLE,
16+
ODB_FUNCTION_TITLE,
17+
IMAGE_FUNCTION_TITLE,
18+
} from '../constants'
1119
import { getApiHandler } from '../templates/getApiHandler'
1220
import { getHandler } from '../templates/getHandler'
1321
import { getResolverForPages, getResolverForSourceFiles } from '../templates/getPageResolver'
1422

1523
import { ApiConfig, ApiRouteType, extractConfigFromFile } from './analysis'
1624
import { getSourceFileForPage } from './files'
25+
import { writeFunctionConfiguration } from './functionsMetaData'
1726
import { getFunctionNameForPage } from './utils'
1827

1928
export interface ApiRouteConfig {
@@ -70,7 +79,7 @@ export const generateFunctions = async (
7079
await writeFile(join(functionsDir, functionName, 'pages.js'), resolverSource)
7180
}
7281

73-
const writeHandler = async (functionName: string, isODB: boolean) => {
82+
const writeHandler = async (functionName: string, functionTitle: string, isODB: boolean) => {
7483
const handlerSource = await getHandler({ isODB, publishDir, appDir: relative(functionDir, appDir) })
7584
await ensureDir(join(functionsDir, functionName))
7685

@@ -87,10 +96,11 @@ export const generateFunctions = async (
8796
join(__dirname, '..', '..', 'lib', 'templates', 'handlerUtils.js'),
8897
join(functionsDir, functionName, 'handlerUtils.js'),
8998
)
99+
writeFunctionConfiguration({ functionName, functionTitle, functionsDir })
90100
}
91101

92-
await writeHandler(HANDLER_FUNCTION_NAME, false)
93-
await writeHandler(ODB_FUNCTION_NAME, true)
102+
await writeHandler(HANDLER_FUNCTION_NAME, HANDLER_FUNCTION_TITLE, false)
103+
await writeHandler(ODB_FUNCTION_NAME, ODB_FUNCTION_TITLE, true)
94104
}
95105

96106
/**
@@ -154,6 +164,11 @@ export const setupImageFunction = async ({
154164
})
155165

156166
await copyFile(join(__dirname, '..', '..', 'lib', 'templates', 'ipx.js'), join(functionDirectory, functionName))
167+
writeFunctionConfiguration({
168+
functionName: IMAGE_FUNCTION_NAME,
169+
functionTitle: IMAGE_FUNCTION_TITLE,
170+
functionsDir: functionsPath,
171+
})
157172

158173
// If we have edge functions then the request will have already been rewritten
159174
// so this won't match. This is matched if edge is disabled or unavailable.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { existsSync, readJSON, writeFile } from 'fs-extra'
2+
import { join } from 'pathe'
3+
4+
import { NEXT_PLUGIN, NEXT_PLUGIN_NAME } from '../constants'
5+
6+
import { resolveModuleRoot } from './config'
7+
8+
const getNextRuntimeVersion = async (packageJsonPath: string, useNodeModulesPath: boolean) => {
9+
if (!existsSync(packageJsonPath)) {
10+
return
11+
}
12+
13+
const packagePlugin = await readJSON(packageJsonPath)
14+
15+
return useNodeModulesPath ? packagePlugin.version : packagePlugin.dependencies[NEXT_PLUGIN]
16+
}
17+
18+
// The information needed to create a function configuration file
19+
export interface FunctionInfo {
20+
// The name of the function, e.g. `___netlify-handler`
21+
functionName: string
22+
23+
// The name of the function that will be displayed in logs, e.g. `Next.js SSR handler`
24+
functionTitle: string
25+
26+
// The directory where the function is located, e.g. `.netlify/functions`
27+
functionsDir: string
28+
}
29+
30+
/**
31+
* Creates a function configuration file for the given function.
32+
*
33+
* @param functionInfo The information needed to create a function configuration file
34+
*/
35+
export const writeFunctionConfiguration = async (functionInfo: FunctionInfo) => {
36+
const { functionName, functionTitle, functionsDir } = functionInfo
37+
const pluginPackagePath = '.netlify/plugins/package.json'
38+
const moduleRoot = resolveModuleRoot(NEXT_PLUGIN)
39+
const nodeModulesPath = moduleRoot ? join(moduleRoot, 'package.json') : null
40+
41+
const nextPluginVersion =
42+
(await getNextRuntimeVersion(nodeModulesPath, true)) ||
43+
(await getNextRuntimeVersion(pluginPackagePath, false)) ||
44+
// The runtime version should always be available, but if it's not, return 'unknown'
45+
'unknown'
46+
47+
const metadata = {
48+
config: {
49+
name: functionTitle,
50+
generator: `${NEXT_PLUGIN_NAME}@${nextPluginVersion}`,
51+
},
52+
version: 1,
53+
}
54+
55+
await writeFile(join(functionsDir, functionName, `${functionName}.json`), JSON.stringify(metadata))
56+
}

test/functionsMetaData.spec.ts

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { readJSON } from 'fs-extra'
2+
import mock from 'mock-fs'
3+
import { join } from 'pathe'
4+
import { NEXT_PLUGIN_NAME } from '../packages/runtime/src/constants'
5+
import { writeFunctionConfiguration } from '../packages/runtime/src/helpers/functionsMetaData'
6+
7+
describe('writeFunctionConfiguration', () => {
8+
afterEach(() => {
9+
mock.restore()
10+
})
11+
12+
it('should write the configuration for a function using node modules version of @netlify/plugin-nextjs', async () => {
13+
const nextRuntimeVersion = '23.4.5'
14+
15+
mock({
16+
'.netlify/plugins/package.json': JSON.stringify({
17+
name: 'test',
18+
version: '1.0.0',
19+
dependencies: {
20+
'@netlify/plugin-nextjs': '29.3.4',
21+
},
22+
}),
23+
'node_modules/@netlify/plugin-nextjs/package.json': JSON.stringify({
24+
name: '@netlify/plugin-nextjs',
25+
version: nextRuntimeVersion,
26+
}),
27+
'.netlify/functions/some-folder/someFunctionName': {},
28+
})
29+
30+
const functionName = 'someFunctionName'
31+
const functionTitle = 'some function title'
32+
const functionsDir = '.netlify/functions/some-folder'
33+
34+
const expected = {
35+
config: {
36+
name: functionTitle,
37+
generator: `${NEXT_PLUGIN_NAME}@${nextRuntimeVersion}`,
38+
},
39+
version: 1,
40+
}
41+
42+
const filePathToSaveTo = join(functionsDir, functionName, `${functionName}.json`)
43+
await writeFunctionConfiguration({ functionName, functionTitle, functionsDir })
44+
const actual = await readJSON(filePathToSaveTo)
45+
46+
expect(actual).toEqual(expected)
47+
})
48+
49+
it('should write the configuration for a function using version of @netlify/plugin-nextjs in package.json', async () => {
50+
const nextRuntimeVersion = '23.4.5'
51+
52+
mock({
53+
'.netlify/plugins/package.json': JSON.stringify({
54+
name: 'test',
55+
version: '1.0.0',
56+
dependencies: {
57+
'@netlify/plugin-nextjs': nextRuntimeVersion,
58+
},
59+
}),
60+
'.netlify/functions/some-folder/someFunctionName': {},
61+
})
62+
63+
const functionName = 'someFunctionName'
64+
const functionTitle = 'some function title'
65+
const functionsDir = '.netlify/functions/some-folder'
66+
67+
const expected = {
68+
config: {
69+
name: functionTitle,
70+
generator: `${NEXT_PLUGIN_NAME}@${nextRuntimeVersion}`,
71+
},
72+
version: 1,
73+
}
74+
75+
const filePathToSaveTo = join(functionsDir, functionName, `${functionName}.json`)
76+
await writeFunctionConfiguration({ functionName, functionTitle, functionsDir })
77+
const actual = await readJSON(filePathToSaveTo)
78+
79+
expect(actual).toEqual(expected)
80+
})
81+
82+
it('should write the configuration for a function with runtime version not found', async () => {
83+
mock({
84+
'.netlify/functions/some-folder/someFunctionName': {},
85+
})
86+
87+
const functionName = 'someFunctionName'
88+
const functionTitle = 'some function title'
89+
const functionsDir = '.netlify/functions/some-folder'
90+
91+
const expected = {
92+
config: {
93+
name: functionTitle,
94+
generator: '@netlify/next-runtime@unknown',
95+
},
96+
version: 1,
97+
}
98+
99+
const filePathToSaveTo = join(functionsDir, functionName, `${functionName}.json`)
100+
await writeFunctionConfiguration({ functionName, functionTitle, functionsDir })
101+
const actual = await readJSON(filePathToSaveTo)
102+
103+
expect(actual).toEqual(expected)
104+
})
105+
})

0 commit comments

Comments
 (0)