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
8 changes: 8 additions & 0 deletions packages/build/src/core/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ const tExecBuild = async function ({
timers: timersB,
configMutations,
metrics,
returnValues,
} = await runAndReportBuild({
pluginsOptions,
netlifyConfig,
Expand Down Expand Up @@ -222,6 +223,7 @@ const tExecBuild = async function ({
timers: timersB,
configMutations,
metrics,
returnValues,
}
}

Expand Down Expand Up @@ -285,6 +287,7 @@ export const runAndReportBuild = async function ({
timers: timersA,
configMutations,
metrics,
returnValues,
} = await initAndRunBuild({
pluginsOptions,
netlifyConfig,
Expand Down Expand Up @@ -370,6 +373,7 @@ export const runAndReportBuild = async function ({
timers: timersA,
configMutations,
metrics,
returnValues,
}
} catch (error) {
const [{ statuses }] = getErrorInfo(error)
Expand Down Expand Up @@ -502,6 +506,7 @@ const initAndRunBuild = async function ({
timers: timersC,
configMutations,
metrics,
returnValues,
} = await runBuild({
childProcesses,
pluginsOptions: pluginsOptionsA,
Expand Down Expand Up @@ -560,6 +565,7 @@ const initAndRunBuild = async function ({
timers: timersC,
configMutations,
metrics,
returnValues,
}
} finally {
// Terminate the child processes of plugins so that they don't linger after
Expand Down Expand Up @@ -651,6 +657,7 @@ const runBuild = async function ({
timers: timersB,
configMutations,
metrics,
returnValues,
} = await runSteps({
steps,
buildbotServerSocket,
Expand Down Expand Up @@ -698,5 +705,6 @@ const runBuild = async function ({
timers: timersB,
configMutations,
metrics,
returnValues,
}
}
16 changes: 14 additions & 2 deletions packages/build/src/core/dev.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { handleBuildError } from '../error/handle.js'
import { getGeneratedFunctions } from '../steps/return_values.js'

import { execBuild, startBuild } from './build.js'
import { getSeverity } from './severity.js'
Expand All @@ -8,7 +9,11 @@ export const startDev = async (devCommand, flags = {}) => {
const errorParams = { errorMonitor, mode, logs, debug, testOpts }

try {
const { netlifyConfig: netlifyConfigA, configMutations } = await execBuild({
const {
netlifyConfig: netlifyConfigA,
configMutations,
returnValues,
} = await execBuild({
...normalizedFlags,
errorMonitor,
errorParams,
Expand All @@ -21,7 +26,14 @@ export const startDev = async (devCommand, flags = {}) => {
})
const { success, severityCode } = getSeverity('success')

return { success, severityCode, netlifyConfig: netlifyConfigA, logs, configMutations }
return {
success,
severityCode,
netlifyConfig: netlifyConfigA,
logs,
configMutations,
generatedFunctions: getGeneratedFunctions(returnValues),
}
} catch (error) {
const { severity, message, stack } = await handleBuildError(error, errorParams)
const { success, severityCode } = getSeverity(severity)
Expand Down
11 changes: 10 additions & 1 deletion packages/build/src/core/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { reportError } from '../error/report.js'
import { getSystemLogger } from '../log/logger.js'
import type { BufferedLogs } from '../log/logger.js'
import { logTimer, logBuildSuccess } from '../log/messages/core.js'
import { getGeneratedFunctions } from '../steps/return_values.js'
import { trackBuildComplete } from '../telemetry/main.js'
import { reportTimers } from '../time/report.js'
import { RootExecutionAttributes } from '../tracing/main.js'
Expand Down Expand Up @@ -72,6 +73,7 @@ export async function buildSite(flags: Partial<BuildFlags> = {}): Promise<{
durationNs,
configMutations,
metrics,
returnValues,
} = await execBuild({
...flagsA,
buildId,
Expand Down Expand Up @@ -117,7 +119,14 @@ export async function buildSite(flags: Partial<BuildFlags> = {}): Promise<{
testOpts,
errorParams,
})
return { success, severityCode, netlifyConfig: netlifyConfigA, logs, configMutations }
return {
success,
severityCode,
netlifyConfig: netlifyConfigA,
logs,
configMutations,
generatedFunctions: getGeneratedFunctions(returnValues),
}
} catch (error) {
const { severity } = await handleBuildError(error, errorParams as any)
const { pluginsOptions, siteInfo, userNodeVersion }: any = errorParams
Expand Down
21 changes: 13 additions & 8 deletions packages/build/src/log/messages/core_steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,22 @@ export const logFunctionsToBundle = function ({
}) {
let needsSpace = false

if (generatedFunctions.length !== 0) {
for (const id in generatedFunctions) {
const { displayName, generatorType, functionNames } = generatedFunctions[id]
for (const id in generatedFunctions) {
if (generatedFunctions[id].length === 0) {
continue
}

if (needsSpace) log(logs, '')
// Getting the generator block from the first function, since it will be
// the same for all of them.
const { generator } = generatedFunctions[id][0]
const functionNames = generatedFunctions[id].map((func) => path.basename(func.path))

log(logs, `Packaging ${type} generated by ${THEME.highlightWords(displayName)} ${generatorType}:`)
logArray(logs, functionNames, { indent: false })
if (needsSpace) log(logs, '')

needsSpace = true
}
log(logs, `Packaging ${type} generated by ${THEME.highlightWords(generator.displayName)} ${generator.type}:`)
logArray(logs, functionNames, { indent: false })

needsSpace = true
}

if (internalFunctions.length !== 0) {
Expand Down
2 changes: 1 addition & 1 deletion packages/build/src/plugins_core/edge_functions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ const logFunctions = async ({
internalFunctionsSrc: internalSrcDirectory,
frameworkFunctions: frameworkFunctions.map(({ name }) => name),
type: 'Edge Functions',
generatedFunctions: [],
generatedFunctions: {},
})
}

Expand Down
42 changes: 14 additions & 28 deletions packages/build/src/plugins_core/functions/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { basename, resolve } from 'path'
import { resolve } from 'path'

import { type NodeBundlerName, RUNTIME, zipFunctions, type FunctionResult } from '@netlify/zip-it-and-ship-it'
import { pathExists } from 'path-exists'

import { addErrorInfo } from '../../error/info.js'
import { log } from '../../log/logger.js'
import type { ReturnValue } from '../../types/step.js'
import { type GeneratedFunction, getGeneratedFunctions } from '../../steps/return_values.js'
import { logBundleResults, logFunctionsNonExistingDir, logFunctionsToBundle } from '../../log/messages/core_steps.js'
import { FRAMEWORKS_API_FUNCTIONS_ENDPOINT } from '../../utils/frameworks_api.js'

Expand Down Expand Up @@ -168,7 +168,7 @@ const coreStep = async function ({
}
}

const generatedFunctions = getGeneratedFunctionPaths(returnValues)
const generatedFunctions = getGeneratedFunctions(returnValues)

logFunctionsToBundle({
logs,
Expand All @@ -178,7 +178,7 @@ const coreStep = async function ({
internalFunctions,
internalFunctionsSrc: relativeInternalFunctionsSrc,
frameworkFunctions,
generatedFunctions: getGeneratedFunctionsByGenerator(returnValues),
generatedFunctions: getGeneratedFunctionsByGenerator(generatedFunctions),
})

if (
Expand All @@ -205,7 +205,7 @@ const coreStep = async function ({
repositoryRoot,
userNodeVersion,
systemLog,
generatedFunctions,
generatedFunctions: generatedFunctions.map((func) => func.path),
})

const metrics = getMetrics(internalFunctions, userFunctions)
Expand Down Expand Up @@ -257,31 +257,17 @@ const hasFunctionsDirectories = async function ({
return false
}

// Takes a list of return values and produces an array with the paths of all
// generated functions.
const getGeneratedFunctionPaths = (returnValues: Record<string, ReturnValue>) => {
return Object.values(returnValues).flatMap(
(returnValue) => returnValue.generatedFunctions?.map((func) => func.path) || [],
)
}

// Takes a list of return values and produces an object with the names of the
// generated functions for each generator. This is used for printing logs only.
const getGeneratedFunctionsByGenerator = (returnValues: Record<string, ReturnValue>) => {
const result: Record<string, { displayName: string; generatorType: string; functionNames: string[] }> = {}

for (const id in returnValues) {
const { displayName, generatedFunctions, generatorType } = returnValues[id]

if (!generatedFunctions || generatedFunctions.length === 0) {
continue
}

result[id] = {
displayName,
generatorType,
functionNames: generatedFunctions.map((func) => basename(func.path)),
}
const getGeneratedFunctionsByGenerator = (
generatedFunctions: GeneratedFunction[],
): Record<string, GeneratedFunction[]> => {
const result: Record<string, GeneratedFunction[]> = {}

for (const func of generatedFunctions) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
result[func.generator.name] = result[func.generator.name] || []
Comment on lines +265 to +269
Copy link
Member

@serhalp serhalp Jul 4, 2025

Choose a reason for hiding this comment

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

FWIW this is because given f: Record<string, Foo> with noUncheckedIndexAccess disabled, f[string] results in the type Foo, not Foo | undefined. ESLint is being pedantically correct here (arguably).

You can avoid the suppression this way:

Suggested change
const result: Record<string, GeneratedFunction[]> = {}
for (const func of generatedFunctions) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
result[func.generator.name] = result[func.generator.name] || []
const result: Record<string, GeneratedFunction[] | undefined> = {}
for (const func of generatedFunctions) {
result[func.generator.name] = result[func.generator.name] || []

Copy link
Member Author

Choose a reason for hiding this comment

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

But that's not the type I want to return. I don't want consumers of this method to think that some keys will have the value undefined, because that shouldn't be the case.

Copy link
Member

Choose a reason for hiding this comment

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

It's problematic either way. It's just a limitation of TypeScript's type system without noUncheckedIndexAccess:

  • With your type, a user dereferencing result['kjfandsakj'] will get GeneratedFunction[].
  • With my type, a user iterating over result's own keys and dereferencing result[key] will get GeneratedFunction[] | undefined.

Both cases are not what we intend to express 😞. The latter is safer, but way more annoying.

https://claude.ai/share/a8fbb3cb-bd72-4761-a2e6-1d55e876889c

The only good solution is enabling noUncheckedIndexAccess.

(To be clear, this is absolutely nonblocking tangential rambling—please proceed with merging!)

result[func.generator.name].push(func)
}

return result
Expand Down
33 changes: 33 additions & 0 deletions packages/build/src/steps/return_values.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
type GeneratorType = 'build plugin' | 'extension'

export interface GeneratedFunction {
generator: {
displayName: string
name: string
type: GeneratorType
}
path: string
}

export interface ReturnValue {
displayName?: string
generatedFunctions?: { path: string }[]
generatorType: GeneratorType
}

export const getGeneratedFunctions = (returnValues?: Record<string, ReturnValue>): GeneratedFunction[] => {
return Object.entries(returnValues ?? {}).flatMap(([name, returnValue]) => {
const generator = {
displayName: returnValue.displayName ?? name,
name,
type: returnValue.generatorType,
}

return (
returnValue.generatedFunctions?.map((func) => ({
generator,
path: func.path,
})) ?? []
)
})
}
2 changes: 1 addition & 1 deletion packages/build/src/steps/run_steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export const runSteps = async function ({

const statusesA = addStatus({ newStatus, statuses, event, packageName, pluginPackageJson })

/** @type import('../types/step.js').ReturnValue */
/** @type import('../steps/return_values.js').ReturnValue */
const augmentedReturnValue = returnValue
? {
...returnValue,
Expand Down
9 changes: 0 additions & 9 deletions packages/build/src/types/step.ts

This file was deleted.

15 changes: 13 additions & 2 deletions packages/build/tests/core/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -460,15 +460,18 @@ test.serial('Passes the right properties to zip-it-and-ship-it', async (t) => {
})

test.serial('Passes functions generated by build plugins to zip-it-and-ship-it', async (t) => {
const mockZipFunctions = sinon.stub().resolves()
const mockZipFunctions = sinon.stub().resolves([])
const stub = sinon.stub(zipItAndShipIt, 'zipFunctions').get(() => mockZipFunctions)
const fixtureName = 'functions_generated_from_steps'
const fixtureDir = join(FIXTURES_DIR, fixtureName)

await new Fixture(`./fixtures/${fixtureName}`).withFlags({ mode: 'buildbot' }).runWithBuild()
const { success, generatedFunctions } = await new Fixture(`./fixtures/${fixtureName}`)
.withFlags({ mode: 'buildbot' })
.runWithBuildAndIntrospect()

stub.restore()

t.true(success)
t.is(mockZipFunctions.callCount, 1)

const { generated, user } = mockZipFunctions.firstCall.args[0]
Expand All @@ -487,6 +490,14 @@ test.serial('Passes functions generated by build plugins to zip-it-and-ship-it',
t.is(user.directories.length, 1)
t.true(user.directories.includes(resolve(fixtureDir, 'netlify/functions')))
t.is(user.functions, undefined)

t.is(generatedFunctions.length, 1)
t.deepEqual(generatedFunctions[0].generator, {
displayName: './.netlify/plugins/node_modules/plugin/plugin.mjs',
name: './.netlify/plugins/node_modules/plugin/plugin.mjs',
type: 'build plugin',
})
t.is(generatedFunctions[0].path, join(fixtureDir, '.netlify/plugins/node_modules/plugin/functions/plugin-func1.mjs'))
})

test.serial('Passes the right feature flags to zip-it-and-ship-it', async (t) => {
Expand Down
Loading