Skip to content
Merged
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
2,172 changes: 1,049 additions & 1,123 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions packages/dev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@
"vitest": "^3.0.0"
},
"dependencies": {
"@netlify/blobs": "9.1.1",
"@netlify/config": "^22.0.1",
"@netlify/blobs": "^9.1.1",
"@netlify/config": "^23.0.4",
"@netlify/dev-utils": "2.1.1",
"@netlify/functions": "3.1.8",
"@netlify/redirects": "1.1.3",
Expand Down
59 changes: 38 additions & 21 deletions packages/dev/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { promises as fs } from 'node:fs'
import path from 'node:path'
import process from 'node:process'

Expand Down Expand Up @@ -105,34 +106,15 @@ export class NetlifyDev {
this.#projectRoot = options.projectRoot ?? process.cwd()
}

private async getConfig() {
const configFilePath = path.resolve(this.#projectRoot, 'netlify.toml')
const configFileExists = await isFile(configFilePath)
const config = await resolveConfig({
config: configFileExists ? configFilePath : undefined,
context: 'dev',
cwd: process.cwd(),
host: this.#apiHost,
offline: !this.#siteID,
mode: 'cli',
repositoryRoot: this.#projectRoot,
scheme: this.#apiScheme,
siteId: this.#siteID,
token: this.#apiToken,
})

return config
}

async handle(request: Request) {
private async handleInEphemeralDirectory(request: Request, destPath: string) {
// Functions
const userFunctionsPath =
this.#config?.config.functionsDirectory ?? path.join(this.#projectRoot, 'netlify/functions')
const userFunctionsPathExists = await isDirectory(userFunctionsPath)
const functions = this.#features.functions
? new FunctionsHandler({
config: this.#config,
destPath: path.join(this.#projectRoot, '.netlify', 'functions-serve'),
destPath: destPath,
projectRoot: this.#projectRoot,
settings: {},
siteId: this.#siteID,
Expand Down Expand Up @@ -209,6 +191,41 @@ export class NetlifyDev {
}
}

private async getConfig() {
const configFilePath = path.resolve(this.#projectRoot, 'netlify.toml')
const configFileExists = await isFile(configFilePath)
const config = await resolveConfig({
config: configFileExists ? configFilePath : undefined,
context: 'dev',
cwd: process.cwd(),
host: this.#apiHost,
offline: !this.#siteID,
mode: 'cli',
repositoryRoot: this.#projectRoot,
scheme: this.#apiScheme,
siteId: this.#siteID,
token: this.#apiToken,
})

return config
}

async handle(request: Request) {
const servePath = path.join(this.#projectRoot, '.netlify', 'functions-serve')

await fs.mkdir(servePath, { recursive: true })

const destPath = await fs.mkdtemp(path.join(servePath, '_'))

try {
return await this.handleInEphemeralDirectory(request, destPath)
} finally {
try {
await fs.rm(destPath, { force: true, recursive: true })
} catch {}
}
}

get siteIsLinked() {
return Boolean(this.#siteID)
}
Expand Down
2 changes: 1 addition & 1 deletion packages/functions/dev/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { ExtendedRoute, FunctionResult, ModuleFormat } from '@netlify/zip-it-and
export interface FunctionBuilder {
build: ({ cache }: { cache: BuildCache }) => Promise<BuildResult | undefined>
builderName: string
target: string
}

export interface BuildResult {
Expand All @@ -17,6 +16,7 @@ export interface BuildResult {
runtimeAPIVersion?: number
srcFiles: string[]
schedule?: string
targetDirectory?: string
}

export type BuildCache = MemoizeCache<FunctionResult>
34 changes: 27 additions & 7 deletions packages/functions/dev/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import semver from 'semver'

import { BuildResult } from './builder.js'
import { Runtime } from './runtimes/index.js'
import { HandlerContext, HandlerEvent } from '../src/main.js'
import { lambdaEventFromWebRequest } from './runtimes/nodejs/lambda.js'
import { HandlerContext } from '../src/main.js'

export type FunctionBuildCache = MemoizeCache<FunctionResult>

const BACKGROUND_FUNCTION_SUFFIX = '-background'
const TYPESCRIPT_EXTENSIONS = new Set(['.cts', '.mts', '.ts'])
Expand Down Expand Up @@ -38,11 +39,14 @@ interface NetlifyFunctionOptions {
config: any
directory: string
displayName?: string
excludedRoutes?: Route[]
mainFile: string
name: string
projectRoot: string
routes?: ExtendedRoute[]
runtime: Runtime
settings: any
targetDirectory: string
timeoutBackground: number
timeoutSynchronous: number
}
Expand All @@ -59,6 +63,7 @@ export class NetlifyFunction {
private readonly directory: string
private readonly projectRoot: string
private readonly settings: any
private readonly targetDirectory: string
private readonly timeoutBackground: number
private readonly timeoutSynchronous: number

Expand All @@ -74,27 +79,36 @@ export class NetlifyFunction {
// and will get populated on every build.
private srcFiles = new Set<string>()

public excludedRoutes: Route[] | undefined
public routes: ExtendedRoute[] | undefined

constructor({
blobsContext,
config,
directory,
displayName,
excludedRoutes,
mainFile,
name,
projectRoot,
routes,
runtime,
settings,
targetDirectory,
timeoutBackground,
timeoutSynchronous,
}: NetlifyFunctionOptions) {
this.blobsContext = blobsContext
this.config = config
this.directory = directory
this.excludedRoutes = excludedRoutes
this.mainFile = mainFile
this.name = name
this.displayName = displayName ?? name
this.projectRoot = projectRoot
this.routes = routes
this.runtime = runtime
this.targetDirectory = targetDirectory
this.timeoutBackground = timeoutBackground
this.timeoutSynchronous = timeoutSynchronous
this.settings = settings
Expand Down Expand Up @@ -181,6 +195,7 @@ export class NetlifyFunction {
directory: this.directory,
func: this,
projectRoot: this.projectRoot,
targetDirectory: this.targetDirectory,
})
.then((buildFunction) => buildFunction({ cache }))

Expand All @@ -191,12 +206,13 @@ export class NetlifyFunction {
throw new Error(`Could not build function ${this.name}`)
}

const { includedFiles = [], schedule, srcFiles } = buildData
const { includedFiles = [], routes, schedule, srcFiles } = buildData
const srcFilesSet = new Set<string>(srcFiles)
const srcFilesDiff = this.getSrcFilesDiff(srcFilesSet)

this.buildData = buildData
this.buildError = null
this.routes = routes

this.srcFiles = srcFilesSet
this.schedule = schedule || this.schedule
Expand Down Expand Up @@ -238,7 +254,12 @@ export class NetlifyFunction {
}

// Invokes the function and returns its response object.
async invoke(request: Request, clientContext: HandlerContext['clientContext']) {
async invoke(request: Request, clientContext: HandlerContext['clientContext'], buildCache: FunctionBuildCache = {}) {
// If we haven't started building the function, do it now.
if (!this.buildQueue) {
this.build({ cache: buildCache })
}

await this.buildQueue

if (this.buildError) {
Expand Down Expand Up @@ -268,11 +289,10 @@ export class NetlifyFunction {
* @returns matched route
*/
async matchURLPath(rawPath: string, method: string) {
await this.buildQueue

let path = rawPath !== '/' && rawPath.endsWith('/') ? rawPath.slice(0, -1) : rawPath
path = path.toLowerCase()
const { excludedRoutes = [], routes = [] } = this.buildData ?? {}
const { excludedRoutes = [], routes = [] } = this

const matchingRoute = routes.find((route: ExtendedRoute) => {
if (route.methods && route.methods.length !== 0 && !route.methods.includes(method)) {
return false
Expand Down
39 changes: 13 additions & 26 deletions packages/functions/dev/main.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { Buffer } from 'node:buffer'

import type { EnvironmentContext as BlobsContext } from '@netlify/blobs'
import { Manifest } from '@netlify/zip-it-and-ship-it'
import { DevEventHandler } from '@netlify/dev-utils'

import type { NetlifyFunction } from './function.js'
import { FunctionsRegistry } from './registry.js'
import type { FunctionBuildCache, NetlifyFunction } from './function.js'
import { FunctionsRegistry, type FunctionRegistryOptions } from './registry.js'
import { headersObjectFromWebHeaders } from './runtimes/nodejs/lambda.js'
import { buildClientContext } from './server/client-context.js'

Expand All @@ -27,36 +23,27 @@ export interface FunctionMatch {
preferStatic: boolean
}

interface FunctionsHandlerOptions {
type FunctionsHandlerOptions = FunctionRegistryOptions & {
accountId?: string
blobsContext?: BlobsContext
destPath: string
config: any
debug?: boolean
eventHandler?: DevEventHandler
frameworksAPIFunctionsPath?: string
internalFunctionsPath?: string
manifest?: Manifest
projectRoot: string
siteId?: string
settings: any
timeouts: any
userFunctionsPath?: string
}

export class FunctionsHandler {
private accountID?: string
private buildCache: FunctionBuildCache
private registry: FunctionsRegistry
private scan: Promise<void>
private siteID?: string

constructor(options: FunctionsHandlerOptions) {
const registry = new FunctionsRegistry(options)
constructor({ accountId, siteId, userFunctionsPath, ...registryOptions }: FunctionsHandlerOptions) {
const registry = new FunctionsRegistry(registryOptions)

this.accountID = options.accountId
this.accountID = accountId
this.buildCache = {}
this.registry = registry
this.scan = registry.scan([options.userFunctionsPath])
this.siteID = options.siteId
this.scan = registry.scan([userFunctionsPath])
this.siteID = siteId
}

private async invoke(request: Request, func: NetlifyFunction) {
Expand All @@ -82,7 +69,7 @@ export class FunctionsHandler {

if (func.isBackground) {
// Background functions do not receive a clientContext
await func.invoke(request, {})
await func.invoke(request, {}, this.buildCache)

return new Response(null, { status: 202 })
}
Expand All @@ -100,10 +87,10 @@ export class FunctionsHandler {
newRequest.headers.set('user-agent', CLOCKWORK_USERAGENT)
newRequest.headers.set('x-nf-event', 'schedule')

return await func.invoke(newRequest, clientContext)
return await func.invoke(newRequest, clientContext, this.buildCache)
}

return await func.invoke(request, clientContext)
return await func.invoke(request, clientContext, this.buildCache)
}

async match(request: Request): Promise<FunctionMatch | undefined> {
Expand Down
12 changes: 8 additions & 4 deletions packages/functions/dev/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { runtimes } from './runtimes/index.js'
export const DEFAULT_FUNCTION_URL_EXPRESSION = /^\/.netlify\/(functions|builders)\/([^/]+).*/
const TYPES_PACKAGE = '@netlify/functions'

interface FunctionRegistryOptions {
export interface FunctionRegistryOptions {
blobsContext?: BlobsContext
destPath: string
config: any
Expand Down Expand Up @@ -236,7 +236,7 @@ export class FunctionsRegistry {
return { func: null, route: null }
}

const { routes = [] } = (await func.getBuildData()) ?? {}
const { routes = [] } = func

if (routes.length !== 0) {
this.handleEvent({
Expand Down Expand Up @@ -313,7 +313,7 @@ export class FunctionsRegistry {
} catch {
func.mainFile = join(unzippedDirectory, basename(manifestEntry.mainFile))
}
} else {
} else if (this.watch) {
this.buildFunctionAndWatchFiles(func, !isReload)
}

Expand Down Expand Up @@ -350,6 +350,7 @@ export class FunctionsRegistry {
},
configFileDirectories: [this.internalFunctionsPath].filter(Boolean) as string[],
config: this.config.functions,
parseISC: true,
})

// user-defined functions take precedence over internal functions,
Expand Down Expand Up @@ -384,7 +385,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, excludedRoutes, mainFile, name, routes, runtime: runtimeName }) => {
if (ignoredFunctions.has(name)) {
return
}
Expand Down Expand Up @@ -414,11 +415,14 @@ export class FunctionsRegistry {
config: this.config,
directory,
displayName,
excludedRoutes,
mainFile,
name,
projectRoot: this.projectRoot,
routes,
runtime,
settings: this.settings,
targetDirectory: this.destPath,
timeoutBackground: this.timeouts.backgroundFunctions,
timeoutSynchronous: this.timeouts.syncFunctions,
})
Expand Down
1 change: 1 addition & 0 deletions packages/functions/dev/runtimes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface GetBuildFunctionOptions {
directory: string
func: NetlifyFunction
projectRoot: string
targetDirectory: string
}

export interface InvokeFunctionOptions {
Expand Down
Loading
Loading