Skip to content

feat: load generated functions from Netlify Build #7408

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

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
5 changes: 2 additions & 3 deletions src/commands/dev/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export const dev = async (options: OptionValues, command: BaseCommand) => {

log(`${NETLIFYDEVLOG} Setting up local dev server`)

const { configMutations, configPath: configPathOverride } = await runDevTimeline({
const { configMutations, generatedFunctions } = await runDevTimeline({
command,
options,
settings,
Expand All @@ -208,7 +208,7 @@ export const dev = async (options: OptionValues, command: BaseCommand) => {
blobsContext,
command,
config: mutatedConfig,

generatedFunctions,
debug: options.debug,
settings,
site,
Expand Down Expand Up @@ -247,7 +247,6 @@ export const dev = async (options: OptionValues, command: BaseCommand) => {
blobsContext,
command,
config: mutatedConfig,
configPath: configPathOverride,
debug: options.debug,
disableEdgeFunctions: options.internalDisableEdgeFunctions,
projectDir: command.workingDir,
Expand Down
1 change: 1 addition & 0 deletions src/commands/functions/functions-serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const functionsServe = async (options: OptionValues, command: BaseCommand
siteUrl,
capabilities,
timeouts,
generatedFunctions: [],
geolocationMode: options.geo,
geoCountry: options.country,
offline: options.offline,
Expand Down
3 changes: 2 additions & 1 deletion src/commands/serve/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export const serve = async (options: OptionValues, command: BaseCommand) => {
// which is what build plugins use.
process.env[BLOBS_CONTEXT_VARIABLE] = encodeBlobsContext(await getBlobsContextWithAPIAccess(blobsOptions))

const { configPath: configPathOverride } = await runBuildTimeline({
const { configPath: configPathOverride, generatedFunctions } = await runBuildTimeline({
command,
settings,
options,
Expand All @@ -130,6 +130,7 @@ export const serve = async (options: OptionValues, command: BaseCommand) => {
command,
config: mergedConfig,
debug: options.debug,
generatedFunctions,
loadDistFunctions: true,
settings,
site,
Expand Down
8 changes: 8 additions & 0 deletions src/lib/functions/netlify-function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ export default class NetlifyFunction<BuildResult extends BaseBuildResult> {
public readonly runtime: Runtime<BuildResult>
public schedule?: string

// The path to the function boundary: if the function is in a sub-directory,
// this will hold the path to the sub-directory; if not, it's the path to the
// main file.
public readonly srcPath: string

// Determines whether this is a background function based on the function
// name.
public readonly isBackground: boolean
Expand All @@ -78,6 +83,7 @@ export default class NetlifyFunction<BuildResult extends BaseBuildResult> {
projectRoot,
runtime,
settings,
srcPath,
timeoutBackground,
timeoutSynchronous,
}: {
Expand All @@ -91,6 +97,7 @@ export default class NetlifyFunction<BuildResult extends BaseBuildResult> {
runtime: Runtime<BuildResult>
// TODO(serhalp): This is confusing. Refactor to accept entire settings or rename or something?
settings: Pick<ServerSettings, 'functions' | 'functionsPort'>
srcPath: string
timeoutBackground?: number
timeoutSynchronous?: number
}) {
Expand All @@ -105,6 +112,7 @@ export default class NetlifyFunction<BuildResult extends BaseBuildResult> {
this.timeoutBackground = timeoutBackground
this.timeoutSynchronous = timeoutSynchronous
this.settings = settings
this.srcPath = srcPath

this.isBackground = name.endsWith(BACKGROUND)

Expand Down
41 changes: 32 additions & 9 deletions src/lib/functions/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createRequire } from 'module'
import { basename, extname, isAbsolute, join, resolve } from 'path'
import { env } from 'process'

import type { GeneratedFunction } from '@netlify/build'
import { type ListedFunction, listFunctions, type Manifest } from '@netlify/zip-it-and-ship-it'
import extractZip from 'extract-zip'

Expand Down Expand Up @@ -74,6 +75,7 @@ export class FunctionsRegistry {
private config: NormalizedCachedConfigConfig
private debug: boolean
private frameworksAPIPaths: ReturnType<typeof getFrameworksAPIPaths>
private generatedFunctions: GeneratedFunction[]
private isConnected: boolean
private logLambdaCompat: boolean
private manifest?: Manifest
Expand All @@ -88,6 +90,7 @@ export class FunctionsRegistry {
config,
debug = false,
frameworksAPIPaths,
generatedFunctions,
isConnected = false,
logLambdaCompat,
manifest,
Expand All @@ -103,6 +106,7 @@ export class FunctionsRegistry {
config: NormalizedCachedConfigConfig
debug?: boolean
frameworksAPIPaths: ReturnType<typeof getFrameworksAPIPaths>
generatedFunctions: GeneratedFunction[]
isConnected?: boolean
logLambdaCompat: boolean
manifest?: Manifest
Expand All @@ -115,6 +119,7 @@ export class FunctionsRegistry {
this.config = config
this.debug = debug
this.frameworksAPIPaths = frameworksAPIPaths
this.generatedFunctions = generatedFunctions ?? []
this.isConnected = isConnected
this.projectRoot = projectRoot
this.timeouts = timeouts
Expand Down Expand Up @@ -465,14 +470,32 @@ export class FunctionsRegistry {

await Promise.all(directories.map((path) => FunctionsRegistry.prepareDirectory(path)))

const functions = await this.listFunctions(directories, {
featureFlags: {
buildRustSource: env.NETLIFY_EXPERIMENTAL_BUILD_RUST_SOURCE === 'true',
const functions = await this.listFunctions(
{
generated: {
functions: this.generatedFunctions.map((func) => func.path),
},
user: {
// In reality, `directories` contains both directories with user and
// generated functions. The registry currently lacks knowledge about
// the contents of each directory, so we put them in the same bag and
// rely on the order of the directories to get the priority right.
// But now that zip-it-and-ship-it accepts an object with mixed paths
// that lets us specify exactly which paths contain user functions or
// generated functions, we should refactor this call so it encodes
// that distiction.
directories,
},
},
configFileDirectories: [getPathInProject([INTERNAL_FUNCTIONS_FOLDER])],
// @ts-expect-error -- TODO(serhalp): Function config types do not match. Investigate and fix.
config: this.config.functions,
})
{
featureFlags: {
buildRustSource: env.NETLIFY_EXPERIMENTAL_BUILD_RUST_SOURCE === 'true',
},
configFileDirectories: [getPathInProject([INTERNAL_FUNCTIONS_FOLDER])],
// @ts-expect-error -- TODO(serhalp): Function config types do not match. Investigate and fix.
config: this.config.functions,
},
)

// user-defined functions take precedence over internal functions,
// so we want to ignore any internal functions where there's a user-defined one with the same name
Expand Down Expand Up @@ -506,7 +529,7 @@ export class FunctionsRegistry {
// zip-it-and-ship-it returns an array sorted based on which extension should have precedence,
// where the last ones precede the previous ones. This is why
// we reverse the array so we get the right functions precedence in the CLI.
functions.reverse().map(async ({ displayName, mainFile, name, runtime: runtimeName }) => {
functions.reverse().map(async ({ displayName, mainFile, name, runtime: runtimeName, srcPath }) => {
if (ignoredFunctions.has(name)) {
return
}
Expand All @@ -527,7 +550,6 @@ export class FunctionsRegistry {
const func = new NetlifyFunction({
blobsContext: this.blobsContext,
config: this.config,
directory: directories.find((directory) => mainFile.startsWith(directory)),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was weird. We were computing directory by looking at the function and ascertaining which one of the main directories it belonged to. We then passed this property along and further downstream we'd use it to decide what path to send to zipFunction, which should always be the path to the function boundary (i.e. the function sub-directory if it lives in a sub-directory, or the function main file if not).

But that's exactly what the srcPath that we get from zip-it-and-ship-it is. So I've removed this property altogether, passed srcPath to NetlifyFunction and then read it from there when calling zipFunction.

mainFile,
name,
displayName,
Expand All @@ -539,6 +561,7 @@ export class FunctionsRegistry {
timeoutBackground: this.timeouts.backgroundFunctions,
timeoutSynchronous: this.timeouts.syncFunctions,
settings: this.settings,
srcPath,
})

// If a function we're registering was also unregistered in this run,
Expand Down
18 changes: 2 additions & 16 deletions src/lib/functions/runtimes/js/builders/zisi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ const addFunctionsConfigDefaults = (config: NormalizedFunctionsConfig) => ({
const buildFunction = async ({
cache,
config,
directory,
featureFlags,
func,
hasTypeModule,
Expand All @@ -48,7 +47,6 @@ const buildFunction = async ({
}: {
cache: BuildCommandCache<FunctionResult>
config: NormalizedFunctionsConfig
directory?: string | undefined
featureFlags: FeatureFlags
// This seems like it should be `ZisiBuildResult` but it's technically referenced from `detectZisiBuilder` so TS
// can't know at that point that we'll only end up calling it with a `ZisiBuildResult`... Consider refactoring?
Expand All @@ -63,16 +61,6 @@ const buildFunction = async ({
config,
featureFlags: { ...featureFlags, zisi_functions_api_v2: true },
}
const functionDirectory = path.dirname(func.mainFile)

// If we have a function at `functions/my-func/index.js` and we pass
// that path to `zipFunction`, it will lack the context of the whole
// functions directory and will infer the name of the function to be
// `index`, not `my-func`. Instead, we need to pass the directory of
// the function. The exception is when the function is a file at the
// root of the functions directory (e.g. `functions/my-func.js`). In
// this case, we use `mainFile` as the function path of `zipFunction`.
const entryPath = functionDirectory === directory ? func.mainFile : functionDirectory
const {
entryFilename,
excludedRoutes,
Expand All @@ -86,9 +74,9 @@ const buildFunction = async ({
schedule,
} = await memoizedBuild({
cache,
cacheKey: `zisi-${entryPath}`,
cacheKey: `zisi-${func.srcPath}`,
command: async () => {
const result = await zipFunction(entryPath, targetDirectory, zipOptions)
const result = await zipFunction(func.srcPath, targetDirectory, zipOptions)
if (result == null) {
throw new Error('Failed to build function')
}
Expand Down Expand Up @@ -179,7 +167,6 @@ const netlifyConfigToZisiConfig = ({

export default async function detectZisiBuilder({
config,
directory,
errorExit,
func,
metadata,
Expand Down Expand Up @@ -233,7 +220,6 @@ export default async function detectZisiBuilder({
buildFunction({
cache,
config: functionsConfig,
directory,
func,
projectRoot,
targetDirectory,
Expand Down
13 changes: 3 additions & 10 deletions src/lib/functions/runtimes/js/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { createConnection } from 'net'
import { dirname } from 'path'
import { pathToFileURL } from 'url'
import { Worker } from 'worker_threads'

Expand Down Expand Up @@ -30,25 +29,19 @@ lambdaLocal.getLogger().level = 'alert'

export async function getBuildFunction({
config,
directory,
errorExit,
func,
projectRoot,
}: Parameters<GetBuildFunction<JsBuildResult>>[0]) {
const metadata = await getFunctionMetadata({ mainFile: func.mainFile, config, projectRoot })
const zisiBuilder = await detectZisiBuilder({ config, directory, errorExit, func, metadata, projectRoot })
const zisiBuilder = await detectZisiBuilder({ config, errorExit, func, metadata, projectRoot })

if (zisiBuilder) {
return zisiBuilder.build
}

// If there's no function builder, we create a simple one on-the-fly which
// returns as `srcFiles` the function directory, if there is one, or its
// main file otherwise.
const functionDirectory = dirname(func.mainFile)
const srcFiles = functionDirectory === directory ? [func.mainFile] : [functionDirectory]

const build: BuildFunction<JsBuildResult> = () => Promise.resolve({ schedule: metadata?.schedule, srcFiles })
const build: BuildFunction<JsBuildResult> = () =>
Promise.resolve({ schedule: metadata?.schedule, srcFiles: [func.srcPath] })
return build
}

Expand Down
4 changes: 4 additions & 0 deletions src/lib/functions/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { promises as fs } from 'fs'
import type { IncomingHttpHeaders } from 'http'
import path from 'path'

import type { GeneratedFunction } from '@netlify/build'
import express, { type Request, type RequestHandler } from 'express'
import expressLogging from 'express-logging'
import { jwtDecode } from 'jwt-decode'
Expand Down Expand Up @@ -305,6 +306,7 @@ export const startFunctionsServer = async (
backgroundFunctions?: boolean
}
debug: boolean
generatedFunctions: GeneratedFunction[]
loadDistFunctions?: boolean
// TODO(serhalp): This is confusing. Refactor to accept entire settings or rename or something?
settings: Pick<ServerSettings, 'functions' | 'functionsPort'>
Expand All @@ -319,6 +321,7 @@ export const startFunctionsServer = async (
command,
config,
debug,
generatedFunctions,
loadDistFunctions,
settings,
site,
Expand Down Expand Up @@ -380,6 +383,7 @@ export const startFunctionsServer = async (
config,
debug,
frameworksAPIPaths: command.netlify.frameworksAPIPaths,
generatedFunctions,
isConnected: Boolean(siteUrl),
logLambdaCompat: isFeatureFlagEnabled('cli_log_lambda_compat', siteInfo),
manifest,
Expand Down
Loading
Loading