@netlify/build 27.18.5-rc → 27.18.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/package.json +8 -9
  2. package/src/core/bin.js +83 -0
  3. package/src/core/build.js +559 -0
  4. package/src/core/config.js +186 -0
  5. package/src/core/constants.js +156 -0
  6. package/src/core/dev.js +31 -0
  7. package/src/core/dry.js +39 -0
  8. package/src/core/feature_flags.js +22 -0
  9. package/src/core/flags.js +204 -0
  10. package/src/core/lingering.js +85 -0
  11. package/src/core/main.js +165 -0
  12. package/src/core/missing_side_file.js +29 -0
  13. package/src/core/normalize_flags.js +70 -0
  14. package/src/core/severity.js +22 -0
  15. package/src/core/user_node_version.js +41 -0
  16. package/src/env/changes.js +52 -0
  17. package/src/env/main.js +19 -0
  18. package/src/env/metadata.js +81 -0
  19. package/src/error/api.js +46 -0
  20. package/src/error/build.js +50 -0
  21. package/src/error/cancel.js +8 -0
  22. package/src/error/colors.js +11 -0
  23. package/src/error/handle.js +57 -0
  24. package/src/error/info.js +46 -0
  25. package/src/error/monitor/location.js +21 -0
  26. package/src/error/monitor/normalize.js +96 -0
  27. package/src/error/monitor/print.js +42 -0
  28. package/src/error/monitor/report.js +138 -0
  29. package/src/error/monitor/start.js +69 -0
  30. package/src/error/parse/clean_stack.js +87 -0
  31. package/src/error/parse/location.js +62 -0
  32. package/src/error/parse/normalize.js +29 -0
  33. package/src/error/parse/parse.js +97 -0
  34. package/src/error/parse/plugin.js +70 -0
  35. package/src/error/parse/properties.js +23 -0
  36. package/src/error/parse/serialize_log.js +42 -0
  37. package/src/error/parse/serialize_status.js +23 -0
  38. package/src/error/parse/stack.js +43 -0
  39. package/src/error/type.js +189 -0
  40. package/src/install/functions.js +28 -0
  41. package/src/install/local.js +62 -0
  42. package/src/install/main.js +81 -0
  43. package/src/install/missing.js +67 -0
  44. package/src/log/colors.js +34 -0
  45. package/src/log/description.js +26 -0
  46. package/src/log/header.js +16 -0
  47. package/src/log/header_func.js +17 -0
  48. package/src/log/logger.js +161 -0
  49. package/src/log/messages/compatibility.js +178 -0
  50. package/src/log/messages/config.js +107 -0
  51. package/src/log/messages/core.js +70 -0
  52. package/src/log/messages/core_steps.js +104 -0
  53. package/src/log/messages/dry.js +63 -0
  54. package/src/log/messages/install.js +32 -0
  55. package/src/log/messages/ipc.js +38 -0
  56. package/src/log/messages/mutations.js +82 -0
  57. package/src/log/messages/plugins.js +39 -0
  58. package/src/log/messages/status.js +16 -0
  59. package/src/log/messages/steps.js +22 -0
  60. package/src/log/old_version.js +41 -0
  61. package/src/log/serialize.js +13 -0
  62. package/src/log/stream.js +85 -0
  63. package/src/log/theme.js +26 -0
  64. package/src/plugins/child/diff.js +55 -0
  65. package/src/plugins/child/error.js +32 -0
  66. package/src/plugins/child/lazy.js +18 -0
  67. package/src/plugins/child/load.js +29 -0
  68. package/src/plugins/child/logic.js +65 -0
  69. package/src/plugins/child/main.js +51 -0
  70. package/src/plugins/child/run.js +28 -0
  71. package/src/plugins/child/status.js +74 -0
  72. package/src/plugins/child/typescript.js +45 -0
  73. package/src/plugins/child/utils.js +56 -0
  74. package/src/plugins/child/validate.js +34 -0
  75. package/src/plugins/compatibility.js +132 -0
  76. package/src/plugins/error.js +50 -0
  77. package/src/plugins/events.js +17 -0
  78. package/src/plugins/expected_version.js +119 -0
  79. package/src/plugins/ipc.js +145 -0
  80. package/src/plugins/list.js +86 -0
  81. package/src/plugins/load.js +70 -0
  82. package/src/plugins/manifest/check.js +106 -0
  83. package/src/plugins/manifest/load.js +41 -0
  84. package/src/plugins/manifest/main.js +22 -0
  85. package/src/plugins/manifest/path.js +31 -0
  86. package/src/plugins/manifest/validate.js +108 -0
  87. package/src/plugins/node_version.js +50 -0
  88. package/src/plugins/options.js +88 -0
  89. package/src/plugins/pinned_version.js +131 -0
  90. package/src/plugins/resolve.js +152 -0
  91. package/src/plugins/spawn.js +72 -0
  92. package/src/plugins_core/add.js +49 -0
  93. package/src/plugins_core/build_command.js +75 -0
  94. package/src/plugins_core/deploy/buildbot_client.js +113 -0
  95. package/src/plugins_core/deploy/index.js +73 -0
  96. package/src/plugins_core/deploy/manifest.yml +1 -0
  97. package/src/plugins_core/edge_functions/index.js +123 -0
  98. package/src/plugins_core/edge_functions/lib/error.js +21 -0
  99. package/src/plugins_core/edge_functions/lib/internal_manifest.js +60 -0
  100. package/src/plugins_core/edge_functions/validate_manifest/validate_edge_functions_manifest.js +89 -0
  101. package/src/plugins_core/functions/error.js +163 -0
  102. package/src/plugins_core/functions/feature_flags.js +6 -0
  103. package/src/plugins_core/functions/index.js +161 -0
  104. package/src/plugins_core/functions/utils.js +66 -0
  105. package/src/plugins_core/functions/zisi.js +56 -0
  106. package/src/plugins_core/functions_install/index.js +13 -0
  107. package/src/plugins_core/functions_install/manifest.yml +1 -0
  108. package/src/plugins_core/list.js +27 -0
  109. package/src/status/add.js +36 -0
  110. package/src/status/colors.js +23 -0
  111. package/src/status/load_error.js +11 -0
  112. package/src/status/report.js +137 -0
  113. package/src/status/success.js +18 -0
  114. package/src/steps/core_step.js +92 -0
  115. package/src/steps/error.js +102 -0
  116. package/src/steps/get.js +51 -0
  117. package/src/steps/plugin.js +85 -0
  118. package/src/steps/return.js +52 -0
  119. package/src/steps/run_core_steps.js +200 -0
  120. package/src/steps/run_step.js +304 -0
  121. package/src/steps/run_steps.js +179 -0
  122. package/src/steps/update_config.js +93 -0
  123. package/src/telemetry/main.js +136 -0
  124. package/src/time/aggregate.js +146 -0
  125. package/src/time/main.js +48 -0
  126. package/src/time/measure.js +22 -0
  127. package/src/time/report.js +59 -0
  128. package/src/utils/errors.js +12 -0
  129. package/src/utils/json.js +19 -0
  130. package/src/utils/omit.js +6 -0
  131. package/src/utils/package.js +23 -0
  132. package/src/utils/remove_falsy.js +10 -0
  133. package/src/utils/resolve.js +46 -0
  134. package/src/utils/runtime.js +5 -0
  135. package/src/utils/semver.js +34 -0
@@ -0,0 +1,29 @@
1
+ import filterObj from 'filter-obj'
2
+
3
+ import { getLogic } from './logic.js'
4
+ import { registerTypeScript } from './typescript.js'
5
+ import { validatePlugin } from './validate.js'
6
+
7
+ // Load context passed to every plugin method.
8
+ // This also requires the plugin file and fire its top-level function.
9
+ // This also validates the plugin.
10
+ // Do it when parent requests it using the `load` event.
11
+ // Also figure out the list of plugin steps. This is also passed to the parent.
12
+ export const load = async function ({ pluginPath, inputs, packageJson, verbose }) {
13
+ const tsNodeService = registerTypeScript(pluginPath)
14
+ const logic = await getLogic({ pluginPath, inputs, tsNodeService })
15
+
16
+ validatePlugin(logic)
17
+
18
+ const methods = filterObj(logic, isEventHandler)
19
+ const events = Object.keys(methods)
20
+
21
+ // Context passed to every event handler
22
+ const context = { methods, inputs, packageJson, verbose }
23
+
24
+ return { events, context }
25
+ }
26
+
27
+ const isEventHandler = function (event, value) {
28
+ return typeof value === 'function'
29
+ }
@@ -0,0 +1,65 @@
1
+ import { createRequire } from 'module'
2
+ import { pathToFileURL } from 'url'
3
+
4
+ import { ROOT_PACKAGE_JSON } from '../../utils/json.js'
5
+ import { DEV_EVENTS, EVENTS } from '../events.js'
6
+
7
+ import { addTsErrorInfo } from './typescript.js'
8
+
9
+ const require = createRequire(import.meta.url)
10
+
11
+ // Require the plugin file and fire its top-level function.
12
+ // The returned object is the `logic` which includes all event handlers.
13
+ export const getLogic = async function ({ pluginPath, inputs, tsNodeService }) {
14
+ const logic = await importLogic(pluginPath, tsNodeService)
15
+ const logicA = loadLogic({ logic, inputs })
16
+ return logicA
17
+ }
18
+
19
+ const importLogic = async function (pluginPath, tsNodeService) {
20
+ try {
21
+ // `ts-node` is not available programmatically for pure ES modules yet,
22
+ // which is currently making it impossible for local plugins to use both
23
+ // pure ES modules and TypeScript.
24
+ if (tsNodeService !== undefined) {
25
+ // eslint-disable-next-line import/no-dynamic-require
26
+ return require(pluginPath)
27
+ }
28
+
29
+ // `pluginPath` is an absolute file path but `import()` needs URLs.
30
+ // Converting those with `pathToFileURL()` is needed especially on Windows
31
+ // where the drive letter would not work with `import()`.
32
+ const returnValue = await import(pathToFileURL(pluginPath))
33
+ // Plugins should use named exports, but we still support default exports
34
+ // for backward compatibility with CommonJS
35
+ return returnValue.default === undefined ? returnValue : returnValue.default
36
+ } catch (error) {
37
+ addTsErrorInfo(error, tsNodeService)
38
+ // We must change `error.stack` instead of `error.message` because some
39
+ // errors thrown from `import()` access `error.stack` before throwing.
40
+ // `error.stack` is lazily instantiated by Node.js, so changing
41
+ // `error.message` afterwards would not modify `error.stack`. Therefore, the
42
+ // resulting stack trace, which is printed in the build logs, would not
43
+ // include the additional message prefix.
44
+ error.stack = `Could not import plugin:\n${error.stack}`
45
+ throw error
46
+ }
47
+ }
48
+
49
+ const loadLogic = function ({ logic, inputs }) {
50
+ if (typeof logic !== 'function') {
51
+ return logic
52
+ }
53
+
54
+ const metadata = {
55
+ events: new Set([...DEV_EVENTS, ...EVENTS]),
56
+ version: ROOT_PACKAGE_JSON.version,
57
+ }
58
+
59
+ try {
60
+ return logic(inputs, metadata)
61
+ } catch (error) {
62
+ error.message = `Could not load plugin:\n${error.message}`
63
+ throw error
64
+ }
65
+ }
@@ -0,0 +1,51 @@
1
+ import { setInspectColors } from '../../log/colors.js'
2
+ import { sendEventToParent, getEventsFromParent } from '../ipc.js'
3
+
4
+ import { handleProcessErrors, handleError } from './error.js'
5
+ import { load } from './load.js'
6
+ import { run } from './run.js'
7
+
8
+ // Boot plugin child process.
9
+ const bootPlugin = async function () {
10
+ const state = { context: { verbose: false } }
11
+
12
+ try {
13
+ handleProcessErrors()
14
+ setInspectColors()
15
+
16
+ // We need to fire them in parallel because `process.send()` can be slow
17
+ // to await, i.e. parent might send `load` event before child `ready` event
18
+ // returns.
19
+ await Promise.all([handleEvents(state), sendEventToParent('ready', {}, false)])
20
+ } catch (error) {
21
+ await handleError(error, state.context.verbose)
22
+ }
23
+ }
24
+
25
+ // Wait for events from parent to perform plugin methods
26
+ const handleEvents = async function (state) {
27
+ await getEventsFromParent((callId, eventName, payload) => handleEvent({ callId, eventName, payload, state }))
28
+ }
29
+
30
+ // Each event can pass `context` information to the next event
31
+ const handleEvent = async function ({
32
+ callId,
33
+ eventName,
34
+ payload,
35
+ state,
36
+ state: {
37
+ context: { verbose },
38
+ },
39
+ }) {
40
+ try {
41
+ const { context, ...response } = await EVENTS[eventName](payload, state.context)
42
+ state.context = { ...state.context, ...context }
43
+ await sendEventToParent(callId, response, verbose)
44
+ } catch (error) {
45
+ await handleError(error, verbose)
46
+ }
47
+ }
48
+
49
+ const EVENTS = { load, run }
50
+
51
+ bootPlugin()
@@ -0,0 +1,28 @@
1
+ import { getNewEnvChanges, setEnvChanges } from '../../env/changes.js'
2
+ import { logPluginMethodStart, logPluginMethodEnd } from '../../log/messages/ipc.js'
3
+
4
+ import { cloneNetlifyConfig, getConfigMutations } from './diff.js'
5
+ import { getUtils } from './utils.js'
6
+
7
+ // Run a specific plugin event handler
8
+ export const run = async function (
9
+ { event, error, constants, envChanges, netlifyConfig },
10
+ { methods, inputs, packageJson, verbose },
11
+ ) {
12
+ const method = methods[event]
13
+ const runState = {}
14
+ const utils = getUtils({ event, constants, runState })
15
+ const netlifyConfigCopy = cloneNetlifyConfig(netlifyConfig)
16
+ const runOptions = { utils, constants, inputs, netlifyConfig: netlifyConfigCopy, packageJson, error }
17
+
18
+ const envBefore = setEnvChanges(envChanges)
19
+
20
+ logPluginMethodStart(verbose)
21
+ await method(runOptions)
22
+ logPluginMethodEnd(verbose)
23
+
24
+ const newEnvChanges = getNewEnvChanges(envBefore, netlifyConfig, netlifyConfigCopy)
25
+
26
+ const configMutations = getConfigMutations(netlifyConfig, netlifyConfigCopy, event)
27
+ return { ...runState, newEnvChanges, configMutations }
28
+ }
@@ -0,0 +1,74 @@
1
+ import isPlainObj from 'is-plain-obj'
2
+ import mapObj from 'map-obj'
3
+
4
+ import { addErrorInfo } from '../../error/info.js'
5
+
6
+ // Report status information to the UI
7
+ export const show = function (runState, showArgs) {
8
+ validateShowArgs(showArgs)
9
+ const { title, summary, text, extraData } = removeEmptyStrings(showArgs)
10
+ runState.status = { state: 'success', title, summary, text, extraData }
11
+ }
12
+
13
+ // Validate arguments of `utils.status.show()`
14
+ const validateShowArgs = function (showArgs) {
15
+ try {
16
+ validateShowArgsObject(showArgs)
17
+ const { title, summary, text, extraData, ...otherArgs } = showArgs
18
+ validateShowArgsKeys(otherArgs)
19
+ Object.entries({ title, summary, text }).forEach(validateStringArg)
20
+ validateShowArgsSummary(summary)
21
+ validateShowArgsExtraData(extraData)
22
+ } catch (error) {
23
+ error.message = `utils.status.show() ${error.message}`
24
+ addErrorInfo(error, { type: 'pluginValidation' })
25
+ throw error
26
+ }
27
+ }
28
+
29
+ const validateShowArgsObject = function (showArgs) {
30
+ if (showArgs === undefined) {
31
+ throw new Error('requires an argument')
32
+ }
33
+
34
+ if (!isPlainObj(showArgs)) {
35
+ throw new Error('argument must be a plain object')
36
+ }
37
+ }
38
+
39
+ const validateShowArgsKeys = function (otherArgs) {
40
+ const otherKeys = Object.keys(otherArgs).map((arg) => `"${arg}"`)
41
+ if (otherKeys.length !== 0) {
42
+ throw new Error(`must only contain "title", "summary" or "text" properties, not ${otherKeys.join(', ')}`)
43
+ }
44
+ }
45
+
46
+ const validateStringArg = function ([key, value]) {
47
+ if (value !== undefined && typeof value !== 'string') {
48
+ throw new Error(`"${key}" property must be a string`)
49
+ }
50
+ }
51
+
52
+ const validateShowArgsSummary = function (summary) {
53
+ if (summary === undefined || summary.trim() === '') {
54
+ throw new Error('requires specifying a "summary" property')
55
+ }
56
+ }
57
+
58
+ const validateShowArgsExtraData = function (extraData) {
59
+ if (extraData !== undefined && Array.isArray(extraData) === false) {
60
+ throw new TypeError('provided extra data must be an array')
61
+ }
62
+ }
63
+
64
+ const removeEmptyStrings = function (showArgs) {
65
+ return mapObj(showArgs, removeEmptyString)
66
+ }
67
+
68
+ const removeEmptyString = function (key, value) {
69
+ if (typeof value === 'string' && value.trim() === '') {
70
+ return [key]
71
+ }
72
+
73
+ return [key, value]
74
+ }
@@ -0,0 +1,45 @@
1
+ import { extname } from 'path'
2
+
3
+ import { register } from 'ts-node'
4
+
5
+ import { addErrorInfo } from '../../error/info.js'
6
+
7
+ // Allow local plugins to be written with TypeScript.
8
+ // Local plugins cannot be transpiled by the build command since they can be run
9
+ // before it. Therefore, we type-check and transpile them automatically using
10
+ // `ts-node`.
11
+ export const registerTypeScript = function (pluginPath) {
12
+ if (!isTypeScriptPlugin(pluginPath)) {
13
+ return
14
+ }
15
+
16
+ return register()
17
+ }
18
+
19
+ // On TypeScript errors, adds information about the `ts-node` configuration,
20
+ // which includes the resolved `tsconfig.json`.
21
+ export const addTsErrorInfo = function (error, tsNodeService) {
22
+ if (tsNodeService === undefined) {
23
+ return
24
+ }
25
+
26
+ const {
27
+ config: {
28
+ raw: { compilerOptions },
29
+ },
30
+ options: realTsNodeOptions,
31
+ } = tsNodeService
32
+
33
+ // filter out functions as they cannot be serialized
34
+ const tsNodeOptions = Object.fromEntries(
35
+ Object.entries(realTsNodeOptions).filter(([, val]) => typeof val !== 'function'),
36
+ )
37
+
38
+ addErrorInfo(error, { tsConfig: { compilerOptions, tsNodeOptions } })
39
+ }
40
+
41
+ const isTypeScriptPlugin = function (pluginPath) {
42
+ return TYPESCRIPT_EXTENSIONS.has(extname(pluginPath))
43
+ }
44
+
45
+ const TYPESCRIPT_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts'])
@@ -0,0 +1,56 @@
1
+ import { bindOpts as cacheBindOpts } from '@netlify/cache-utils'
2
+ import { add as functionsAdd, list as functionsList, listAll as functionsListAll } from '@netlify/functions-utils'
3
+ import { getGitUtils } from '@netlify/git-utils'
4
+ import { run, runCommand } from '@netlify/run-utils'
5
+
6
+ import { failBuild, failPlugin, cancelBuild, failPluginWithWarning } from '../error.js'
7
+ import { isSoftFailEvent } from '../events.js'
8
+
9
+ import { addLazyProp } from './lazy.js'
10
+ import { show } from './status.js'
11
+
12
+ // Retrieve the `utils` argument.
13
+ export const getUtils = function ({
14
+ event,
15
+ constants: { FUNCTIONS_SRC, INTERNAL_FUNCTIONS_SRC, CACHE_DIR },
16
+ runState,
17
+ }) {
18
+ // eslint-disable-next-line fp/no-mutation
19
+ run.command = runCommand
20
+
21
+ const build = getBuildUtils(event)
22
+ const cache = getCacheUtils(CACHE_DIR)
23
+ const functions = getFunctionsUtils(FUNCTIONS_SRC, INTERNAL_FUNCTIONS_SRC)
24
+ const status = getStatusUtils(runState)
25
+ const utils = { build, cache, run, functions, status }
26
+ addLazyProp(utils, 'git', () => getGitUtils())
27
+ return utils
28
+ }
29
+
30
+ const getBuildUtils = function (event) {
31
+ if (isSoftFailEvent(event)) {
32
+ return {
33
+ failPlugin,
34
+ failBuild: failPluginWithWarning.bind(null, 'failBuild', event),
35
+ cancelBuild: failPluginWithWarning.bind(null, 'cancelBuild', event),
36
+ }
37
+ }
38
+
39
+ return { failBuild, failPlugin, cancelBuild }
40
+ }
41
+
42
+ const getCacheUtils = function (CACHE_DIR) {
43
+ return cacheBindOpts({ cacheDir: CACHE_DIR })
44
+ }
45
+
46
+ const getFunctionsUtils = function (FUNCTIONS_SRC, INTERNAL_FUNCTIONS_SRC) {
47
+ const functionsDirectories = [INTERNAL_FUNCTIONS_SRC, FUNCTIONS_SRC].filter(Boolean)
48
+ const add = (src) => functionsAdd(src, INTERNAL_FUNCTIONS_SRC, { fail: failBuild })
49
+ const list = functionsList.bind(null, functionsDirectories, { fail: failBuild })
50
+ const listAll = functionsListAll.bind(null, functionsDirectories, { fail: failBuild })
51
+ return { add, list, listAll }
52
+ }
53
+
54
+ const getStatusUtils = function (runState) {
55
+ return { show: show.bind(undefined, runState) }
56
+ }
@@ -0,0 +1,34 @@
1
+ import { addErrorInfo } from '../../error/info.js'
2
+ import { serializeArray } from '../../log/serialize.js'
3
+ import { DEV_EVENTS, EVENTS } from '../events.js'
4
+
5
+ // Validate the shape of a plugin return value
6
+ export const validatePlugin = function (logic) {
7
+ try {
8
+ // This validation must work with the return value of `import()` which has
9
+ // a `Module` prototype, not `Object`
10
+ if (typeof logic !== 'object' || logic === null) {
11
+ throw new Error('Plugin must be an object or a function')
12
+ }
13
+
14
+ Object.entries(logic).forEach(([propName, value]) => {
15
+ validateEventHandler(value, propName)
16
+ })
17
+ } catch (error) {
18
+ addErrorInfo(error, { type: 'pluginValidation' })
19
+ throw error
20
+ }
21
+ }
22
+
23
+ // All other properties are event handlers
24
+ const validateEventHandler = function (value, propName) {
25
+ if (!EVENTS.includes(propName) && !DEV_EVENTS.includes(propName)) {
26
+ throw new Error(`Invalid event '${propName}'.
27
+ Please use a valid event name. One of:
28
+ ${serializeArray(EVENTS)}`)
29
+ }
30
+
31
+ if (typeof value !== 'function') {
32
+ throw new TypeError(`Invalid event handler '${propName}': must be a function`)
33
+ }
34
+ }
@@ -0,0 +1,132 @@
1
+ import pEvery from 'p-every'
2
+ import pLocate from 'p-locate'
3
+ import semver from 'semver'
4
+
5
+ import { importJsonFile } from '../utils/json.js'
6
+ import { resolvePath } from '../utils/resolve.js'
7
+
8
+ // Retrieve the `expectedVersion` of a plugin:
9
+ // - This is the version which should be run
10
+ // - This takes version pinning into account
11
+ // - If this does not match the currently cached version, it is installed first
12
+ // This is also used to retrieve the `compatibleVersion` of a plugin
13
+ // - This is the most recent version compatible with this site
14
+ // - This is the same logic except it does not use version pinning
15
+ // - This is only used to print a warning message when the `compatibleVersion`
16
+ // is older than the currently used version.
17
+ export const getExpectedVersion = async function ({ versions, nodeVersion, packageJson, buildDir, pinnedVersion }) {
18
+ const { version, conditions } = await getCompatibleEntry({
19
+ versions,
20
+ nodeVersion,
21
+ packageJson,
22
+ buildDir,
23
+ pinnedVersion,
24
+ })
25
+ const compatWarning = getCompatWarning(conditions)
26
+ return { version, compatWarning }
27
+ }
28
+
29
+ // This function finds the right `compatibility` entry to use with the plugin.
30
+ // - `compatibitlity` entries are meant for backward compatibility
31
+ // Plugins should define each major version in `compatibility`.
32
+ // - The entries are sorted from most to least recent version.
33
+ // - After their first successful run, plugins are pinned by their major
34
+ // version which is passed as `pinnedVersion` to the next builds.
35
+ // When the plugin does not have a `pinnedVersion`, we use the most recent
36
+ // `compatibility` entry with a successful condition.
37
+ // When the plugin has a `pinnedVersion`, we do not use the `compatibility`
38
+ // conditions. Instead, we just use the most recent entry with a `version`
39
+ // matching `pinnedVersion`.
40
+ // When no `compatibility` entry matches, we use:
41
+ // - If there is a `pinnedVersion`, use it unless `latestVersion` matches it
42
+ // - Otherwise, use `latestVersion`
43
+ const getCompatibleEntry = async function ({ versions, nodeVersion, packageJson, buildDir, pinnedVersion }) {
44
+ if (pinnedVersion !== undefined) {
45
+ const matchingVersion = versions.find(({ version }) =>
46
+ semver.satisfies(version, pinnedVersion, { includePrerelease: true }),
47
+ )
48
+
49
+ return matchingVersion || { version: pinnedVersion }
50
+ }
51
+
52
+ const versionsWithConditions = versions.filter(hasConditions)
53
+ const compatibleEntry = await pLocate(versionsWithConditions, ({ conditions }) =>
54
+ matchesCompatField({ conditions, nodeVersion, packageJson, buildDir }),
55
+ )
56
+ return compatibleEntry || { version: versions[0].version }
57
+ }
58
+
59
+ // Ignore entries without conditions. Those are used to specify breaking
60
+ // changes, i.e. meant to be used for version pinning instead.
61
+ const hasConditions = function ({ conditions }) {
62
+ return conditions.length !== 0
63
+ }
64
+
65
+ const matchesCompatField = async function ({ conditions, nodeVersion, packageJson, buildDir }) {
66
+ return await pEvery(conditions, ({ type, condition }) =>
67
+ CONDITIONS[type].test(condition, { nodeVersion, packageJson, buildDir }),
68
+ )
69
+ }
70
+
71
+ // Retrieve warning message shown when using an older version with `compatibility`
72
+ const getCompatWarning = function (conditions = []) {
73
+ return conditions.map(getConditionWarning).join(', ')
74
+ }
75
+
76
+ const getConditionWarning = function ({ type, condition }) {
77
+ return CONDITIONS[type].warning(condition)
78
+ }
79
+
80
+ // Plugins can use `compatibility.{version}.nodeVersion: 'allowedNodeVersion'`
81
+ // to deliver different plugin versions based on the Node.js version
82
+ const nodeVersionTest = function (allowedNodeVersion, { nodeVersion }) {
83
+ return semver.satisfies(nodeVersion, allowedNodeVersion)
84
+ }
85
+
86
+ const nodeVersionWarning = function (allowedNodeVersion) {
87
+ return `Node.js ${allowedNodeVersion}`
88
+ }
89
+
90
+ const siteDependenciesTest = async function (
91
+ allowedSiteDependencies,
92
+ { packageJson: { devDependencies, dependencies }, buildDir },
93
+ ) {
94
+ const siteDependencies = { ...devDependencies, ...dependencies }
95
+ return await pEvery(Object.entries(allowedSiteDependencies), ([dependencyName, allowedVersion]) =>
96
+ siteDependencyTest({ dependencyName, allowedVersion, siteDependencies, buildDir }),
97
+ )
98
+ }
99
+
100
+ const siteDependencyTest = async function ({ dependencyName, allowedVersion, siteDependencies, buildDir }) {
101
+ const siteDependency = siteDependencies[dependencyName]
102
+ if (typeof siteDependency !== 'string') {
103
+ return false
104
+ }
105
+
106
+ // if this is a valid version we can apply the rule directly
107
+ if (semver.clean(siteDependency) !== null) {
108
+ return semver.satisfies(siteDependency, allowedVersion)
109
+ }
110
+
111
+ try {
112
+ // if this is a range we need to get the exact version
113
+ const packageJsonPath = await resolvePath(`${dependencyName}/package.json`, buildDir)
114
+ const { version } = await importJsonFile(packageJsonPath)
115
+ return semver.satisfies(version, allowedVersion)
116
+ } catch {
117
+ return false
118
+ }
119
+ }
120
+
121
+ const siteDependenciesWarning = function (allowedSiteDependencies) {
122
+ return Object.entries(allowedSiteDependencies).map(siteDependencyWarning).join(',')
123
+ }
124
+
125
+ const siteDependencyWarning = function ([dependencyName, allowedVersion]) {
126
+ return `${dependencyName}@${allowedVersion}`
127
+ }
128
+
129
+ export const CONDITIONS = {
130
+ nodeVersion: { test: nodeVersionTest, warning: nodeVersionWarning },
131
+ siteDependencies: { test: siteDependenciesTest, warning: siteDependenciesWarning },
132
+ }
@@ -0,0 +1,50 @@
1
+ import { addErrorInfo } from '../error/info.js'
2
+ import { logFailPluginWarning } from '../log/messages/plugins.js'
3
+
4
+ // Stop build.
5
+ // As opposed to throwing an error directly or to uncaught exceptions, this is
6
+ // displayed as a user error, not an implementation error.
7
+ export const failBuild = function (message, opts) {
8
+ throw normalizeError('failBuild', failBuild, message, opts)
9
+ }
10
+
11
+ // Stop plugin. Same as `failBuild` but only stops plugin not whole build
12
+ export const failPlugin = function (message, opts) {
13
+ throw normalizeError('failPlugin', failPlugin, message, opts)
14
+ }
15
+
16
+ // Cancel build. Same as `failBuild` except it marks the build as "canceled".
17
+ export const cancelBuild = function (message, opts) {
18
+ throw normalizeError('cancelBuild', cancelBuild, message, opts)
19
+ }
20
+
21
+ // `onSuccess`, `onEnd` and `onError` cannot cancel the build since they are run
22
+ // or might be run after deployment. When calling `failBuild()` or
23
+ // `cancelBuild()`, `failPlugin()` is run instead and a warning is printed.
24
+ export const failPluginWithWarning = function (methodName, event, message, opts) {
25
+ logFailPluginWarning(methodName, event)
26
+ failPlugin(message, opts)
27
+ }
28
+
29
+ // An `error` option can be passed to keep the original error message and
30
+ // stack trace. An additional `message` string is always required.
31
+ const normalizeError = function (type, func, message, { error } = {}) {
32
+ const errorA = getError(error, message, func)
33
+ addErrorInfo(errorA, { type })
34
+ return errorA
35
+ }
36
+
37
+ const getError = function (error, message, func) {
38
+ if (error instanceof Error) {
39
+ // This might fail if `name` is a getter or is non-writable.
40
+ try {
41
+ error.message = `${message}\n${error.message}`
42
+ } catch {}
43
+ return error
44
+ }
45
+
46
+ const errorA = new Error(message)
47
+ // Do not include function itself in the stack trace
48
+ Error.captureStackTrace(errorA, func)
49
+ return errorA
50
+ }
@@ -0,0 +1,17 @@
1
+ export { DEV_EVENTS, EVENTS } from '@netlify/config'
2
+
3
+ const isAmongEvents = function (events, event) {
4
+ return events.includes(event)
5
+ }
6
+
7
+ // Check if failure of the event should not make the build fail
8
+ export const isSoftFailEvent = isAmongEvents.bind(null, ['onSuccess', 'onError', 'onEnd'])
9
+
10
+ // Check if the event is triggered even when the build fails
11
+ export const runsAlsoOnBuildFailure = isAmongEvents.bind(null, ['onError', 'onEnd'])
12
+
13
+ // Check if the event is only triggered when the build fails
14
+ export const runsOnlyOnBuildFailure = isAmongEvents.bind(null, ['onError'])
15
+
16
+ // Check if the event is happening after deploy
17
+ export const runsAfterDeploy = isAmongEvents.bind(null, ['onSuccess', 'onEnd'])