Skip to content

Commit

Permalink
feat: add generator meta data for framework generated Netlify Functio…
Browse files Browse the repository at this point in the history
…ns (#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 <nick@iamdeveloper.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Mar 22, 2023
1 parent fb93b54 commit e5ddcd2
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 6 deletions.
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
23 changes: 19 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 @@ -70,7 +79,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))

Expand All @@ -87,10 +96,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 @@ -154,6 +164,11 @@ export const setupImageFunction = async ({
})

await copyFile(join(__dirname, '..', '..', 'lib', 'templates', 'ipx.js'), join(functionDirectory, functionName))
writeFunctionConfiguration({
functionName: IMAGE_FUNCTION_NAME,
functionTitle: IMAGE_FUNCTION_TITLE,
functionsDir: 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
56 changes: 56 additions & 0 deletions packages/runtime/src/helpers/functionsMetaData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { existsSync, readJSON, writeFile } from 'fs-extra'
import { join } from 'pathe'

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

import { resolveModuleRoot } from './config'

const getNextRuntimeVersion = async (packageJsonPath: string, useNodeModulesPath: boolean) => {
if (!existsSync(packageJsonPath)) {
return
}

const packagePlugin = await readJSON(packageJsonPath)

return useNodeModulesPath ? packagePlugin.version : packagePlugin.dependencies[NEXT_PLUGIN]
}

// The information needed to create a function configuration file
export interface FunctionInfo {
// The name of the function, e.g. `___netlify-handler`
functionName: string

// The name of the function that will be displayed in logs, e.g. `Next.js SSR handler`
functionTitle: string

// The directory where the function is located, e.g. `.netlify/functions`
functionsDir: string
}

/**
* Creates a function configuration file for the given function.
*
* @param functionInfo The information needed to create a function configuration file
*/
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 metadata = {
config: {
name: functionTitle,
generator: `${NEXT_PLUGIN_NAME}@${nextPluginVersion}`,
},
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@unknown',
},
version: 1,
}

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

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

0 comments on commit e5ddcd2

Please sign in to comment.