@netlify/build 28.0.0-beta → 29.0.0-rc

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.
package/src/log/logger.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { createWriteStream } from 'fs'
2
+
1
3
  import figures from 'figures'
2
4
  import indentString from 'indent-string'
3
5
 
@@ -105,3 +107,55 @@ export const logErrorSubHeader = function (logs, string, opts) {
105
107
  export const logWarningSubHeader = function (logs, string, opts) {
106
108
  logSubHeader(logs, string, { color: THEME.warningSubHeader, ...opts })
107
109
  }
110
+
111
+ // Combines an array of elements into a single string, separated by a space,
112
+ // and with basic serialization of non-string types
113
+ const reduceLogLines = function (lines) {
114
+ return lines
115
+ .map((input) => {
116
+ if (input instanceof Error) {
117
+ return `${input.message} ${input.stack}`
118
+ }
119
+
120
+ if (typeof input === 'object') {
121
+ try {
122
+ return JSON.stringify(input)
123
+ } catch {
124
+ // Value could not be serialized to JSON, so we return the string
125
+ // representation.
126
+ return String(input)
127
+ }
128
+ }
129
+
130
+ return String(input)
131
+ })
132
+ .join(' ')
133
+ }
134
+
135
+ // Builds a function for logging data to the system logger (i.e. hidden from
136
+ // the user-facing build logs)
137
+ export const getSystemLogger = function (logs, debug, systemLogFile) {
138
+ // If the `debug` flag is used, we return a function that pipes system logs
139
+ // to the regular logger, as the intention is for them to end up in stdout.
140
+ if (debug) {
141
+ return (...args) => log(logs, reduceLogLines(args))
142
+ }
143
+
144
+ // If there's not a file descriptor configured for system logs and `debug`
145
+ // is not set, we return a no-op function that will swallow the errors.
146
+ if (!systemLogFile) {
147
+ return () => {
148
+ // no-op
149
+ }
150
+ }
151
+
152
+ // Return a function that writes to the file descriptor configured for system
153
+ // logs.
154
+ const fileDescriptor = createWriteStream(null, { fd: systemLogFile })
155
+
156
+ fileDescriptor.on('error', () => {
157
+ logError(logs, 'Could not write to system log file')
158
+ })
159
+
160
+ return (...args) => fileDescriptor.write(`${reduceLogLines(args)}\n`)
161
+ }
@@ -52,6 +52,8 @@ const INTERNAL_FLAGS = [
52
52
  'mode',
53
53
  'apiHost',
54
54
  'cacheDir',
55
+ 'systemLogFile',
56
+ 'timeline',
55
57
  ]
56
58
  const HIDDEN_FLAGS = [...SECURE_FLAGS, ...TEST_FLAGS, ...INTERNAL_FLAGS]
57
59
  const HIDDEN_DEBUG_FLAGS = [...SECURE_FLAGS, ...TEST_FLAGS]
@@ -6,18 +6,19 @@ import { addErrorInfo } from '../../error/info.js'
6
6
  // Report status information to the UI
7
7
  export const show = function (runState, showArgs) {
8
8
  validateShowArgs(showArgs)
9
- const { title, summary, text } = removeEmptyStrings(showArgs)
10
- runState.status = { state: 'success', title, summary, text }
9
+ const { title, summary, text, extraData } = removeEmptyStrings(showArgs)
10
+ runState.status = { state: 'success', title, summary, text, extraData }
11
11
  }
12
12
 
13
13
  // Validate arguments of `utils.status.show()`
14
14
  const validateShowArgs = function (showArgs) {
15
15
  try {
16
16
  validateShowArgsObject(showArgs)
17
- const { title, summary, text, ...otherArgs } = showArgs
17
+ const { title, summary, text, extraData, ...otherArgs } = showArgs
18
18
  validateShowArgsKeys(otherArgs)
19
19
  Object.entries({ title, summary, text }).forEach(validateStringArg)
20
20
  validateShowArgsSummary(summary)
21
+ validateShowArgsExtraData(extraData)
21
22
  } catch (error) {
22
23
  error.message = `utils.status.show() ${error.message}`
23
24
  addErrorInfo(error, { type: 'pluginValidation' })
@@ -54,6 +55,12 @@ const validateShowArgsSummary = function (summary) {
54
55
  }
55
56
  }
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
+
57
64
  const removeEmptyStrings = function (showArgs) {
58
65
  return mapObj(showArgs, removeEmptyString)
59
66
  }
@@ -1,6 +1,6 @@
1
1
  import { addErrorInfo } from '../../error/info.js'
2
2
  import { serializeArray } from '../../log/serialize.js'
3
- import { EVENTS } from '../events.js'
3
+ import { DEV_EVENTS, EVENTS } from '../events.js'
4
4
 
5
5
  // Validate the shape of a plugin return value
6
6
  export const validatePlugin = function (logic) {
@@ -22,7 +22,7 @@ export const validatePlugin = function (logic) {
22
22
 
23
23
  // All other properties are event handlers
24
24
  const validateEventHandler = function (value, propName) {
25
- if (!EVENTS.includes(propName)) {
25
+ if (!EVENTS.includes(propName) && !DEV_EVENTS.includes(propName)) {
26
26
  throw new Error(`Invalid event '${propName}'.
27
27
  Please use a valid event name. One of:
28
28
  ${serializeArray(EVENTS)}`)
@@ -1,4 +1,4 @@
1
- export { EVENTS } from '@netlify/config'
1
+ export { DEV_EVENTS, EVENTS } from '@netlify/config'
2
2
 
3
3
  const isAmongEvents = function (events, event) {
4
4
  return events.includes(event)
@@ -6,12 +6,15 @@ import { pathExists } from 'path-exists'
6
6
 
7
7
  import { logFunctionsToBundle } from '../../log/messages/core_steps.js'
8
8
 
9
+ import { tagBundlingError } from './lib/error.js'
9
10
  import { parseManifest } from './lib/internal_manifest.js'
11
+ import { validateEdgeFunctionsManifest } from './validate_manifest/validate_edge_functions_manifest.js'
10
12
 
11
13
  // TODO: Replace this with a custom cache directory.
12
14
  const DENO_CLI_CACHE_DIRECTORY = '.netlify/plugins/deno-cli'
13
15
  const IMPORT_MAP_FILENAME = 'edge-functions-import-map.json'
14
16
 
17
+ // eslint-disable-next-line complexity, max-statements
15
18
  const coreStep = async function ({
16
19
  buildDir,
17
20
  constants: {
@@ -21,6 +24,7 @@ const coreStep = async function ({
21
24
  IS_LOCAL: isRunningLocally,
22
25
  },
23
26
  debug,
27
+ systemLog,
24
28
  featureFlags,
25
29
  logs,
26
30
  netlifyConfig,
@@ -45,13 +49,25 @@ const coreStep = async function ({
45
49
  // Edge Bundler expects the dist directory to exist.
46
50
  await fs.mkdir(distPath, { recursive: true })
47
51
 
48
- await bundle(sourcePaths, distPath, declarations, {
49
- cacheDirectory,
50
- debug,
51
- distImportMapPath,
52
- featureFlags,
53
- importMaps: [importMap].filter(Boolean),
54
- })
52
+ try {
53
+ const { manifest } = await bundle(sourcePaths, distPath, declarations, {
54
+ basePath: buildDir,
55
+ cacheDirectory,
56
+ debug,
57
+ distImportMapPath,
58
+ featureFlags,
59
+ importMaps: [importMap].filter(Boolean),
60
+ systemLogger: featureFlags.edge_functions_system_logger ? systemLog : undefined,
61
+ })
62
+
63
+ systemLog('Edge Functions manifest:', manifest)
64
+ } catch (error) {
65
+ tagBundlingError(error)
66
+
67
+ throw error
68
+ }
69
+
70
+ await validateEdgeFunctionsManifest({ buildDir, constants: { EDGE_FUNCTIONS_DIST: distDirectory } })
55
71
 
56
72
  return {}
57
73
  }
@@ -0,0 +1,21 @@
1
+ import { CUSTOM_ERROR_KEY, getErrorInfo, isBuildError } from '../../../error/info.js'
2
+
3
+ // If we have a custom error tagged with `functionsBundling` (which happens if
4
+ // there is an issue with user code), we tag it as coming from an edge function
5
+ // so that we can adjust the downstream error messages accordingly.
6
+ export const tagBundlingError = (error) => {
7
+ if (!isBuildError(error)) {
8
+ return
9
+ }
10
+
11
+ const [errorInfo = {}] = getErrorInfo(error)
12
+
13
+ if (errorInfo.type !== 'functionsBundling') {
14
+ return
15
+ }
16
+
17
+ error[CUSTOM_ERROR_KEY].location = {
18
+ ...error[CUSTOM_ERROR_KEY].location,
19
+ functionType: 'edge',
20
+ }
21
+ }
@@ -0,0 +1,89 @@
1
+ import { promises as fs } from 'fs'
2
+ import { join, resolve } from 'path'
3
+
4
+ import Ajv from 'ajv'
5
+ import ajvErrors from 'ajv-errors'
6
+
7
+ import { addErrorInfo } from '../../../error/info.js'
8
+
9
+ const ajv = new Ajv({ allErrors: true })
10
+ ajvErrors(ajv)
11
+
12
+ // regex pattern for manifest route pattern
13
+ // checks if the pattern string starts with ^ and ends with $
14
+ // we define this format in edge-bundler:
15
+ // https://github.com/netlify/edge-bundler/blob/main/src/manifest.ts#L66
16
+ const normalizedPatternRegex = /^\^.*\$$/
17
+ ajv.addFormat('regexPattern', {
18
+ async: true,
19
+ validate: (data) => normalizedPatternRegex.test(data),
20
+ })
21
+
22
+ const bundlesSchema = {
23
+ $async: true,
24
+ type: 'object',
25
+ required: ['asset', 'format'],
26
+ properties: {
27
+ asset: { type: 'string' },
28
+ format: { type: 'string' },
29
+ },
30
+ additionalProperties: false,
31
+ }
32
+
33
+ const routesSchema = {
34
+ $async: true,
35
+ type: 'object',
36
+ required: ['function', 'pattern'],
37
+ properties: {
38
+ function: { type: 'string' },
39
+ pattern: { type: 'string', format: 'regexPattern', errorMessage: `must match format ${normalizedPatternRegex}` },
40
+ },
41
+ additionalProperties: false,
42
+ }
43
+
44
+ const edgeManifestSchema = {
45
+ $async: true,
46
+ type: 'object',
47
+ required: ['bundles', 'routes', 'bundler_version'],
48
+ properties: {
49
+ bundles: {
50
+ type: 'array',
51
+ items: bundlesSchema,
52
+ },
53
+ routes: {
54
+ type: 'array',
55
+ items: routesSchema,
56
+ },
57
+ bundler_version: { type: 'string' },
58
+ },
59
+ additionalProperties: false,
60
+ errorMessage: "Couldn't validate Edge Functions manifest.json",
61
+ }
62
+
63
+ const validateManifest = async (manifestData) => {
64
+ const validate = ajv.compile(edgeManifestSchema)
65
+
66
+ await validate(manifestData)
67
+ }
68
+
69
+ export const validateEdgeFunctionsManifest = async function ({
70
+ buildDir,
71
+ constants: { EDGE_FUNCTIONS_DIST: distDirectory },
72
+ }) {
73
+ try {
74
+ const edgeFunctionsDistPath = resolve(buildDir, distDirectory)
75
+ const manifestPath = join(edgeFunctionsDistPath, 'manifest.json')
76
+ const data = await fs.readFile(manifestPath)
77
+ const manifestData = JSON.parse(data)
78
+
79
+ await validateManifest(manifestData)
80
+ } catch (error) {
81
+ const isValidationErr = error instanceof Ajv.ValidationError
82
+ const parsedErr = isValidationErr ? error.errors : error
83
+
84
+ addErrorInfo(parsedErr, { type: 'coreStep' })
85
+ throw new Error(isValidationErr ? JSON.stringify(parsedErr, null, 2) : parsedErr)
86
+ }
87
+
88
+ return {}
89
+ }
@@ -104,7 +104,7 @@ const sendApiStatuses = async function ({
104
104
 
105
105
  const sendApiStatus = async function ({
106
106
  api,
107
- status: { packageName, version, state, event, title, summary, text },
107
+ status: { packageName, version, state, event, title, summary, text, extraData },
108
108
  childEnv,
109
109
  mode,
110
110
  netlifyConfig,
@@ -117,7 +117,16 @@ const sendApiStatus = async function ({
117
117
  try {
118
118
  await api.createPluginRun({
119
119
  deploy_id: deployId,
120
- body: { package: packageName, version, state, reporting_event: event, title, summary, text },
120
+ body: {
121
+ package: packageName,
122
+ version,
123
+ state,
124
+ reporting_event: event,
125
+ title,
126
+ summary,
127
+ text,
128
+ extra_data: extraData,
129
+ },
121
130
  })
122
131
  // Bitballoon API randomly fails with 502.
123
132
  // Builds should be successful when this API call fails, but we still want
@@ -27,6 +27,7 @@ export const fireCoreStep = async function ({
27
27
  redirectsPath,
28
28
  featureFlags,
29
29
  debug,
30
+ systemLog,
30
31
  saveConfig,
31
32
  }) {
32
33
  try {
@@ -54,6 +55,7 @@ export const fireCoreStep = async function ({
54
55
  redirectsPath,
55
56
  featureFlags,
56
57
  debug,
58
+ systemLog,
57
59
  saveConfig,
58
60
  })
59
61
  const {
package/src/steps/get.js CHANGED
@@ -1,4 +1,4 @@
1
- import { EVENTS } from '../plugins/events.js'
1
+ import { DEV_EVENTS, EVENTS } from '../plugins/events.js'
2
2
  import { buildCommandCore } from '../plugins_core/build_command.js'
3
3
  import { deploySite } from '../plugins_core/deploy/index.js'
4
4
  import { bundleEdgeFunctions } from '../plugins_core/edge_functions/index.js'
@@ -7,18 +7,37 @@ import { bundleFunctions } from '../plugins_core/functions/index.js'
7
7
  // Get all build steps
8
8
  export const getSteps = function (steps) {
9
9
  const stepsA = addCoreSteps(steps)
10
- const stepsB = sortSteps(stepsA)
10
+ const stepsB = sortSteps(stepsA, EVENTS)
11
11
  const events = getEvents(stepsB)
12
12
  return { steps: stepsB, events }
13
13
  }
14
14
 
15
+ // Get all dev steps
16
+ export const getDevSteps = function (command, steps) {
17
+ const devCommandStep = {
18
+ event: 'onDev',
19
+ coreStep: async () => {
20
+ await command()
21
+
22
+ return {}
23
+ },
24
+ coreStepId: 'dev_command',
25
+ coreStepName: 'dev.command',
26
+ coreStepDescription: () => 'Run command for local development',
27
+ }
28
+ const sortedSteps = sortSteps([...steps, devCommandStep], DEV_EVENTS)
29
+ const events = getEvents(sortedSteps)
30
+
31
+ return { steps: sortedSteps, events }
32
+ }
33
+
15
34
  const addCoreSteps = function (steps) {
16
35
  return [buildCommandCore, ...steps, bundleFunctions, bundleEdgeFunctions, deploySite]
17
36
  }
18
37
 
19
38
  // Sort plugin steps by event order.
20
- const sortSteps = function (steps) {
21
- return EVENTS.flatMap((event) => steps.filter((step) => step.event === event))
39
+ const sortSteps = function (steps, events) {
40
+ return events.flatMap((event) => steps.filter((step) => step.event === event))
22
41
  }
23
42
 
24
43
  // Retrieve list of unique events
@@ -5,7 +5,7 @@ import { getSeverity } from '../core/severity.js'
5
5
  import { handleBuildError } from '../error/handle.js'
6
6
  import { getErrorInfo } from '../error/info.js'
7
7
  import { startErrorMonitor } from '../error/monitor/start.js'
8
- import { getBufferLogs } from '../log/logger.js'
8
+ import { getBufferLogs, getSystemLogger } from '../log/logger.js'
9
9
  import { logBuildStart } from '../log/messages/core.js'
10
10
  import { reportStatuses } from '../status/report.js'
11
11
 
@@ -39,6 +39,7 @@ import { runSteps } from './run_steps.js'
39
39
  export const runCoreSteps = async (buildSteps, flags = {}) => {
40
40
  const { errorMonitor, mode, logs, debug, ...flagsA } = startBuild(flags)
41
41
  const errorParams = { errorMonitor, mode, logs, debug }
42
+ const systemLog = getSystemLogger(logs, debug)
42
43
 
43
44
  try {
44
45
  const { netlifyConfig: netlifyConfigA, configMutations } = await executeBuildStep({
@@ -49,6 +50,7 @@ export const runCoreSteps = async (buildSteps, flags = {}) => {
49
50
  debug,
50
51
  errorParams,
51
52
  buildSteps,
53
+ systemLog,
52
54
  })
53
55
  const { success, severityCode } = getSeverity('success')
54
56
 
@@ -93,6 +95,7 @@ const executeBuildStep = async function ({
93
95
  featureFlags,
94
96
  buildSteps,
95
97
  repositoryRoot,
98
+ systemLog,
96
99
  }) {
97
100
  const configOpts = getConfigOpts({
98
101
  config,
@@ -140,6 +143,7 @@ const executeBuildStep = async function ({
140
143
  childEnv,
141
144
  buildSteps,
142
145
  repositoryRoot: repositoryRootA,
146
+ systemLog,
143
147
  })
144
148
 
145
149
  return {
@@ -175,6 +179,7 @@ const runBuildStep = async function ({
175
179
  childEnv,
176
180
  buildSteps,
177
181
  repositoryRoot,
182
+ systemLog,
178
183
  }) {
179
184
  const { netlifyConfig: netlifyConfigA, configMutations } = await runSteps({
180
185
  steps: getBuildSteps(buildSteps),
@@ -188,6 +193,7 @@ const runBuildStep = async function ({
188
193
  featureFlags,
189
194
  childEnv,
190
195
  repositoryRoot,
196
+ systemLog,
191
197
  })
192
198
 
193
199
  return { netlifyConfig: netlifyConfigA, configMutations }
@@ -1,4 +1,3 @@
1
- /* eslint-disable max-lines */
2
1
  import { addMutableConstants } from '../core/constants.js'
3
2
  import { logStepStart } from '../log/messages/steps.js'
4
3
  import { runsAlsoOnBuildFailure, runsOnlyOnBuildFailure } from '../plugins/events.js'
@@ -48,6 +47,7 @@ export const runStep = async function ({
48
47
  redirectsPath,
49
48
  logs,
50
49
  debug,
50
+ systemLog,
51
51
  verbose,
52
52
  saveConfig,
53
53
  timers,
@@ -109,6 +109,7 @@ export const runStep = async function ({
109
109
  error,
110
110
  logs,
111
111
  debug,
112
+ systemLog,
112
113
  verbose,
113
114
  saveConfig,
114
115
  timers,
@@ -140,6 +141,7 @@ export const runStep = async function ({
140
141
  redirectsPath: redirectsPathA,
141
142
  logs,
142
143
  debug,
144
+ systemLog,
143
145
  timers: timersA,
144
146
  durationNs,
145
147
  testOpts,
@@ -237,6 +239,7 @@ const tFireStep = function ({
237
239
  error,
238
240
  logs,
239
241
  debug,
242
+ systemLog,
240
243
  verbose,
241
244
  saveConfig,
242
245
  errorParams,
@@ -271,6 +274,7 @@ const tFireStep = function ({
271
274
  redirectsPath,
272
275
  featureFlags,
273
276
  debug,
277
+ systemLog,
274
278
  saveConfig,
275
279
  })
276
280
  }
@@ -294,7 +298,7 @@ const tFireStep = function ({
294
298
  error,
295
299
  logs,
296
300
  debug,
301
+ systemLog,
297
302
  verbose,
298
303
  })
299
304
  }
300
- /* eslint-enable max-lines */
@@ -1,4 +1,3 @@
1
- /* eslint-disable max-lines */
2
1
  import pReduce from 'p-reduce'
3
2
 
4
3
  import { addErrorInfo } from '../error/info.js'
@@ -34,6 +33,7 @@ export const runSteps = async function ({
34
33
  configOpts,
35
34
  logs,
36
35
  debug,
36
+ systemLog,
37
37
  verbose,
38
38
  saveConfig,
39
39
  timers,
@@ -127,6 +127,7 @@ export const runSteps = async function ({
127
127
  redirectsPath: redirectsPathA,
128
128
  logs,
129
129
  debug,
130
+ systemLog,
130
131
  verbose,
131
132
  saveConfig,
132
133
  timers: timersA,
@@ -176,4 +177,3 @@ export const runSteps = async function ({
176
177
  configMutations: configMutationsB,
177
178
  }
178
179
  }
179
- /* eslint-enable max-lines */