netlify-cli 10.17.7 → 10.18.0-rc.4

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,17 +1,17 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
- "version": "10.17.7",
3
+ "version": "10.18.0-rc.4",
4
4
  "lockfileVersion": 2,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "netlify-cli",
9
- "version": "10.17.7",
9
+ "version": "10.18.0-rc.4",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {
13
- "@netlify/build": "^27.11.5",
14
- "@netlify/config": "^18.1.4",
13
+ "@netlify/build": "^27.14.0",
14
+ "@netlify/config": "^18.2.0",
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",
@@ -1189,13 +1189,13 @@
1189
1189
  "integrity": "sha512-4wMPu9iN3/HL97QblBsBay3E1etIciR84izI3U+4iALY+JHCrI+a2jO0qbAZ/nxKoegypYEaiiqWXylm+/zfrw=="
1190
1190
  },
1191
1191
  "node_modules/@netlify/build": {
1192
- "version": "27.11.5",
1193
- "resolved": "https://registry.npmjs.org/@netlify/build/-/build-27.11.5.tgz",
1194
- "integrity": "sha512-d/EqdcVZzi/EA0rJ21xKvHqugDx2gl3h1mpzk/HQpAmvGtgv+Tkj9W4oVYKyd8L2Pyx/oiFBNyhfNXdCEk3wbA==",
1192
+ "version": "27.14.0",
1193
+ "resolved": "https://registry.npmjs.org/@netlify/build/-/build-27.14.0.tgz",
1194
+ "integrity": "sha512-/o1Q+Z2JNpU15S63AsyT4HBPbiweMhBO6jclxNN9kjBnlTefcBJ1c5zN7cOlrcHM+SC+tRPC0oJM/aL/OkXsWg==",
1195
1195
  "dependencies": {
1196
1196
  "@bugsnag/js": "^7.0.0",
1197
1197
  "@netlify/cache-utils": "^4.0.0",
1198
- "@netlify/config": "^18.1.4",
1198
+ "@netlify/config": "^18.2.0",
1199
1199
  "@netlify/edge-bundler": "^1.12.1",
1200
1200
  "@netlify/functions-utils": "^4.2.4",
1201
1201
  "@netlify/git-utils": "^4.0.0",
@@ -2011,9 +2011,9 @@
2011
2011
  }
2012
2012
  },
2013
2013
  "node_modules/@netlify/config": {
2014
- "version": "18.1.4",
2015
- "resolved": "https://registry.npmjs.org/@netlify/config/-/config-18.1.4.tgz",
2016
- "integrity": "sha512-tM8qDVwBMTYB0n7R6EA6BtuVw9Iq6lNrqE8RjAY4eZ9cjbZkow4M2e0XhmYaNRderLhjYrDlqQr/ttUQA1tZRw==",
2014
+ "version": "18.2.0",
2015
+ "resolved": "https://registry.npmjs.org/@netlify/config/-/config-18.2.0.tgz",
2016
+ "integrity": "sha512-UxJ2bg9KQVNzzPhuTRVybtXLuVo/XBw8CCVJkWHkVSJlomYvtnA0epTreRHjtGSvy++10Rhu37WwzRbFNG5rBQ==",
2017
2017
  "dependencies": {
2018
2018
  "chalk": "^5.0.0",
2019
2019
  "cron-parser": "^4.1.0",
@@ -23345,13 +23345,13 @@
23345
23345
  "integrity": "sha512-4wMPu9iN3/HL97QblBsBay3E1etIciR84izI3U+4iALY+JHCrI+a2jO0qbAZ/nxKoegypYEaiiqWXylm+/zfrw=="
23346
23346
  },
23347
23347
  "@netlify/build": {
23348
- "version": "27.11.5",
23349
- "resolved": "https://registry.npmjs.org/@netlify/build/-/build-27.11.5.tgz",
23350
- "integrity": "sha512-d/EqdcVZzi/EA0rJ21xKvHqugDx2gl3h1mpzk/HQpAmvGtgv+Tkj9W4oVYKyd8L2Pyx/oiFBNyhfNXdCEk3wbA==",
23348
+ "version": "27.14.0",
23349
+ "resolved": "https://registry.npmjs.org/@netlify/build/-/build-27.14.0.tgz",
23350
+ "integrity": "sha512-/o1Q+Z2JNpU15S63AsyT4HBPbiweMhBO6jclxNN9kjBnlTefcBJ1c5zN7cOlrcHM+SC+tRPC0oJM/aL/OkXsWg==",
23351
23351
  "requires": {
23352
23352
  "@bugsnag/js": "^7.0.0",
23353
23353
  "@netlify/cache-utils": "^4.0.0",
23354
- "@netlify/config": "^18.1.4",
23354
+ "@netlify/config": "^18.2.0",
23355
23355
  "@netlify/edge-bundler": "^1.12.1",
23356
23356
  "@netlify/functions-utils": "^4.2.4",
23357
23357
  "@netlify/git-utils": "^4.0.0",
@@ -23856,9 +23856,9 @@
23856
23856
  }
23857
23857
  },
23858
23858
  "@netlify/config": {
23859
- "version": "18.1.4",
23860
- "resolved": "https://registry.npmjs.org/@netlify/config/-/config-18.1.4.tgz",
23861
- "integrity": "sha512-tM8qDVwBMTYB0n7R6EA6BtuVw9Iq6lNrqE8RjAY4eZ9cjbZkow4M2e0XhmYaNRderLhjYrDlqQr/ttUQA1tZRw==",
23859
+ "version": "18.2.0",
23860
+ "resolved": "https://registry.npmjs.org/@netlify/config/-/config-18.2.0.tgz",
23861
+ "integrity": "sha512-UxJ2bg9KQVNzzPhuTRVybtXLuVo/XBw8CCVJkWHkVSJlomYvtnA0epTreRHjtGSvy++10Rhu37WwzRbFNG5rBQ==",
23862
23862
  "requires": {
23863
23863
  "chalk": "^5.0.0",
23864
23864
  "cron-parser": "^4.1.0",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
3
  "description": "Netlify command line tool",
4
- "version": "10.17.7",
4
+ "version": "10.18.0-rc.4",
5
5
  "author": "Netlify Inc.",
6
6
  "contributors": [
7
7
  "@whitep4nth3r (https://twitter.com/whitep4nth3r)",
@@ -205,8 +205,8 @@
205
205
  "test:init:eleventy-deps": "npm ci --prefix tests/integration/eleventy-site --no-audit",
206
206
  "test:init:hugo-deps": "npm ci --prefix tests/integration/hugo-site --no-audit",
207
207
  "test:dev:ava": "ava --verbose",
208
- "test:ci:ava:unit": "c8 -r json ava --no-worker-threads tests/unit/**/*.test.js tools/**/*.test.js",
209
- "test:ci:ava:integration": "c8 -r json ava --concurrency 1 --no-worker-threads tests/integration/**/*.test.js",
208
+ "test:ci:ava:unit": "c8 -r json ava --no-worker-threads tests/unit/ tools/",
209
+ "test:ci:ava:integration": "c8 -r json ava --concurrency 1 --no-worker-threads tests/integration/",
210
210
  "test:affected": "node ./tools/affected-test.mjs",
211
211
  "e2e": "node ./tools/e2e/run.mjs",
212
212
  "docs": "node ./site/scripts/docs.mjs",
@@ -222,8 +222,8 @@
222
222
  "prettier": "--ignore-path .gitignore --loglevel=warn \"{src,tools,scripts,site,tests,.github}/**/*.{mjs,cjs,js,md,yml,json,html}\" \"*.{mjs,cjs,js,yml,json,html}\" \".*.{mjs,cjs,js,yml,json,html}\" \"!CHANGELOG.md\" \"!npm-shrinkwrap.json\" \"!**/*/package-lock.json\" \"!.github/**/*.md\""
223
223
  },
224
224
  "dependencies": {
225
- "@netlify/build": "^27.11.5",
226
- "@netlify/config": "^18.1.4",
225
+ "@netlify/build": "^27.14.0",
226
+ "@netlify/config": "^18.2.0",
227
227
  "@netlify/edge-bundler": "^1.12.1",
228
228
  "@netlify/framework-info": "^9.2.0",
229
229
  "@netlify/local-functions-proxy": "^1.1.1",
@@ -1,6 +1,8 @@
1
+ const process = require('process')
2
+
1
3
  // @ts-check
2
4
  const { getBuildOptions, runBuild } = require('../../lib/build')
3
- const { error, exit, generateNetlifyGraphJWT, getToken } = require('../../utils')
5
+ const { error, exit, generateNetlifyGraphJWT, getEnvelopeEnv, getToken } = require('../../utils')
4
6
 
5
7
  /**
6
8
  * @param {import('../../lib/build').BuildConfig} options
@@ -15,9 +17,14 @@ const checkOptions = ({ cachedConfig: { siteInfo = {} }, token }) => {
15
17
  }
16
18
  }
17
19
 
18
- const injectNetlifyGraphEnv = async function (command, { api, buildOptions, site }) {
19
- const siteData = await api.getSite({ siteId: site.id })
20
- const authlifyTokenId = siteData && siteData.authlify_token_id
20
+ const injectEnv = async function (command, { api, buildOptions, context, site, siteInfo }) {
21
+ const isUsingEnvelope = siteInfo && siteInfo.use_envelope
22
+ const authlifyTokenId = siteInfo && siteInfo.authlify_token_id
23
+
24
+ const { env } = buildOptions.cachedConfig
25
+ if (isUsingEnvelope) {
26
+ buildOptions.cachedConfig.env = await getEnvelopeEnv({ api, context, env, siteInfo })
27
+ }
21
28
 
22
29
  if (authlifyTokenId) {
23
30
  const netlifyToken = await command.authenticate()
@@ -48,12 +55,12 @@ const injectNetlifyGraphEnv = async function (command, { api, buildOptions, site
48
55
  */
49
56
  const build = async (options, command) => {
50
57
  command.setAnalyticsPayload({ dry: options.dry })
51
-
52
58
  // Retrieve Netlify Build options
53
59
  const [token] = await getToken()
54
60
 
61
+ const { cachedConfig, siteInfo } = command.netlify
55
62
  const buildOptions = await getBuildOptions({
56
- cachedConfig: command.netlify.cachedConfig,
63
+ cachedConfig,
57
64
  token,
58
65
  options,
59
66
  })
@@ -61,7 +68,8 @@ const build = async (options, command) => {
61
68
  if (!options.offline) {
62
69
  checkOptions(buildOptions)
63
70
  const { api, site } = command.netlify
64
- await injectNetlifyGraphEnv(command, { api, site, buildOptions })
71
+ const { context } = options
72
+ await injectEnv(command, { api, buildOptions, context, site, siteInfo })
65
73
  }
66
74
 
67
75
  const { exitCode } = await runBuild(buildOptions)
@@ -77,8 +85,8 @@ const createBuildCommand = (program) =>
77
85
  program
78
86
  .command('build')
79
87
  .description('(Beta) Build on your local machine')
88
+ .option('--context <context>', 'Specify a build context', process.env.CONTEXT || 'production')
80
89
  .option('--dry', 'Dry run: show instructions without running them', false)
81
- .option('--context [context]', 'Build context')
82
90
  .option('-o, --offline', 'disables any features that require network access', false)
83
91
  .addExamples(['netlify build'])
84
92
  .action(build)
@@ -1,6 +1,7 @@
1
+ const { Option } = require('commander')
1
2
  const execa = require('execa')
2
3
 
3
- const { injectEnvVariables } = require('../../utils')
4
+ const { getEnvelopeEnv, injectEnvVariables } = require('../../utils')
4
5
 
5
6
  /**
6
7
  * The dev:exec command
@@ -8,8 +9,14 @@ const { injectEnvVariables } = require('../../utils')
8
9
  * @param {import('../base-command').BaseCommand} command
9
10
  */
10
11
  const devExec = async (cmd, options, command) => {
11
- const { cachedConfig, config, site } = command.netlify
12
- await injectEnvVariables({ devConfig: { ...config.dev }, env: cachedConfig.env, site })
12
+ const { api, cachedConfig, config, site, siteInfo } = command.netlify
13
+
14
+ let { env } = cachedConfig
15
+ if (siteInfo.use_envelope) {
16
+ env = await getEnvelopeEnv({ api, context: options.context, env, siteInfo })
17
+ }
18
+
19
+ await injectEnvVariables({ devConfig: { ...config.dev }, env, site })
13
20
 
14
21
  await execa(cmd, command.args.slice(1), {
15
22
  stdio: 'inherit',
@@ -25,6 +32,11 @@ const createDevExecCommand = (program) =>
25
32
  program
26
33
  .command('dev:exec')
27
34
  .argument('<...cmd>', `the command that should be executed`)
35
+ .addOption(
36
+ new Option('--context <context>', 'Specify a deploy context for environment variables')
37
+ .choices(['production', 'deploy-preview', 'branch-deploy', 'dev'])
38
+ .default('dev'),
39
+ )
28
40
  .description(
29
41
  'Exec command\nRuns a command within the netlify dev environment, e.g. with env variables from any installed addons',
30
42
  )
@@ -430,7 +430,7 @@ const dev = async (options, command) => {
430
430
  }
431
431
 
432
432
  let { env } = command.netlify.cachedConfig
433
- if (siteInfo.use_envelope) {
433
+ if (!options.offline && siteInfo.use_envelope) {
434
434
  env = await getEnvelopeEnv({ api, context: options.context, env, siteInfo })
435
435
  }
436
436
 
@@ -74,11 +74,7 @@ const envClone = async (options, command) => {
74
74
  return false
75
75
  }
76
76
 
77
- log(
78
- `Successfully cloned environment variables from ${chalk.greenBright(siteFrom.name)} to ${chalk.greenBright(
79
- siteTo.name,
80
- )}`,
81
- )
77
+ log(`Successfully cloned environment variables from ${chalk.green(siteFrom.name)} to ${chalk.green(siteTo.name)}`)
82
78
 
83
79
  return true
84
80
  }
@@ -98,7 +94,7 @@ const mongoToMongo = async ({ api, siteFrom, siteTo }) => {
98
94
  ] = [siteFrom, siteTo]
99
95
 
100
96
  if (isEmpty(envFrom)) {
101
- log(`${chalk.greenBright(siteFrom.name)} has no environment variables, nothing to clone`)
97
+ log(`${chalk.green(siteFrom.name)} has no environment variables, nothing to clone`)
102
98
  return false
103
99
  }
104
100
 
@@ -130,7 +126,7 @@ const mongoToEnvelope = async ({ api, siteFrom, siteTo }) => {
130
126
  const keysFrom = Object.keys(envFrom)
131
127
 
132
128
  if (isEmpty(envFrom)) {
133
- log(`${chalk.greenBright(siteFrom.name)} has no environment variables, nothing to clone`)
129
+ log(`${chalk.green(siteFrom.name)} has no environment variables, nothing to clone`)
134
130
  return false
135
131
  }
136
132
 
@@ -163,7 +159,7 @@ const envelopeToMongo = async ({ api, siteFrom, siteTo }) => {
163
159
  const envFrom = translateFromEnvelopeToMongo(envelopeVariables)
164
160
 
165
161
  if (isEmpty(envFrom)) {
166
- log(`${chalk.greenBright(siteFrom.name)} has no environment variables, nothing to clone`)
162
+ log(`${chalk.green(siteFrom.name)} has no environment variables, nothing to clone`)
167
163
  return false
168
164
  }
169
165
 
@@ -201,7 +197,7 @@ const envelopeToEnvelope = async ({ api, siteFrom, siteTo }) => {
201
197
  const keysFrom = envelopeFrom.map(({ key }) => key)
202
198
 
203
199
  if (isEmpty(keysFrom)) {
204
- log(`${chalk.greenBright(siteFrom.name)} has no environment variables, nothing to clone`)
200
+ log(`${chalk.green(siteFrom.name)} has no environment variables, nothing to clone`)
205
201
  return false
206
202
  }
207
203
 
@@ -26,7 +26,7 @@ const envGet = async (name, options, command) => {
26
26
  env = await getEnvelopeEnv({ api, context, env, key: name, scope, siteInfo })
27
27
  } else if (context !== 'dev' || scope !== 'any') {
28
28
  error(
29
- `To specify a context or scope, please run ${chalk.yellowBright(
29
+ `To specify a context or scope, please run ${chalk.yellow(
30
30
  'netlify open:admin',
31
31
  )} to open the Netlify UI and opt in to the new environment variables experience from Site settings`,
32
32
  )
@@ -42,9 +42,9 @@ const envGet = async (name, options, command) => {
42
42
  }
43
43
 
44
44
  if (!value) {
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)}`)
45
+ const withContext = `in the ${chalk.magenta(context)} context`
46
+ const withScope = scope === 'any' ? '' : ` and the ${chalk.magenta(scope)} scope`
47
+ log(`No value set ${withContext}${withScope} for environment variable ${chalk.yellow(name)}`)
48
48
  return false
49
49
  }
50
50
 
@@ -15,7 +15,7 @@ const { exit, log, logJson, translateFromEnvelopeToMongo, translateFromMongoToEn
15
15
  * @returns {Promise<boolean>}
16
16
  */
17
17
  const envImport = async (fileName, options, command) => {
18
- const { api, site } = command.netlify
18
+ const { api, cachedConfig, site } = command.netlify
19
19
  const siteId = site.id
20
20
 
21
21
  if (!siteId) {
@@ -37,10 +37,10 @@ const envImport = async (fileName, options, command) => {
37
37
  return false
38
38
  }
39
39
 
40
- const siteData = await api.getSite({ siteId })
40
+ const { siteInfo } = cachedConfig
41
41
 
42
- const importIntoService = siteData.use_envelope ? importIntoEnvelope : importIntoMongo
43
- const finalEnv = await importIntoService({ api, importedEnv, options, siteData })
42
+ const importIntoService = siteInfo.use_envelope ? importIntoEnvelope : importIntoMongo
43
+ const finalEnv = await importIntoService({ api, importedEnv, options, siteInfo })
44
44
 
45
45
  // Return new environment variables of site if using json flag
46
46
  if (options.json) {
@@ -49,7 +49,7 @@ const envImport = async (fileName, options, command) => {
49
49
  }
50
50
 
51
51
  // List newly imported environment variables in a table
52
- log(`site: ${siteData.name}`)
52
+ log(`site: ${siteInfo.name}`)
53
53
  const table = new AsciiTable(`Imported environment variables`)
54
54
 
55
55
  table.setHeading('Key', 'Value')
@@ -61,9 +61,9 @@ const envImport = async (fileName, options, command) => {
61
61
  * Updates the imported env in the site record
62
62
  * @returns {Promise<object>}
63
63
  */
64
- const importIntoMongo = async ({ api, importedEnv, options, siteData }) => {
65
- const { env = {} } = siteData.build_settings
66
- const siteId = siteData.id
64
+ const importIntoMongo = async ({ api, importedEnv, options, siteInfo }) => {
65
+ const { env = {} } = siteInfo.build_settings
66
+ const siteId = siteInfo.id
67
67
 
68
68
  const finalEnv = options.replaceExisting ? importedEnv : { ...env, ...importedEnv }
69
69
 
@@ -86,10 +86,10 @@ const importIntoMongo = async ({ api, importedEnv, options, siteData }) => {
86
86
  * Saves the imported env in the Envelope service
87
87
  * @returns {Promise<object>}
88
88
  */
89
- const importIntoEnvelope = async ({ api, importedEnv, options, siteData }) => {
89
+ const importIntoEnvelope = async ({ api, importedEnv, options, siteInfo }) => {
90
90
  // fetch env vars
91
- const accountId = siteData.account_slug
92
- const siteId = siteData.id
91
+ const accountId = siteInfo.account_slug
92
+ const siteId = siteInfo.id
93
93
  const dotEnvKeys = Object.keys(importedEnv)
94
94
  const envelopeVariables = await api.getEnvVars({ accountId, siteId })
95
95
  const envelopeKeys = envelopeVariables.map(({ key }) => key)
@@ -22,7 +22,7 @@ const getTable = ({ environment, hideValues, scopesColumn }) => {
22
22
  // Key
23
23
  key,
24
24
  // Value
25
- hideValues ? MASK : variable.value,
25
+ hideValues ? MASK : variable.value || ' ',
26
26
  // Scope
27
27
  scopesColumn && getHumanReadableScopes(variable.scopes),
28
28
  ].filter(Boolean),
@@ -55,7 +55,7 @@ const envList = async (options, command) => {
55
55
  environment = await getEnvelopeEnv({ api, context, env, scope, siteInfo })
56
56
  } else if (context !== 'dev' || scope !== 'any') {
57
57
  error(
58
- `To specify a context or scope, please run ${chalk.yellowBright(
58
+ `To specify a context or scope, please run ${chalk.yellow(
59
59
  'netlify open:admin',
60
60
  )} to open the Netlify UI and opt in to the new environment variables experience from Site settings`,
61
61
  )
@@ -76,9 +76,9 @@ const envList = async (options, command) => {
76
76
  return false
77
77
  }
78
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` : ''
79
+ const forSite = `for site ${chalk.green(siteInfo.name)}`
80
+ const withContext = isUsingEnvelope ? `in the ${chalk.magenta(options.context)} context` : ''
81
+ const withScope = isUsingEnvelope && scope !== 'any' ? `and ${chalk.yellow(options.scope)} scope` : ''
82
82
  if (isEmpty(environment)) {
83
83
  log(`No environment variables set ${forSite} ${withContext} ${withScope}`)
84
84
  return false
@@ -1,5 +1,9 @@
1
1
  // @ts-check
2
- const { log, logJson, translateFromEnvelopeToMongo } = require('../../utils')
2
+ const { Option } = require('commander')
3
+
4
+ const { chalk, error, log, logJson, translateFromEnvelopeToMongo } = require('../../utils')
5
+
6
+ const AVAILABLE_SCOPES = ['builds', 'functions', 'runtime', 'post_processing']
3
7
 
4
8
  /**
5
9
  * The env:set command
@@ -10,7 +14,9 @@ const { log, logJson, translateFromEnvelopeToMongo } = require('../../utils')
10
14
  * @returns {Promise<boolean>}
11
15
  */
12
16
  const envSet = async (key, value, options, command) => {
13
- const { api, site } = command.netlify
17
+ const { context, scope } = options
18
+
19
+ const { api, cachedConfig, site } = command.netlify
14
20
  const siteId = site.id
15
21
 
16
22
  if (!siteId) {
@@ -18,11 +24,26 @@ const envSet = async (key, value, options, command) => {
18
24
  return false
19
25
  }
20
26
 
21
- const siteData = await api.getSite({ siteId })
27
+ const { siteInfo } = cachedConfig
28
+ let finalEnv
22
29
 
23
30
  // Get current environment variables set in the UI
24
- const setInService = siteData.use_envelope ? setInEnvelope : setInMongo
25
- const finalEnv = await setInService({ api, siteData, key, value })
31
+ if (siteInfo.use_envelope) {
32
+ finalEnv = await setInEnvelope({ api, siteInfo, key, value, context, scope })
33
+ } else if (context || scope) {
34
+ error(
35
+ `To specify a context or scope, please run ${chalk.yellow(
36
+ 'netlify open:admin',
37
+ )} to open the Netlify UI and opt in to the new environment variables experience from Site settings`,
38
+ )
39
+ return false
40
+ } else {
41
+ finalEnv = await setInMongo({ api, siteInfo, key, value })
42
+ }
43
+
44
+ if (!finalEnv) {
45
+ return false
46
+ }
26
47
 
27
48
  // Return new environment variables of site if using json flag
28
49
  if (options.json) {
@@ -30,22 +51,27 @@ const envSet = async (key, value, options, command) => {
30
51
  return false
31
52
  }
32
53
 
33
- log(`Set environment variable ${key}=${value} for site ${siteData.name}`)
54
+ const withScope = scope ? ` scoped to ${chalk.white(scope)}` : ''
55
+ log(
56
+ `Set environment variable ${chalk.yellow(`${key}${value ? '=' : ''}${value}`)}${withScope} in the ${chalk.magenta(
57
+ context || 'all',
58
+ )} context`,
59
+ )
34
60
  }
35
61
 
36
62
  /**
37
63
  * Updates the env for a site record with a new key/value pair
38
64
  * @returns {Promise<object>}
39
65
  */
40
- const setInMongo = async ({ api, key, siteData, value }) => {
41
- const { env = {} } = siteData.build_settings
66
+ const setInMongo = async ({ api, key, siteInfo, value }) => {
67
+ const { env = {} } = siteInfo.build_settings
42
68
  const newEnv = {
43
69
  ...env,
44
70
  [key]: value,
45
71
  }
46
72
  // Apply environment variable updates
47
73
  await api.updateSite({
48
- siteId: siteData.id,
74
+ siteId: siteInfo.id,
49
75
  body: {
50
76
  build_settings: {
51
77
  env: newEnv,
@@ -59,28 +85,52 @@ const setInMongo = async ({ api, key, siteData, value }) => {
59
85
  * Updates the env for a site configured with Envelope with a new key/value pair
60
86
  * @returns {Promise<object>}
61
87
  */
62
- const setInEnvelope = async ({ api, key, siteData, value }) => {
63
- const accountId = siteData.account_slug
64
- const siteId = siteData.id
88
+ const setInEnvelope = async ({ api, context, key, scope, siteInfo, value }) => {
89
+ const accountId = siteInfo.account_slug
90
+ const siteId = siteInfo.id
65
91
  // fetch envelope env vars
66
92
  const envelopeVariables = await api.getEnvVars({ accountId, siteId })
67
- const scopes = ['builds', 'functions', 'runtime', 'post_processing']
68
- const values = [{ context: 'all', value }]
69
- // check if we need to create or update
70
- const exists = envelopeVariables.some((envVar) => envVar.key === key)
71
- const method = exists ? api.updateEnvVar : api.createEnvVars
72
- const body = exists ? { key, scopes, values } : [{ key, scopes, values }]
93
+ const contexts = context || ['all']
94
+ const scopes = scope || AVAILABLE_SCOPES
95
+
96
+ let values = contexts.map((ctx) => ({ context: ctx, value }))
73
97
 
98
+ const existing = envelopeVariables.find((envVar) => envVar.key === key)
99
+
100
+ const params = { accountId, siteId, key }
74
101
  try {
75
- await method({ accountId, siteId, key, body })
76
- } catch (error) {
77
- throw error.json ? error.json.msg : error
102
+ if (existing) {
103
+ if (!value) {
104
+ // eslint-disable-next-line prefer-destructuring
105
+ values = existing.values
106
+ }
107
+ if (context && scope) {
108
+ error(
109
+ 'Setting the context and scope at the same time on an existing env var is not allowed. Run the set command separately for each update.',
110
+ )
111
+ return false
112
+ }
113
+ if (context) {
114
+ // update individual value(s)
115
+ await Promise.all(values.map((val) => api.setEnvVarValue({ ...params, body: val })))
116
+ } else {
117
+ // otherwise update whole env var
118
+ const body = { key, scopes, values }
119
+ await api.updateEnvVar({ ...params, body })
120
+ }
121
+ } else {
122
+ // create whole env var
123
+ const body = [{ key, scopes, values }]
124
+ await api.createEnvVars({ ...params, body })
125
+ }
126
+ } catch (error_) {
127
+ throw error_.json ? error_.json.msg : error_
78
128
  }
79
129
 
80
- const env = translateFromEnvelopeToMongo(envelopeVariables)
130
+ const env = translateFromEnvelopeToMongo(envelopeVariables, context ? context[0] : 'dev')
81
131
  return {
82
132
  ...env,
83
- [key]: value,
133
+ [key]: value || env[key],
84
134
  }
85
135
  }
86
136
 
@@ -94,7 +144,30 @@ const createEnvSetCommand = (program) =>
94
144
  .command('env:set')
95
145
  .argument('<key>', 'Environment variable key')
96
146
  .argument('[value]', 'Value to set to', '')
147
+ .addOption(
148
+ new Option('-c, --context <context...>', 'Specify a deploy context (default: all contexts)').choices([
149
+ 'production',
150
+ 'deploy-preview',
151
+ 'branch-deploy',
152
+ 'dev',
153
+ ]),
154
+ )
155
+ .addOption(
156
+ new Option('-s, --scope <scope...>', 'Specify a scope (default: all scopes)').choices([
157
+ 'builds',
158
+ 'functions',
159
+ 'post_processing',
160
+ 'runtime',
161
+ ]),
162
+ )
97
163
  .description('Set value of environment variable')
164
+ .addExamples([
165
+ 'netlify env:set VAR_NAME value # set in all contexts and scopes',
166
+ 'netlify env:set VAR_NAME value --context production',
167
+ 'netlify env:set VAR_NAME value --context production deploy-preview',
168
+ 'netlify env:set VAR_NAME value --scope builds',
169
+ 'netlify env:set VAR_NAME value --scope builds functions',
170
+ ])
98
171
  .action(async (key, value, options, command) => {
99
172
  await envSet(key, value, options, command)
100
173
  })
@@ -1,5 +1,9 @@
1
+ const { Option } = require('commander')
2
+
1
3
  // @ts-check
2
- const { log, logJson, translateFromEnvelopeToMongo } = require('../../utils')
4
+ const { chalk, error, log, logJson, translateFromEnvelopeToMongo } = require('../../utils')
5
+
6
+ const AVAILABLE_CONTEXTS = ['production', 'deploy-preview', 'branch-deploy', 'dev']
3
7
 
4
8
  /**
5
9
  * The env:unset command
@@ -9,7 +13,8 @@ const { log, logJson, translateFromEnvelopeToMongo } = require('../../utils')
9
13
  * @returns {Promise<boolean>}
10
14
  */
11
15
  const envUnset = async (key, options, command) => {
12
- const { api, site } = command.netlify
16
+ const { context } = options
17
+ const { api, cachedConfig, site } = command.netlify
13
18
  const siteId = site.id
14
19
 
15
20
  if (!siteId) {
@@ -17,10 +22,21 @@ const envUnset = async (key, options, command) => {
17
22
  return false
18
23
  }
19
24
 
20
- const siteData = await api.getSite({ siteId })
21
-
22
- const unsetInService = siteData.use_envelope ? unsetInEnvelope : unsetInMongo
23
- const finalEnv = await unsetInService({ api, siteData, key })
25
+ const { siteInfo } = cachedConfig
26
+
27
+ let finalEnv
28
+ if (siteInfo.use_envelope) {
29
+ finalEnv = await unsetInEnvelope({ api, context, siteInfo, key })
30
+ } else if (context) {
31
+ error(
32
+ `To specify a context, please run ${chalk.yellow(
33
+ 'netlify open:admin',
34
+ )} to open the Netlify UI and opt in to the new environment variables experience from Site settings`,
35
+ )
36
+ return false
37
+ } else {
38
+ finalEnv = await unsetInMongo({ api, siteInfo, key })
39
+ }
24
40
 
25
41
  // Return new environment variables of site if using json flag
26
42
  if (options.json) {
@@ -28,18 +44,18 @@ const envUnset = async (key, options, command) => {
28
44
  return false
29
45
  }
30
46
 
31
- log(`Unset environment variable ${key} for site ${siteData.name}`)
47
+ log(`Unset environment variable ${chalk.yellow(key)} in the ${chalk.magenta(context || 'all')} context`)
32
48
  }
33
49
 
34
50
  /**
35
51
  * Deletes a given key from the env of a site record
36
52
  * @returns {Promise<object>}
37
53
  */
38
- const unsetInMongo = async ({ api, key, siteData }) => {
54
+ const unsetInMongo = async ({ api, key, siteInfo }) => {
39
55
  // Get current environment variables set in the UI
40
56
  const {
41
57
  build_settings: { env = {} },
42
- } = siteData
58
+ } = siteInfo
43
59
 
44
60
  const newEnv = env
45
61
 
@@ -48,7 +64,7 @@ const unsetInMongo = async ({ api, key, siteData }) => {
48
64
 
49
65
  // Apply environment variable updates
50
66
  await api.updateSite({
51
- siteId: siteData.id,
67
+ siteId: siteInfo.id,
52
68
  body: {
53
69
  build_settings: {
54
70
  env: newEnv,
@@ -63,24 +79,44 @@ const unsetInMongo = async ({ api, key, siteData }) => {
63
79
  * Deletes a given key from the env of a site configured with Envelope
64
80
  * @returns {Promise<object>}
65
81
  */
66
- const unsetInEnvelope = async ({ api, key, siteData }) => {
67
- const accountId = siteData.account_slug
68
- const siteId = siteData.id
82
+ const unsetInEnvelope = async ({ api, context, key, siteInfo }) => {
83
+ const accountId = siteInfo.account_slug
84
+ const siteId = siteInfo.id
69
85
  // fetch envelope env vars
70
86
  const envelopeVariables = await api.getEnvVars({ accountId, siteId })
87
+ const contexts = context || ['all']
88
+
89
+ const env = translateFromEnvelopeToMongo(envelopeVariables, context ? context[0] : 'dev')
71
90
 
72
91
  // check if the given key exists
73
- const env = translateFromEnvelopeToMongo(envelopeVariables)
74
- if (!env[key]) {
92
+ const variable = envelopeVariables.find((envVar) => envVar.key === key)
93
+ if (!variable) {
75
94
  // if not, no need to call delete; return early
76
95
  return env
77
96
  }
78
97
 
79
- // delete the given key
98
+ const params = { accountId, siteId, key }
80
99
  try {
81
- await api.deleteEnvVar({ accountId, siteId, key })
82
- } catch (error) {
83
- throw error.json ? error.json.msg : error
100
+ if (context) {
101
+ // if context(s) are passed, delete the matching contexts, and the `all` context
102
+ const values = variable.values.filter((val) => [...contexts, 'all'].includes(val.context))
103
+ if (values) {
104
+ await Promise.all(values.map((value) => api.deleteEnvVarValue({ ...params, id: value.id })))
105
+ // if this was the `all` context, we need to create 3 values in the other contexts
106
+ if (values.length === 1 && values[0].context === 'all') {
107
+ const newContexts = AVAILABLE_CONTEXTS.filter((ctx) => !context.includes(ctx))
108
+ const allValue = values[0].value
109
+ await Promise.all(
110
+ newContexts.map((ctx) => api.setEnvVarValue({ ...params, body: { context: ctx, value: allValue } })),
111
+ )
112
+ }
113
+ }
114
+ } else {
115
+ // otherwise, if no context passed, delete the whole key
116
+ await api.deleteEnvVar({ accountId, siteId, key })
117
+ }
118
+ } catch (error_) {
119
+ throw error_.json ? error_.json.msg : error_
84
120
  }
85
121
 
86
122
  delete env[key]
@@ -98,6 +134,19 @@ const createEnvUnsetCommand = (program) =>
98
134
  .command('env:unset')
99
135
  .aliases(['env:delete', 'env:remove'])
100
136
  .argument('<key>', 'Environment variable key')
137
+ .addOption(
138
+ new Option('-c, --context <context...>', 'Specify a deploy context (default: all contexts)').choices([
139
+ 'production',
140
+ 'deploy-preview',
141
+ 'branch-deploy',
142
+ 'dev',
143
+ ]),
144
+ )
145
+ .addExamples([
146
+ 'netlify env:unset VAR_NAME # unset in all contexts',
147
+ 'netlify env:unset VAR_NAME --context production',
148
+ 'netlify env:unset VAR_NAME --context production deploy-preview',
149
+ ])
101
150
  .description('Unset an environment variable which removes it from the UI')
102
151
  .action(async (key, options, command) => {
103
152
  await envUnset(key, options, command)
@@ -15,6 +15,6 @@
15
15
  "author": "Netlify",
16
16
  "license": "MIT",
17
17
  "dependencies": {
18
- "@netlify/functions": "^1.1.0"
18
+ "@netlify/functions": "^1.2.0"
19
19
  }
20
20
  }
@@ -9,15 +9,15 @@
9
9
  "version": "1.0.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
- "@netlify/functions": "^1.1.0",
12
+ "@netlify/functions": "^1.2.0",
13
13
  "@types/node": "^14.0.0",
14
14
  "typescript": "^4.0.0"
15
15
  }
16
16
  },
17
17
  "node_modules/@netlify/functions": {
18
- "version": "1.1.0",
19
- "resolved": "https://registry.npmjs.org/@netlify/functions/-/functions-1.1.0.tgz",
20
- "integrity": "sha512-XUFC5nt4iLMrDK+6WjYrDOW9h6XGIQlEk3o++xglFbDKc6dsP+k6rjfz3vl0w8S9Oiosxj3uLaPW18szJc1UgA==",
18
+ "version": "1.2.0",
19
+ "resolved": "https://registry.npmjs.org/@netlify/functions/-/functions-1.2.0.tgz",
20
+ "integrity": "sha512-zCOJPoZQLv4ISHjyBS7asqzR6Y9NU+Vb0VKYDD0xUwYmReMhLTDchjGMkt5x0Jk1EVnJwUvA29rGyQEj3tIgAA==",
21
21
  "dependencies": {
22
22
  "is-promise": "^4.0.0"
23
23
  },
@@ -50,9 +50,9 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "@netlify/functions": {
53
- "version": "1.1.0",
54
- "resolved": "https://registry.npmjs.org/@netlify/functions/-/functions-1.1.0.tgz",
55
- "integrity": "sha512-XUFC5nt4iLMrDK+6WjYrDOW9h6XGIQlEk3o++xglFbDKc6dsP+k6rjfz3vl0w8S9Oiosxj3uLaPW18szJc1UgA==",
53
+ "version": "1.2.0",
54
+ "resolved": "https://registry.npmjs.org/@netlify/functions/-/functions-1.2.0.tgz",
55
+ "integrity": "sha512-zCOJPoZQLv4ISHjyBS7asqzR6Y9NU+Vb0VKYDD0xUwYmReMhLTDchjGMkt5x0Jk1EVnJwUvA29rGyQEj3tIgAA==",
56
56
  "requires": {
57
57
  "is-promise": "^4.0.0"
58
58
  }
@@ -14,7 +14,7 @@
14
14
  "author": "Netlify",
15
15
  "license": "MIT",
16
16
  "dependencies": {
17
- "@netlify/functions": "^1.1.0",
17
+ "@netlify/functions": "^1.2.0",
18
18
  "@types/node": "^14.0.0",
19
19
  "typescript": "^4.0.0"
20
20
  }
@@ -15,7 +15,7 @@
15
15
  "author": "Netlify",
16
16
  "license": "MIT",
17
17
  "dependencies": {
18
- "@netlify/functions": "^1.1.0",
18
+ "@netlify/functions": "^1.2.0",
19
19
  "@types/node": "^14.18.9",
20
20
  "typescript": "^4.5.5"
21
21
  }
package/src/lib/build.js CHANGED
@@ -1,4 +1,6 @@
1
1
  // @ts-check
2
+ const process = require('process')
3
+
2
4
  const netlifyBuildPromise = import('@netlify/build')
3
5
 
4
6
  /**
@@ -20,6 +22,7 @@ const netlifyBuildPromise = import('@netlify/build')
20
22
  */
21
23
  const getBuildOptions = ({ cachedConfig, options: { context, cwd, debug, dry, json, offline, silent }, token }) => ({
22
24
  cachedConfig,
25
+ siteId: cachedConfig.siteInfo.id,
23
26
  token,
24
27
  dry,
25
28
  debug,
@@ -42,6 +45,18 @@ const getBuildOptions = ({ cachedConfig, options: { context, cwd, debug, dry, js
42
45
  */
43
46
  const runBuild = async (options) => {
44
47
  const { default: build } = await netlifyBuildPromise
48
+
49
+ // If netlify NETLIFY_API_URL is set we need to pass this information to @netlify/build
50
+ // TODO don't use testOpts, but add real properties to do this.
51
+ if (process.env.NETLIFY_API_URL) {
52
+ const apiUrl = new URL(process.env.NETLIFY_API_URL)
53
+ const testOpts = {
54
+ scheme: apiUrl.protocol.slice(0, -1),
55
+ host: apiUrl.host,
56
+ }
57
+ options = { ...options, testOpts }
58
+ }
59
+
45
60
  const { configMutations, netlifyConfig: newConfig, severityCode: exitCode } = await build(options)
46
61
  return { exitCode, newConfig, configMutations }
47
62
  }
@@ -24,6 +24,7 @@ class EdgeFunctionsRegistry {
24
24
  * @param {object} opts.config
25
25
  * @param {string} opts.configPath
26
26
  * @param {string[]} opts.directories
27
+ * @param {Record<string, string>} opts.env
27
28
  * @param {() => Promise<object>} opts.getUpdatedConfig
28
29
  * @param {EdgeFunction[]} opts.internalFunctions
29
30
  * @param {string} opts.projectDir
@@ -197,6 +198,7 @@ class EdgeFunctionsRegistry {
197
198
  })
198
199
 
199
200
  env.DENO_REGION = 'local'
201
+ env.NETLIFY_DEV = 'true'
200
202
  // We use it in the bootstrap layer to detect whether we're running in production or not
201
203
  // (see https://github.com/netlify/edge-functions-bootstrap/blob/main/src/bootstrap/environment.ts#L2)
202
204
  // env.DENO_DEPLOYMENT_ID = 'xxx='
@@ -828,12 +828,32 @@ const detectLocalCLISessionMetadata = async ({ siteRoot }) => {
828
828
  * @param {string} input.jwt The GraphJWT string
829
829
  * @param {string} input.sessionId The id of the cli session to fetch the current metadata for
830
830
  * @param {string} input.siteRoot Path to the root of the project
831
+ * @param {object} input.config The parsed netlify.toml config file
831
832
  * @param {string} input.docId The GraphQL operations document id to fetch
832
833
  * @param {string} input.schemaId The GraphQL schemaId to use when generating code
833
834
  */
834
- const publishCliSessionMetadataPublishEvent = async ({ docId, jwt, schemaId, sessionId, siteRoot }) => {
835
+ const publishCliSessionMetadataPublishEvent = async ({ config, docId, jwt, schemaId, sessionId, siteRoot }) => {
835
836
  const detectedMetadata = await detectLocalCLISessionMetadata({ siteRoot })
836
837
 
838
+ /** @type {CodegenHelpers.CodegenModuleMeta | null} */
839
+ let codegen = null
840
+
841
+ const codegenModule = await getCodegenModule({ config })
842
+
843
+ if (codegenModule) {
844
+ codegen = {
845
+ id: codegenModule.id,
846
+ version: codegenModule.id,
847
+ generators: codegenModule.generators.map((generator) => ({
848
+ id: generator.id,
849
+ name: generator.name,
850
+ options: generator.generateHandlerOptions || null,
851
+ supportedDefinitionTypes: generator.supportedDefinitionTypes,
852
+ version: generator.version,
853
+ })),
854
+ }
855
+ }
856
+
837
857
  /** @type {CliEventHelper.OneGraphNetlifyCliSessionMetadataPublishEvent} */
838
858
  const event = {
839
859
  __typename: 'OneGraphNetlifyCliSessionMetadataPublishEvent',
@@ -848,7 +868,7 @@ const publishCliSessionMetadataPublishEvent = async ({ docId, jwt, schemaId, ses
848
868
  siteRootFriendly: detectedMetadata.siteRoot,
849
869
  persistedDocId: docId,
850
870
  schemaId,
851
- codegenModule: detectedMetadata.codegen,
871
+ codegenModule: codegen,
852
872
  arch: detectedMetadata.arch,
853
873
  nodeVersion: detectedMetadata.nodeVersion,
854
874
  platform: detectedMetadata.platform,
@@ -171,13 +171,14 @@ const translateFromMongoToEnvelope = (env = {}) => {
171
171
  /**
172
172
  * Translates an Envelope env into a Mongo env
173
173
  * @param {Array<object>} envVars - The array of Envelope env vars
174
+ * @param {enum<dev,branch-deploy,deploy-preview,production>} context - The deploy context of the environment variable
174
175
  * @returns {object} The env object as compatible with Mongo
175
176
  */
176
- const translateFromEnvelopeToMongo = (envVars = []) =>
177
+ const translateFromEnvelopeToMongo = (envVars = [], context = 'dev') =>
177
178
  envVars
178
179
  .sort((left, right) => (left.key.toLowerCase() < right.key.toLowerCase() ? -1 : 1))
179
180
  .reduce((acc, cur) => {
180
- const envVar = cur.values.find((val) => ['dev', 'all'].includes(val.context))
181
+ const envVar = cur.values.find((val) => [context, 'all'].includes(val.context))
181
182
  if (envVar && envVar.value) {
182
183
  return {
183
184
  ...acc,