netlify-cli 10.16.0 → 10.17.0

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.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
- "version": "10.16.0",
3
+ "version": "10.17.0",
4
4
  "lockfileVersion": 2,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "netlify-cli",
9
- "version": "10.16.0",
9
+ "version": "10.17.0",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {
@@ -15,7 +15,7 @@
15
15
  "@netlify/edge-bundler": "^1.12.1",
16
16
  "@netlify/framework-info": "^9.2.0",
17
17
  "@netlify/local-functions-proxy": "^1.1.1",
18
- "@netlify/plugins-list": "^6.37.0",
18
+ "@netlify/plugins-list": "^6.38.0",
19
19
  "@netlify/zip-it-and-ship-it": "^5.13.4",
20
20
  "@octokit/rest": "^18.0.0",
21
21
  "@sindresorhus/slugify": "^1.1.0",
@@ -81,7 +81,7 @@
81
81
  "multiparty": "^4.2.1",
82
82
  "netlify": "^12.0.0",
83
83
  "netlify-headers-parser": "^6.0.2",
84
- "netlify-onegraph-internal": "0.8.1",
84
+ "netlify-onegraph-internal": "0.8.3",
85
85
  "netlify-redirect-parser": "^13.0.5",
86
86
  "netlify-redirector": "^0.2.1",
87
87
  "node-fetch": "^2.6.0",
@@ -3890,9 +3890,9 @@
3890
3890
  "integrity": "sha512-ni6R1xdR8EtH0iB8ixGt9ocuboW+Q8eN4ilTX8lfNHS6Y6Q2S+O/aB2n1BnAgv39wopeQsQ2meL9vfEePURl7w=="
3891
3891
  },
3892
3892
  "node_modules/@netlify/plugins-list": {
3893
- "version": "6.37.0",
3894
- "resolved": "https://registry.npmjs.org/@netlify/plugins-list/-/plugins-list-6.37.0.tgz",
3895
- "integrity": "sha512-sThBWfyHQ0TEVjUCPlzrt0tTjA6MVIydE718Hqb7jR8x51nSkOrWgIQmaQaJWZsGErRPX2hW501Ajdh0eIW6Bw==",
3893
+ "version": "6.38.0",
3894
+ "resolved": "https://registry.npmjs.org/@netlify/plugins-list/-/plugins-list-6.38.0.tgz",
3895
+ "integrity": "sha512-fa6BfTCFF3gU2ygS4i175P2Zez8FKXgxKWtjdJPm8eh4UhJ0flIHdooTLZWTpBDL5YiDizk2cviPvOL8fQS93w==",
3896
3896
  "engines": {
3897
3897
  "node": "^12.20.0 || ^14.14.0 || >=16.0.0"
3898
3898
  }
@@ -16319,9 +16319,9 @@
16319
16319
  }
16320
16320
  },
16321
16321
  "node_modules/netlify-onegraph-internal": {
16322
- "version": "0.8.1",
16323
- "resolved": "https://registry.npmjs.org/netlify-onegraph-internal/-/netlify-onegraph-internal-0.8.1.tgz",
16324
- "integrity": "sha512-gCBwR6wOLWiiE+MMgYtx7OVIUvsNGPPP8oe0ltkYjCATcN/e/l0/z/FrRaTGPXnR8wMBp9Kibgibskr8h5dIpg==",
16322
+ "version": "0.8.3",
16323
+ "resolved": "https://registry.npmjs.org/netlify-onegraph-internal/-/netlify-onegraph-internal-0.8.3.tgz",
16324
+ "integrity": "sha512-qxdUkP6IcXUN/6FmYrfLM+M1pXK5TUTxkhhVKXHr+jpApFV4pmEsiuWU7jX4j4AqpaF7+glVJolyVD5F3SBpvg==",
16325
16325
  "dependencies": {
16326
16326
  "graphql": "16.5.0",
16327
16327
  "node-fetch": "^2.6.0",
@@ -25319,9 +25319,9 @@
25319
25319
  "integrity": "sha512-ni6R1xdR8EtH0iB8ixGt9ocuboW+Q8eN4ilTX8lfNHS6Y6Q2S+O/aB2n1BnAgv39wopeQsQ2meL9vfEePURl7w=="
25320
25320
  },
25321
25321
  "@netlify/plugins-list": {
25322
- "version": "6.37.0",
25323
- "resolved": "https://registry.npmjs.org/@netlify/plugins-list/-/plugins-list-6.37.0.tgz",
25324
- "integrity": "sha512-sThBWfyHQ0TEVjUCPlzrt0tTjA6MVIydE718Hqb7jR8x51nSkOrWgIQmaQaJWZsGErRPX2hW501Ajdh0eIW6Bw=="
25322
+ "version": "6.38.0",
25323
+ "resolved": "https://registry.npmjs.org/@netlify/plugins-list/-/plugins-list-6.38.0.tgz",
25324
+ "integrity": "sha512-fa6BfTCFF3gU2ygS4i175P2Zez8FKXgxKWtjdJPm8eh4UhJ0flIHdooTLZWTpBDL5YiDizk2cviPvOL8fQS93w=="
25325
25325
  },
25326
25326
  "@netlify/run-utils": {
25327
25327
  "version": "4.0.1",
@@ -34795,9 +34795,9 @@
34795
34795
  }
34796
34796
  },
34797
34797
  "netlify-onegraph-internal": {
34798
- "version": "0.8.1",
34799
- "resolved": "https://registry.npmjs.org/netlify-onegraph-internal/-/netlify-onegraph-internal-0.8.1.tgz",
34800
- "integrity": "sha512-gCBwR6wOLWiiE+MMgYtx7OVIUvsNGPPP8oe0ltkYjCATcN/e/l0/z/FrRaTGPXnR8wMBp9Kibgibskr8h5dIpg==",
34798
+ "version": "0.8.3",
34799
+ "resolved": "https://registry.npmjs.org/netlify-onegraph-internal/-/netlify-onegraph-internal-0.8.3.tgz",
34800
+ "integrity": "sha512-qxdUkP6IcXUN/6FmYrfLM+M1pXK5TUTxkhhVKXHr+jpApFV4pmEsiuWU7jX4j4AqpaF7+glVJolyVD5F3SBpvg==",
34801
34801
  "requires": {
34802
34802
  "graphql": "16.5.0",
34803
34803
  "node-fetch": "^2.6.0",
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
3
  "description": "Netlify command line tool",
4
- "version": "10.16.0",
4
+ "version": "10.17.0",
5
5
  "author": "Netlify Inc.",
6
6
  "contributors": [
7
+ "@whitep4nth3r (https://twitter.com/whitep4nth3r)",
7
8
  "Abraham Schilling <AbrahamSchilling@gmail.com> (https://gitlab.com/n4bb12)",
8
9
  "Alberto De Agostini (https://twitter.com/albertodeago88)",
9
10
  "Alejandro Ñáñez Ortiz <git@alejandro.dev> (https://twitter.com/alejandronanez)",
@@ -225,7 +226,7 @@
225
226
  "@netlify/edge-bundler": "^1.12.1",
226
227
  "@netlify/framework-info": "^9.2.0",
227
228
  "@netlify/local-functions-proxy": "^1.1.1",
228
- "@netlify/plugins-list": "^6.37.0",
229
+ "@netlify/plugins-list": "^6.38.0",
229
230
  "@netlify/zip-it-and-ship-it": "^5.13.4",
230
231
  "@octokit/rest": "^18.0.0",
231
232
  "@sindresorhus/slugify": "^1.1.0",
@@ -291,7 +292,7 @@
291
292
  "multiparty": "^4.2.1",
292
293
  "netlify": "^12.0.0",
293
294
  "netlify-headers-parser": "^6.0.2",
294
- "netlify-onegraph-internal": "0.8.1",
295
+ "netlify-onegraph-internal": "0.8.3",
295
296
  "netlify-redirect-parser": "^13.0.5",
296
297
  "netlify-redirector": "^0.2.1",
297
298
  "node-fetch": "^2.6.0",
@@ -38,6 +38,7 @@ const {
38
38
  error,
39
39
  exit,
40
40
  generateNetlifyGraphJWT,
41
+ getEnvelopeEnv,
41
42
  getSiteInformation,
42
43
  getToken,
43
44
  injectEnvVariables,
@@ -428,7 +429,12 @@ const dev = async (options, command) => {
428
429
  ...options,
429
430
  }
430
431
 
431
- await injectEnvVariables({ devConfig, env: command.netlify.cachedConfig.env, site })
432
+ let { env } = command.netlify.cachedConfig
433
+ if (siteInfo.use_envelope) {
434
+ env = await getEnvelopeEnv({ api, context: options.context, env, siteInfo })
435
+ }
436
+
437
+ await injectEnvVariables({ devConfig, env, site })
432
438
  await promptEditorHelper({ chalk, config, log, NETLIFYDEVLOG, repositoryRoot, state })
433
439
 
434
440
  const { addonsUrls, capabilities, siteUrl, timeouts } = await getSiteInformation({
@@ -621,6 +627,11 @@ const createDevCommand = (program) => {
621
627
  `Local dev server\nThe dev command will run a local dev server with Netlify's proxy and redirect rules`,
622
628
  )
623
629
  .option('-c ,--command <command>', 'command to run')
630
+ .addOption(
631
+ new Option('--context <context>', 'Specify a deploy context for environment variables')
632
+ .choices(['production', 'deploy-preview', 'branch-deploy', 'dev'])
633
+ .default('dev'),
634
+ )
624
635
  .option('-p ,--port <port>', 'port of netlify dev', (value) => Number.parseInt(value))
625
636
  .option('--targetPort <port>', 'port of target app server', (value) => Number.parseInt(value))
626
637
  .option('--framework <name>', 'framework to use. Defaults to #auto which automatically detects a framework')
@@ -670,6 +681,7 @@ const createDevCommand = (program) => {
670
681
  'netlify dev',
671
682
  'netlify dev -d public',
672
683
  'netlify dev -c "hugo server -w" --targetPort 1313',
684
+ 'netlify dev --context production',
673
685
  'netlify dev --graph',
674
686
  'netlify dev --edgeInspect',
675
687
  'netlify dev --edgeInspect=127.0.0.1:9229',
@@ -1,5 +1,7 @@
1
1
  // @ts-check
2
- const { log, logJson } = require('../../utils')
2
+ const { Option } = require('commander')
3
+
4
+ const { chalk, error, getEnvelopeEnv, log, logJson } = require('../../utils')
3
5
 
4
6
  /**
5
7
  * The env:get command
@@ -8,6 +10,7 @@ const { log, logJson } = require('../../utils')
8
10
  * @param {import('../base-command').BaseCommand} command
9
11
  */
10
12
  const envGet = async (name, options, command) => {
13
+ const { context, scope } = options
11
14
  const { api, cachedConfig, site } = command.netlify
12
15
  const siteId = site.id
13
16
 
@@ -16,9 +19,21 @@ const envGet = async (name, options, command) => {
16
19
  return false
17
20
  }
18
21
 
19
- const siteData = await api.getSite({ siteId })
22
+ const { siteInfo } = cachedConfig
23
+ let { env } = cachedConfig
24
+
25
+ if (siteInfo.use_envelope) {
26
+ env = await getEnvelopeEnv({ api, context, env, key: name, scope, siteInfo })
27
+ } else if (context !== 'dev' || scope !== 'any') {
28
+ error(
29
+ `To specify a context or scope, please run ${chalk.yellowBright(
30
+ 'netlify open:admin',
31
+ )} to open the Netlify UI and opt in to the new environment variables experience from Site settings`,
32
+ )
33
+ return false
34
+ }
20
35
 
21
- const { value } = cachedConfig.env[name] || {}
36
+ const { value } = env[name] || {}
22
37
 
23
38
  // Return json response for piping commands
24
39
  if (options.json) {
@@ -27,7 +42,9 @@ const envGet = async (name, options, command) => {
27
42
  }
28
43
 
29
44
  if (!value) {
30
- log(`Environment variable ${name} not set for site ${siteData.name}`)
45
+ const withContext = `in the ${chalk.magentaBright(context)} context`
46
+ const withScope = scope === 'any' ? '' : ` in the ${chalk.magentaBright(context)} scope`
47
+ log(`No value set ${withContext}${withScope} for environment variable ${chalk.yellowBright(name)}`)
31
48
  return false
32
49
  }
33
50
 
@@ -43,6 +60,16 @@ const createEnvGetCommand = (program) =>
43
60
  program
44
61
  .command('env:get')
45
62
  .argument('<name>', 'Environment variable name')
63
+ .addOption(
64
+ new Option('-c, --context <context>', 'Specify a deploy context')
65
+ .choices(['production', 'deploy-preview', 'branch-deploy', 'dev'])
66
+ .default('dev'),
67
+ )
68
+ .addOption(
69
+ new Option('-s, --scope <scope>', 'Specify a scope')
70
+ .choices(['builds', 'functions', 'post_processing', 'runtime', 'any'])
71
+ .default('any'),
72
+ )
46
73
  .description('Get resolved value of specified environment variable (includes netlify.toml)')
47
74
  .action(async (name, options, command) => {
48
75
  await envGet(name, options, command)
@@ -1,20 +1,33 @@
1
1
  // @ts-check
2
2
  const AsciiTable = require('ascii-table')
3
3
  const { isCI } = require('ci-info')
4
+ const { Option } = require('commander')
4
5
  const inquirer = require('inquirer')
5
6
  const isEmpty = require('lodash/isEmpty')
6
7
 
7
- const { chalk, log, logJson } = require('../../utils')
8
+ const { chalk, error, getEnvelopeEnv, getHumanReadableScopes, log, logJson } = require('../../utils')
8
9
 
9
10
  const [logUpdatePromise, ansiEscapesPromise] = [import('log-update'), import('ansi-escapes')]
10
11
 
11
12
  const MASK_LENGTH = 50
12
13
  const MASK = '*'.repeat(MASK_LENGTH)
13
14
 
14
- const getTable = ({ environment, hideValues }) => {
15
+ const getTable = ({ environment, hideValues, scopesColumn }) => {
15
16
  const table = new AsciiTable(`Environment variables`)
16
- table.setHeading('Key', 'Value')
17
- table.addRowMatrix(Object.entries(environment).map(([key, value]) => [key, hideValues ? MASK : value]))
17
+ const headings = ['Key', 'Value', scopesColumn && 'Scope'].filter(Boolean)
18
+ table.setHeading(...headings)
19
+ table.addRowMatrix(
20
+ Object.entries(environment).map(([key, variable]) =>
21
+ [
22
+ // Key
23
+ key,
24
+ // Value
25
+ hideValues ? MASK : variable.value,
26
+ // Scope
27
+ scopesColumn && getHumanReadableScopes(variable.scopes),
28
+ ].filter(Boolean),
29
+ ),
30
+ )
18
31
  return table.toString()
19
32
  }
20
33
 
@@ -25,6 +38,7 @@ const getTable = ({ environment, hideValues }) => {
25
38
  * @returns {Promise<boolean>}
26
39
  */
27
40
  const envList = async (options, command) => {
41
+ const { context, scope } = options
28
42
  const { api, cachedConfig, site } = command.netlify
29
43
  const siteId = site.id
30
44
 
@@ -33,36 +47,55 @@ const envList = async (options, command) => {
33
47
  return false
34
48
  }
35
49
 
36
- const siteData = await api.getSite({ siteId })
37
- const environment = Object.fromEntries(
38
- Object.entries(cachedConfig.env)
39
- // Omitting general variables to reduce noise.
40
- .filter(([, variable]) => variable.sources[0] !== 'general')
41
- .map(([key, variable]) => [key, variable.value]),
50
+ const { env, siteInfo } = cachedConfig
51
+ const isUsingEnvelope = siteInfo.use_envelope
52
+ let environment = env
53
+
54
+ if (isUsingEnvelope) {
55
+ environment = await getEnvelopeEnv({ api, context, env, scope, siteInfo })
56
+ } else if (context !== 'dev' || scope !== 'any') {
57
+ error(
58
+ `To specify a context or scope, please run ${chalk.yellowBright(
59
+ 'netlify open:admin',
60
+ )} to open the Netlify UI and opt in to the new environment variables experience from Site settings`,
61
+ )
62
+ return false
63
+ }
64
+
65
+ // filter out general sources
66
+ environment = Object.fromEntries(
67
+ Object.entries(environment).filter(([, variable]) => variable.sources[0] !== 'general'),
42
68
  )
43
69
 
44
70
  // Return json response for piping commands
45
71
  if (options.json) {
46
- logJson(environment)
72
+ const envDictionary = Object.fromEntries(
73
+ Object.entries(environment).map(([key, variable]) => [key, variable.value]),
74
+ )
75
+ logJson(envDictionary)
47
76
  return false
48
77
  }
49
78
 
79
+ const forSite = `for site ${chalk.greenBright(siteInfo.name)}`
80
+ const withContext = isUsingEnvelope ? `in the ${chalk.magentaBright(options.context)} context` : ''
81
+ const withScope = isUsingEnvelope && scope !== 'any' ? `and ${chalk.yellowBright(options.scope)} scope` : ''
50
82
  if (isEmpty(environment)) {
51
- log(`No environment variables set for site ${chalk.greenBright(siteData.name)}`)
83
+ log(`No environment variables set ${forSite} ${withContext} ${withScope}`)
52
84
  return false
53
85
  }
54
86
 
55
87
  // List environment in a table
56
- log(`Listing environment variables for site: ${chalk.greenBright(siteData.name)}`)
88
+ const count = Object.keys(environment).length
89
+ log(`${count} environment variable${count === 1 ? '' : 's'} ${forSite} ${withContext} ${withScope}`)
57
90
 
58
91
  if (isCI) {
59
- log(getTable({ environment, hideValues: false }))
92
+ log(getTable({ environment, hideValues: false, scopesColumn: isUsingEnvelope }))
60
93
  return false
61
94
  }
62
95
 
63
96
  const { default: logUpdate } = await logUpdatePromise
64
97
 
65
- logUpdate(getTable({ environment, hideValues: true }))
98
+ logUpdate(getTable({ environment, hideValues: true, scopesColumn: isUsingEnvelope }))
66
99
  const { showValues } = await inquirer.prompt([
67
100
  {
68
101
  type: 'confirm',
@@ -76,7 +109,7 @@ const envList = async (options, command) => {
76
109
  const { default: ansiEscapes } = await ansiEscapesPromise
77
110
  // since inquirer adds a prompt, we need to account for it when printing the table again
78
111
  log(ansiEscapes.eraseLines(3))
79
- logUpdate(getTable({ environment, hideValues: false }))
112
+ logUpdate(getTable({ environment, hideValues: false, scopesColumn: isUsingEnvelope }))
80
113
  log(`${chalk.cyan('?')} Show values? ${chalk.cyan('Yes')}`)
81
114
  }
82
115
  }
@@ -89,6 +122,16 @@ const envList = async (options, command) => {
89
122
  const createEnvListCommand = (program) =>
90
123
  program
91
124
  .command('env:list')
125
+ .addOption(
126
+ new Option('-c, --context <context>', 'Specify a deploy context')
127
+ .choices(['production', 'deploy-preview', 'branch-deploy', 'dev'])
128
+ .default('dev'),
129
+ )
130
+ .addOption(
131
+ new Option('-s, --scope <scope>', 'Specify a scope')
132
+ .choices(['builds', 'functions', 'post_processing', 'runtime', 'any'])
133
+ .default('any'),
134
+ )
92
135
  .description('Lists resolved environment variables for site (includes netlify.toml)')
93
136
  .action(async (options, command) => {
94
137
  await envList(options, command)
@@ -57,7 +57,7 @@ const logGitSetupInstructionsAndExit = () => {
57
57
 
58
58
  ${chalk.cyanBright.bold('git init')}
59
59
 
60
- 2. Commit your files
60
+ 2. Add your files
61
61
 
62
62
  ${chalk.cyanBright.bold('git add .')}
63
63
 
package/src/utils/dev.js CHANGED
@@ -14,7 +14,7 @@ const { loadDotEnvFiles } = require('./dot-env')
14
14
  // Possible sources of environment variables. For the purpose of printing log messages only. Order does not matter.
15
15
  const ENV_VAR_SOURCES = {
16
16
  account: {
17
- name: 'shared build settings',
17
+ name: 'shared',
18
18
  printFn: chalk.magenta,
19
19
  },
20
20
  addons: {
@@ -34,7 +34,7 @@ const ENV_VAR_SOURCES = {
34
34
  printFn: chalk.red,
35
35
  },
36
36
  ui: {
37
- name: 'build settings',
37
+ name: 'site settings',
38
38
  printFn: chalk.blue,
39
39
  },
40
40
  }
@@ -1,3 +1,153 @@
1
+ /**
2
+ * Finds a matching environment variable value from a given context
3
+ * @param {Array<object>} values - An array of environment variable values from Envelope
4
+ * @param {enum<dev,branch-deploy,deploy-preview,production>} context - The deploy context of the environment variable value
5
+ * @returns {object<context: enum<dev,branch-deploy,deploy-preview,production>, value: string>} The matching environment variable value object
6
+ */
7
+ const findValueFromContext = (values, context) => values.find((val) => [context, 'all'].includes(val.context))
8
+
9
+ /**
10
+ * Finds environment variables that match a given source
11
+ * @param {object} env - The dictionary of environment variables
12
+ * @param {enum<general,account,addons,ui,configFile>} source - The source of the environment variable
13
+ * @returns {object} The dictionary of env vars that match the given source
14
+ */
15
+ const filterEnvBySource = (env, source) =>
16
+ Object.fromEntries(Object.entries(env).filter(([, variable]) => variable.sources[0] === source))
17
+
18
+ /**
19
+ * Fetches data from Envelope
20
+ * @param {string} accountId - The account id
21
+ * @param {object} api - The api singleton object
22
+ * @param {string} key - If present, fetch a single key (case-sensitive)
23
+ * @param {string} siteId - The site id
24
+ * @returns {Array<object>} An array of environment variables from the Envelope service
25
+ */
26
+ const fetchEnvelopeItems = async function ({ accountId, api, key, siteId }) {
27
+ if (accountId === undefined) {
28
+ return {}
29
+ }
30
+ try {
31
+ // if a single key is passed, fetch that single env var
32
+ if (key) {
33
+ const envelopeItem = await api.getEnvVar({ accountId, key, siteId })
34
+ return [envelopeItem]
35
+ }
36
+ // otherwise, fetch the entire list of env vars
37
+ const envelopeItems = await api.getEnvVars({ accountId, siteId })
38
+ return envelopeItems
39
+ } catch {
40
+ // Collaborators aren't allowed to read shared env vars,
41
+ // so return an empty array silently in that case
42
+ return []
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Filters and sorts data from Envelope by a given context and/or scope
48
+ * @param {enum<dev,branch-deploy,deploy-preview,production>} context - The deploy context of the environment variable value
49
+ * @param {Array<object>} envelopeItems - An array of environment variables from the Envelope service
50
+ * @param {enum<any,builds,functions,runtime,post_processing>} scope - The scope of the environment variables
51
+ * @param {enum<general,account,addons,ui,configFile>} source - The source of the environment variable
52
+ * @returns {object} A dicionary in the following format:
53
+ * {
54
+ * FOO: {
55
+ * context: 'dev',
56
+ * scopes: ['builds', 'functions'],
57
+ * sources: ['ui'],
58
+ * value: 'bar',
59
+ * },
60
+ * BAZ: {
61
+ * context: 'dev',
62
+ * scopes: ['runtime'],
63
+ * sources: ['account'],
64
+ * value: 'bang',
65
+ * },
66
+ * }
67
+ */
68
+ const formatEnvelopeData = ({ context = 'dev', envelopeItems = [], scope = 'any', source }) =>
69
+ envelopeItems
70
+ // filter by context
71
+ .filter(({ values }) => Boolean(findValueFromContext(values, context)))
72
+ // filter by scope
73
+ .filter(({ scopes }) => (scope === 'any' ? true : scopes.includes(scope)))
74
+ // sort alphabetically, case insensitive
75
+ .sort((left, right) => (left.key.toLowerCase() < right.key.toLowerCase() ? -1 : 1))
76
+ // format the data
77
+ .reduce((acc, cur) => {
78
+ const { context: ctx, value } = findValueFromContext(cur.values, context)
79
+ return {
80
+ ...acc,
81
+ [cur.key]: {
82
+ context: ctx,
83
+ scopes: cur.scopes,
84
+ sources: [source],
85
+ value,
86
+ },
87
+ }
88
+ }, {})
89
+
90
+ /**
91
+ * Collects env vars from multiple sources and arranges them in the correct order of precedence
92
+ * @param {object} api - The api singleton object
93
+ * @param {enum<dev,branch-deploy,deploy-preview,production>} context - The deploy context of the environment variable
94
+ * @param {object} env - The dictionary of environment variables
95
+ * @param {string} key - If present, fetch a single key (case-sensitive)
96
+ * @param {enum<any,builds,functions,runtime,post_processing>} scope - The scope of the environment variables
97
+ * @param {object} siteInfo - The site object
98
+ * @returns {object} An object of environment variables keys and their metadata
99
+ */
100
+ const getEnvelopeEnv = async ({ api, context = 'dev', env, key = '', scope = 'any', siteInfo }) => {
101
+ const { account_slug: accountId, id: siteId } = siteInfo
102
+
103
+ const [accountEnvelopeItems, siteEnvelopeItems] = await Promise.all([
104
+ fetchEnvelopeItems({ api, accountId, key }),
105
+ fetchEnvelopeItems({ api, accountId, key, siteId }),
106
+ ])
107
+
108
+ const accountEnv = formatEnvelopeData({ context, envelopeItems: accountEnvelopeItems, scope, source: 'account' })
109
+ const siteEnv = formatEnvelopeData({ context, envelopeItems: siteEnvelopeItems, scope, source: 'ui' })
110
+ const generalEnv = filterEnvBySource(env, 'general')
111
+ const addonsEnv = filterEnvBySource(env, 'addons')
112
+ const configFileEnv = filterEnvBySource(env, 'configFile')
113
+
114
+ // filter out configFile env vars if a non-configFile scope is passed
115
+ const includeConfigEnvVars = ['any', 'builds', 'post_processing'].includes(scope)
116
+
117
+ // Sources of environment variables, in ascending order of precedence.
118
+ return {
119
+ ...generalEnv,
120
+ ...accountEnv,
121
+ ...(includeConfigEnvVars ? addonsEnv : {}),
122
+ ...siteEnv,
123
+ ...(includeConfigEnvVars ? configFileEnv : {}),
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Returns a human-readable, comma-separated list of scopes
129
+ * @param {Array<enum<builds,functions,runtime,post_processing>>} scopes - An array of scopes
130
+ * @returns {string} A human-readable, comma-separated list of scopes
131
+ */
132
+ const getHumanReadableScopes = (scopes) => {
133
+ const AVAILABLE_SCOPES = {
134
+ builds: 'Builds',
135
+ functions: 'Functions',
136
+ post_processing: 'Post processing',
137
+ runtime: 'Runtime',
138
+ }
139
+ if (!scopes) {
140
+ // if `scopes` is not available, the env var comes from netlify.toml
141
+ // env vars specified in netlify.toml are present in the `builds` and `post_processing` scope
142
+ return 'Builds, Post processing'
143
+ }
144
+ if (scopes.length === Object.keys(AVAILABLE_SCOPES).length) {
145
+ // shorthand instead of listing every available scope
146
+ return 'All'
147
+ }
148
+ return scopes.map((scope) => AVAILABLE_SCOPES[scope]).join(', ')
149
+ }
150
+
1
151
  /**
2
152
  * Translates a Mongo env into an Envelope env
3
153
  * @param {object} env - The site's env as it exists in Mongo
@@ -38,6 +188,11 @@ const translateFromEnvelopeToMongo = (envVars = []) =>
38
188
  }, {})
39
189
 
40
190
  module.exports = {
41
- translateFromMongoToEnvelope,
191
+ findValueFromContext,
192
+ filterEnvBySource,
193
+ formatEnvelopeData,
194
+ getEnvelopeEnv,
195
+ getHumanReadableScopes,
42
196
  translateFromEnvelopeToMongo,
197
+ translateFromMongoToEnvelope,
43
198
  }