UNPKG

@netlify/build

Version:
187 lines (186 loc) • 7.92 kB
import { resolve } from 'path'; import { RUNTIME, zipFunctions } 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 { logBundleResults, logFunctionsNonExistingDir, logFunctionsToBundle } from '../../log/messages/core_steps.js'; import { FRAMEWORKS_API_FUNCTIONS_ENDPOINT } from '../../utils/frameworks_api.js'; import { getZipError } from './error.js'; import { getUserAndInternalFunctions, validateFunctionsSrc } from './utils.js'; import { getZisiParameters } from './zisi.js'; // Get a list of all unique bundlers in this run const getBundlers = (results = []) => // using a Set to filter duplicates new Set(results .map((bundle) => (bundle.runtime === RUNTIME.JAVASCRIPT ? bundle.bundler : null)) .filter(Boolean)); // see https://docs.netlify.com/functions/trigger-on-events/#available-triggers const eventTriggeredFunctions = new Set([ 'deploy-building', 'deploy-succeeded', 'deploy-failed', 'deploy-deleted', 'deploy-locked', 'deploy-unlocked', 'submission-created', 'split-test-activated', 'split-test-deactivated', 'split-test-modified', 'identity-validate', 'identity-signup', 'identity-login', ]); const validateCustomRoutes = function (functions) { for (const { routes, name, schedule } of functions) { if (!routes || routes.length === 0) continue; if (schedule) { const error = new Error(`Scheduled functions must not specify a custom path. Please remove the "path" configuration. Learn more about scheduled functions at https://ntl.fyi/custom-path-scheduled-functions.`); addErrorInfo(error, { type: 'resolveConfig' }); throw error; } if (eventTriggeredFunctions.has(name.toLowerCase().replace('-background', ''))) { const error = new Error(`Event-triggered functions must not specify a custom path. Please remove the "path" configuration or pick a different name for the function. Learn more about event-triggered functions at https://ntl.fyi/custom-path-event-triggered-functions.`); addErrorInfo(error, { type: 'resolveConfig' }); throw error; } } }; const zipFunctionsAndLogResults = async ({ branch, buildDir, childEnv, featureFlags, functionsConfig, functionsDist, functionsSrc, frameworkFunctionsSrc, internalFunctionsSrc, isRunningLocally, logs, repositoryRoot, userNodeVersion, systemLog, }) => { const zisiParameters = getZisiParameters({ branch, buildDir, childEnv, featureFlags, functionsConfig, functionsDist, internalFunctionsSrc, isRunningLocally, repositoryRoot, userNodeVersion, systemLog, }); try { // Printing an empty line before bundling output. log(logs, ''); const sourceDirectories = [internalFunctionsSrc, frameworkFunctionsSrc, functionsSrc].filter(Boolean); const results = await zipItAndShipIt.zipFunctions(sourceDirectories, functionsDist, zisiParameters); validateCustomRoutes(results); const bundlers = Array.from(getBundlers(results)); logBundleResults({ logs, results }); return { bundlers }; } catch (error) { throw await getZipError(error, functionsSrc); } }; // Plugin to package Netlify functions with @netlify/zip-it-and-ship-it const coreStep = async function ({ childEnv, constants: { INTERNAL_FUNCTIONS_SRC: relativeInternalFunctionsSrc, IS_LOCAL: isRunningLocally, FUNCTIONS_SRC: relativeFunctionsSrc, FUNCTIONS_DIST: relativeFunctionsDist, }, buildDir, branch, packagePath, logs, netlifyConfig, featureFlags, repositoryRoot, userNodeVersion, systemLog, }) { const functionsSrc = relativeFunctionsSrc === undefined ? undefined : resolve(buildDir, relativeFunctionsSrc); const functionsDist = resolve(buildDir, relativeFunctionsDist); const internalFunctionsSrc = resolve(buildDir, relativeInternalFunctionsSrc); const internalFunctionsSrcExists = await pathExists(internalFunctionsSrc); const frameworkFunctionsSrc = resolve(buildDir, packagePath || '', FRAMEWORKS_API_FUNCTIONS_ENDPOINT); const frameworkFunctionsSrcExists = await pathExists(frameworkFunctionsSrc); const functionsSrcExists = await validateFunctionsSrc({ functionsSrc, relativeFunctionsSrc }); const [userFunctions = [], internalFunctions = [], frameworkFunctions = []] = await getUserAndInternalFunctions({ featureFlags, functionsSrc, functionsSrcExists, internalFunctionsSrc, internalFunctionsSrcExists, frameworkFunctionsSrc, frameworkFunctionsSrcExists, }); if (functionsSrc && !functionsSrcExists) { logFunctionsNonExistingDir(logs, relativeFunctionsSrc); if (internalFunctions.length !== 0) { log(logs, ''); } } logFunctionsToBundle({ logs, userFunctions, userFunctionsSrc: relativeFunctionsSrc, userFunctionsSrcExists: functionsSrcExists, internalFunctions, internalFunctionsSrc: relativeInternalFunctionsSrc, frameworkFunctions, }); if (userFunctions.length === 0 && internalFunctions.length === 0 && frameworkFunctions.length === 0) { return {}; } const { bundlers } = await zipFunctionsAndLogResults({ branch, buildDir, childEnv, featureFlags, functionsConfig: netlifyConfig.functions, functionsDist, functionsSrc, frameworkFunctionsSrc, internalFunctionsSrc, isRunningLocally, logs, repositoryRoot, userNodeVersion, systemLog, }); const metrics = getMetrics(internalFunctions, userFunctions); return { tags: { bundler: bundlers, }, metrics, }; }; // We run this core step if at least one of the functions directories (the // one configured by the user or the internal one) exists. We use a dynamic // `condition` because the directories might be created by the build command // or plugins. const hasFunctionsDirectories = async function ({ buildDir, constants: { INTERNAL_FUNCTIONS_SRC, FUNCTIONS_SRC }, packagePath, }) { const hasFunctionsSrc = FUNCTIONS_SRC !== undefined && FUNCTIONS_SRC !== ''; if (hasFunctionsSrc) { return true; } const internalFunctionsSrc = resolve(buildDir, INTERNAL_FUNCTIONS_SRC); if (await pathExists(internalFunctionsSrc)) { return true; } const frameworkFunctionsSrc = resolve(buildDir, packagePath || '', FRAMEWORKS_API_FUNCTIONS_ENDPOINT); return await pathExists(frameworkFunctionsSrc); }; export const bundleFunctions = { event: 'onBuild', coreStep, coreStepId: 'functions_bundling', coreStepName: 'Functions bundling', coreStepDescription: () => 'Functions bundling', condition: hasFunctionsDirectories, }; // Named imports with ES modules cannot be mocked (unlike CommonJS) because // they are bound at load time. // However, some of our tests are asserting which arguments are passed to // `zip-it-and-ship-it` methods. Therefore, we need to use an intermediary // function and export them so tests can use it. export const zipItAndShipIt = { async zipFunctions(...args) { return await zipFunctions(...args); }, }; const getMetrics = (internalFunctions, userFunctions) => { return [ { type: 'increment', name: 'buildbot.build.functions', value: internalFunctions.length, tags: { type: 'lambda:generated' }, }, { type: 'increment', name: 'buildbot.build.functions', value: userFunctions.length, tags: { type: 'lambda:user' }, }, ]; };