netlify-cli 15.5.1 → 15.7.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
3
  "description": "Netlify command line tool",
4
- "version": "15.5.1",
4
+ "version": "15.7.0",
5
5
  "author": "Netlify Inc.",
6
6
  "type": "module",
7
7
  "engines": {
@@ -44,14 +44,15 @@
44
44
  "dependencies": {
45
45
  "@bugsnag/js": "7.20.2",
46
46
  "@fastify/static": "6.10.2",
47
- "@netlify/build": "29.12.4",
48
- "@netlify/build-info": "7.0.6",
49
- "@netlify/config": "20.4.6",
47
+ "@netlify/build": "29.12.8",
48
+ "@netlify/build-info": "7.0.8",
49
+ "@netlify/config": "20.5.1",
50
50
  "@netlify/edge-bundler": "8.16.2",
51
51
  "@netlify/framework-info": "9.8.10",
52
52
  "@netlify/local-functions-proxy": "1.1.1",
53
- "@netlify/zip-it-and-ship-it": "9.9.1",
54
- "@octokit/rest": "19.0.11",
53
+ "@netlify/serverless-functions-api": "1.5.1",
54
+ "@netlify/zip-it-and-ship-it": "9.10.0",
55
+ "@octokit/rest": "19.0.13",
55
56
  "@skn0tt/lambda-local": "2.0.3",
56
57
  "ansi-escapes": "6.2.0",
57
58
  "ansi-styles": "6.2.1",
@@ -73,7 +74,7 @@
73
74
  "copy-template-dir": "1.4.0",
74
75
  "cron-parser": "4.8.1",
75
76
  "debug": "4.3.4",
76
- "decache": "4.6.1",
77
+ "decache": "4.6.2",
77
78
  "dot-prop": "7.2.0",
78
79
  "dotenv": "16.0.3",
79
80
  "env-paths": "3.0.0",
@@ -132,7 +133,7 @@
132
133
  "pump": "3.0.0",
133
134
  "raw-body": "2.5.2",
134
135
  "read-pkg-up": "9.1.0",
135
- "semver": "7.5.1",
136
+ "semver": "7.5.3",
136
137
  "source-map-support": "0.5.21",
137
138
  "strip-ansi-control-characters": "2.0.0",
138
139
  "tabtab": "3.0.2",
@@ -527,6 +527,7 @@ export default class BaseCommand extends Command {
527
527
  pathPrefix,
528
528
  scheme,
529
529
  offline,
530
+ siteFeatureFlagPrefix: 'cli',
530
531
  })
531
532
  } catch (error_) {
532
533
  const isUserError = error_.customErrorInfo !== undefined && error_.customErrorInfo.type === 'resolveConfig'
@@ -172,6 +172,10 @@ const dev = async (options, command) => {
172
172
  siteUrl,
173
173
  capabilities,
174
174
  timeouts,
175
+ geolocationMode: options.geo,
176
+ geoCountry: options.country,
177
+ offline: options.offline,
178
+ state,
175
179
  })
176
180
 
177
181
  // Try to add `.netlify` to `.gitignore`.
@@ -198,8 +202,6 @@ const dev = async (options, command) => {
198
202
  configPath: configPathOverride,
199
203
  debug: options.debug,
200
204
  env,
201
- geolocationMode: options.geo,
202
- geoCountry: options.country,
203
205
  getUpdatedConfig,
204
206
  inspectSettings,
205
207
  offline: options.offline,
@@ -207,6 +209,8 @@ const dev = async (options, command) => {
207
209
  site,
208
210
  siteInfo,
209
211
  state,
212
+ geolocationMode: options.geo,
213
+ geoCountry: options.country,
210
214
  })
211
215
 
212
216
  if (devConfig.autoLaunch !== false) {
@@ -100,7 +100,6 @@ const formatRegistryArrayForInquirer = async function (lang, funcType) {
100
100
  .filter((folderName) => !folderName.endsWith('.md'))
101
101
  .map(async (folderName) => {
102
102
  const templatePath = path.join(templatesDir, lang, folderName, '.netlify-function-template.mjs')
103
- // eslint-disable-next-line import/no-dynamic-require
104
103
  const template = await import(pathToFileURL(templatePath))
105
104
 
106
105
  return template.default
@@ -362,7 +361,6 @@ const downloadFromURL = async function (command, options, argumentName, function
362
361
  if (await fileExistsAsync(fnTemplateFile)) {
363
362
  const {
364
363
  default: { addons = [], onComplete },
365
- // eslint-disable-next-line import/no-dynamic-require
366
364
  } = await import(pathToFileURL(fnTemplateFile).href)
367
365
 
368
366
  await installAddons(command, addons, path.resolve(fnFolder))
@@ -387,7 +385,6 @@ const getNpmInstallPackages = (existingPackages = {}, neededPackages = {}) =>
387
385
  // we don't do this check, we may be upgrading the version of a module used in
388
386
  // another part of the project, which we don't want to do.
389
387
  const installDeps = async ({ functionPackageJson, functionPath, functionsDir }) => {
390
- // eslint-disable-next-line import/no-dynamic-require
391
388
  const { dependencies: functionDependencies, devDependencies: functionDevDependencies } = require(functionPackageJson)
392
389
  const sitePackageJson = await findUp('package.json', { cwd: functionsDir })
393
390
  const npmInstallFlags = ['--no-audit', '--no-fund']
@@ -401,7 +398,6 @@ const installDeps = async ({ functionPackageJson, functionPath, functionsDir })
401
398
  return
402
399
  }
403
400
 
404
- // eslint-disable-next-line import/no-dynamic-require
405
401
  const { dependencies: siteDependencies, devDependencies: siteDevDependencies } = require(sitePackageJson)
406
402
  const dependencies = getNpmInstallPackages(siteDependencies, functionDependencies)
407
403
  const devDependencies = getNpmInstallPackages(siteDevDependencies, functionDevDependencies)
@@ -68,7 +68,7 @@ const processPayloadFromFlag = function (payloadString) {
68
68
  if (pathexists) {
69
69
  try {
70
70
  // there is code execution potential here
71
- // eslint-disable-next-line import/no-dynamic-require
71
+
72
72
  payload = require(payloadpath)
73
73
  return payload
74
74
  } catch (error_) {
@@ -13,7 +13,7 @@ const DEFAULT_PORT = 9999
13
13
  * @param {import('../base-command.mjs').default} command
14
14
  */
15
15
  const functionsServe = async (options, command) => {
16
- const { api, config, site, siteInfo } = command.netlify
16
+ const { api, config, site, siteInfo, state } = command.netlify
17
17
 
18
18
  const functionsDir = getFunctionsDir({ options, config }, join('netlify', 'functions'))
19
19
  let { env } = command.netlify.cachedConfig
@@ -48,6 +48,10 @@ const functionsServe = async (options, command) => {
48
48
  timeouts,
49
49
  functionsPrefix: '/.netlify/functions/',
50
50
  buildersPrefix: '/.netlify/builders/',
51
+ geolocationMode: options.geo,
52
+ geoCountry: options.country,
53
+ offline: options.offline,
54
+ state,
51
55
  })
52
56
  }
53
57
 
@@ -8,7 +8,7 @@ export const getRecipe = async (name) => {
8
8
  const recipePath = resolve(directoryPath, '../../recipes', name, 'index.mjs')
9
9
 
10
10
  // windows needs a URL for absolute paths
11
- // eslint-disable-next-line import/no-dynamic-require
11
+
12
12
  const recipe = await import(pathToFileURL(recipePath).href)
13
13
 
14
14
  return recipe
@@ -22,7 +22,7 @@ export const listRecipes = async () => {
22
22
  const recipePath = join(recipesPath, name, 'index.mjs')
23
23
 
24
24
  // windows needs a URL for absolute paths
25
- // eslint-disable-next-line import/no-dynamic-require
25
+
26
26
  const recipe = await import(pathToFileURL(recipePath).href)
27
27
 
28
28
  return {
@@ -101,6 +101,10 @@ const serve = async (options, command) => {
101
101
  siteUrl,
102
102
  capabilities,
103
103
  timeouts,
104
+ geolocationMode: options.geo,
105
+ geoCountry: options.country,
106
+ offline: options.offline,
107
+ state,
104
108
  })
105
109
 
106
110
  // Try to add `.netlify` to `.gitignore`.
@@ -22,11 +22,25 @@ const addFunctionsConfigDefaults = (config) => ({
22
22
  },
23
23
  })
24
24
 
25
- const buildFunction = async ({ cache, config, directory, func, hasTypeModule, projectRoot, targetDirectory }) => {
25
+ /**
26
+ * @param {object} params
27
+ * @param {import("@netlify/zip-it-and-ship-it/dist/feature_flags.js").FeatureFlags} params.featureFlags
28
+ */
29
+ const buildFunction = async ({
30
+ cache,
31
+ config,
32
+ directory,
33
+ featureFlags,
34
+ func,
35
+ hasTypeModule,
36
+ projectRoot,
37
+ targetDirectory,
38
+ }) => {
26
39
  const zipOptions = {
27
40
  archiveFormat: 'none',
28
41
  basePath: projectRoot,
29
42
  config,
43
+ featureFlags: { ...featureFlags, zisi_functions_api_v2: true },
30
44
  }
31
45
  const functionDirectory = path.dirname(func.mainFile)
32
46
 
@@ -42,9 +56,11 @@ const buildFunction = async ({ cache, config, directory, func, hasTypeModule, pr
42
56
  // this case, we use `mainFile` as the function path of `zipFunction`.
43
57
  const entryPath = functionDirectory === directory ? func.mainFile : functionDirectory
44
58
  const {
59
+ entryFilename,
45
60
  includedFiles,
46
61
  inputs,
47
62
  path: functionPath,
63
+ runtimeAPIVersion,
48
64
  schedule,
49
65
  } = await memoizedBuild({
50
66
  cache,
@@ -52,7 +68,7 @@ const buildFunction = async ({ cache, config, directory, func, hasTypeModule, pr
52
68
  command: () => zipFunction(entryPath, targetDirectory, zipOptions),
53
69
  })
54
70
  const srcFiles = inputs.filter((inputPath) => !inputPath.includes(`${path.sep}node_modules${path.sep}`))
55
- const buildPath = path.join(functionPath, `${func.name}.js`)
71
+ const buildPath = path.join(functionPath, entryFilename)
56
72
 
57
73
  // some projects include a package.json with "type=module", forcing Node to interpret every descending file
58
74
  // as ESM. ZISI outputs CJS, so we emit an overriding directive into the output directory.
@@ -67,7 +83,7 @@ const buildFunction = async ({ cache, config, directory, func, hasTypeModule, pr
67
83
 
68
84
  clearFunctionsCache(targetDirectory)
69
85
 
70
- return { buildPath, includedFiles, srcFiles, schedule }
86
+ return { buildPath, includedFiles, runtimeAPIVersion, srcFiles, schedule }
71
87
  }
72
88
 
73
89
  /**
@@ -76,14 +92,14 @@ const buildFunction = async ({ cache, config, directory, func, hasTypeModule, pr
76
92
  * @param {string} params.mainFile
77
93
  * @param {string} params.projectRoot
78
94
  */
79
- export const parseForSchedule = async ({ config, mainFile, projectRoot }) => {
95
+ export const parseFunctionForMetadata = async ({ config, mainFile, projectRoot }) => {
80
96
  const { listFunction } = await import('@netlify/zip-it-and-ship-it')
81
- const listedFunction = await listFunction(mainFile, {
97
+
98
+ return await listFunction(mainFile, {
82
99
  config: netlifyConfigToZisiConfig({ config, projectRoot }),
100
+ featureFlags: { zisi_functions_api_v2: true },
83
101
  parseISC: true,
84
102
  })
85
-
86
- return listedFunction && listedFunction.schedule
87
103
  }
88
104
 
89
105
  // Clears the cache for any files inside the directory from which functions are
@@ -109,26 +125,44 @@ const getTargetDirectory = async ({ errorExit }) => {
109
125
  const netlifyConfigToZisiConfig = ({ config, projectRoot }) =>
110
126
  addFunctionsConfigDefaults(normalizeFunctionsConfig({ functionsConfig: config.functions, projectRoot }))
111
127
 
112
- export default async function handler({ config, directory, errorExit, func, projectRoot }) {
128
+ export default async function handler({ config, directory, errorExit, func, metadata, projectRoot }) {
113
129
  const functionsConfig = netlifyConfigToZisiConfig({ config, projectRoot })
114
130
 
115
131
  const packageJson = await readPackageUp(func.mainFile)
116
132
  const hasTypeModule = packageJson && packageJson.packageJson.type === 'module'
117
133
 
118
- // We must use esbuild for certain file extensions.
119
- const mustTranspile = ['.mjs', '.ts'].includes(path.extname(func.mainFile))
120
- const mustUseEsbuild = hasTypeModule || mustTranspile
121
-
122
- if (mustUseEsbuild && !functionsConfig['*'].nodeBundler) {
123
- functionsConfig['*'].nodeBundler = 'esbuild'
124
- }
125
-
126
- // TODO: Resolve functions config globs so that we can check for the bundler
127
- // on a per-function basis.
128
- const isUsingEsbuild = ['esbuild_zisi', 'esbuild'].includes(functionsConfig['*'].nodeBundler)
129
-
130
- if (!isUsingEsbuild) {
131
- return false
134
+ /** @type {import("@netlify/zip-it-and-ship-it/dist/feature_flags.js").FeatureFlags} */
135
+ const featureFlags = {}
136
+
137
+ if (metadata.runtimeAPIVersion === 2) {
138
+ // For TypeScript we use NFT, otherwise we leave the file untouched with the `none` bundler
139
+ const isTypescript = ['.ts', '.mts', '.cts'].includes(path.extname(func.mainFile))
140
+
141
+ if (isTypescript) {
142
+ functionsConfig['*'].nodeBundler = 'nft'
143
+ } else {
144
+ // using esbuild is less performant than `none`, but it emits sourcemaps and thus
145
+ // enables debugging functions
146
+ functionsConfig['*'].nodeBundler = 'esbuild'
147
+ featureFlags.zisi_pure_esm = true
148
+ featureFlags.zisi_pure_esm_mjs = true
149
+ }
150
+ } else {
151
+ // We must use esbuild for certain file extensions.
152
+ const mustTranspile = ['.mjs', '.ts', '.mts', '.cts'].includes(path.extname(func.mainFile))
153
+ const mustUseEsbuild = hasTypeModule || mustTranspile
154
+
155
+ if (mustUseEsbuild && !functionsConfig['*'].nodeBundler) {
156
+ functionsConfig['*'].nodeBundler = 'esbuild'
157
+ }
158
+
159
+ // TODO: Resolve functions config globs so that we can check for the bundler
160
+ // on a per-function basis.
161
+ const isUsingEsbuild = ['esbuild_zisi', 'esbuild'].includes(functionsConfig['*'].nodeBundler)
162
+
163
+ if (!isUsingEsbuild) {
164
+ return false
165
+ }
132
166
  }
133
167
 
134
168
  // Enable source map support.
@@ -138,7 +172,16 @@ export default async function handler({ config, directory, errorExit, func, proj
138
172
 
139
173
  return {
140
174
  build: ({ cache = {} }) =>
141
- buildFunction({ cache, config: functionsConfig, directory, func, projectRoot, targetDirectory, hasTypeModule }),
175
+ buildFunction({
176
+ cache,
177
+ config: functionsConfig,
178
+ directory,
179
+ func,
180
+ projectRoot,
181
+ targetDirectory,
182
+ hasTypeModule,
183
+ featureFlags,
184
+ }),
142
185
  builderName: 'zip-it-and-ship-it',
143
186
  target: targetDirectory,
144
187
  }
@@ -0,0 +1 @@
1
+ export const SECONDS_TO_MILLISECONDS = 1000
@@ -1,15 +1,17 @@
1
+ import { createConnection } from 'net'
1
2
  import { dirname } from 'path'
3
+ import { pathToFileURL } from 'url'
4
+ import { Worker } from 'worker_threads'
2
5
 
3
6
  import lambdaLocal from '@skn0tt/lambda-local'
4
7
  import winston from 'winston'
5
8
 
6
9
  import detectNetlifyLambdaBuilder from './builders/netlify-lambda.mjs'
7
- import detectZisiBuilder, { parseForSchedule } from './builders/zisi.mjs'
10
+ import detectZisiBuilder, { parseFunctionForMetadata } from './builders/zisi.mjs'
11
+ import { SECONDS_TO_MILLISECONDS } from './constants.mjs'
8
12
 
9
13
  export const name = 'js'
10
14
 
11
- const SECONDS_TO_MILLISECONDS = 1000
12
-
13
15
  let netlifyLambdaDetectorCache
14
16
 
15
17
  const logger = winston.createLogger({
@@ -37,7 +39,8 @@ export const getBuildFunction = async ({ config, directory, errorExit, func, pro
37
39
  return netlifyLambdaBuilder.build
38
40
  }
39
41
 
40
- const zisiBuilder = await detectZisiBuilder({ config, directory, errorExit, func, projectRoot })
42
+ const metadata = await parseFunctionForMetadata({ mainFile: func.mainFile, config, projectRoot })
43
+ const zisiBuilder = await detectZisiBuilder({ config, directory, errorExit, func, metadata, projectRoot })
41
44
 
42
45
  if (zisiBuilder) {
43
46
  return zisiBuilder.build
@@ -48,15 +51,55 @@ export const getBuildFunction = async ({ config, directory, errorExit, func, pro
48
51
  // main file otherwise.
49
52
  const functionDirectory = dirname(func.mainFile)
50
53
  const srcFiles = functionDirectory === directory ? [func.mainFile] : [functionDirectory]
51
- const schedule = await parseForSchedule({ mainFile: func.mainFile, config, projectRoot })
52
54
 
53
- return () => ({ schedule, srcFiles })
55
+ return () => ({ schedule: metadata.schedule, srcFiles })
54
56
  }
55
57
 
58
+ const workerURL = new URL('worker.mjs', import.meta.url)
59
+
56
60
  export const invokeFunction = async ({ context, event, func, timeout }) => {
61
+ if (func.buildData.runtimeAPIVersion !== 2) {
62
+ return await invokeFunctionDirectly({ context, event, func, timeout })
63
+ }
64
+
65
+ const workerData = {
66
+ clientContext: JSON.stringify(context),
67
+ event,
68
+ // If a function builder has defined a `buildPath` property, we use it.
69
+ // Otherwise, we'll invoke the function's main file.
70
+ // Because we use import() we have to use file:// URLs for Windows.
71
+ entryFilePath: pathToFileURL(func.buildData?.buildPath ?? func.mainFile).href,
72
+ timeoutMs: timeout * SECONDS_TO_MILLISECONDS,
73
+ }
74
+
75
+ const worker = new Worker(workerURL, { workerData })
76
+ return await new Promise((resolve, reject) => {
77
+ worker.on('message', (result) => {
78
+ if (result?.streamPort) {
79
+ const client = createConnection(
80
+ {
81
+ port: result.streamPort,
82
+ host: 'localhost',
83
+ },
84
+ () => {
85
+ result.body = client
86
+ resolve(result)
87
+ },
88
+ )
89
+ client.on('error', reject)
90
+ } else {
91
+ resolve(result)
92
+ }
93
+ })
94
+
95
+ worker.on('error', reject)
96
+ })
97
+ }
98
+
99
+ export const invokeFunctionDirectly = async ({ context, event, func, timeout }) => {
57
100
  // If a function builder has defined a `buildPath` property, we use it.
58
101
  // Otherwise, we'll invoke the function's main file.
59
- const lambdaPath = (func.buildData && func.buildData.buildPath) || func.mainFile
102
+ const lambdaPath = func.buildData?.buildPath ?? func.mainFile
60
103
  const result = await lambdaLocal.execute({
61
104
  clientContext: JSON.stringify(context),
62
105
  event,
@@ -0,0 +1,49 @@
1
+ import { createServer } from 'net'
2
+ import { isMainThread, workerData, parentPort } from 'worker_threads'
3
+
4
+ import lambdaLocal from '@skn0tt/lambda-local'
5
+ import { isStream } from 'is-stream'
6
+ import sourceMapSupport from 'source-map-support'
7
+
8
+ if (isMainThread) {
9
+ throw new Error(`Do not import "${import.meta.url}" in the main thread.`)
10
+ }
11
+
12
+ sourceMapSupport.install()
13
+
14
+ lambdaLocal.getLogger().level = 'warn'
15
+
16
+ const { clientContext, entryFilePath, event, timeoutMs } = workerData
17
+
18
+ const lambdaFunc = await import(entryFilePath)
19
+
20
+ const result = await lambdaLocal.execute({
21
+ clientContext,
22
+ event,
23
+ lambdaFunc,
24
+ region: 'dev',
25
+ timeoutMs,
26
+ verboseLevel: 3,
27
+ })
28
+
29
+ // When the result body is a StreamResponse
30
+ // we open up a http server that proxies back to the main thread.
31
+ if (result && isStream(result.body)) {
32
+ const { body } = result
33
+ delete result.body
34
+ await new Promise((resolve, reject) => {
35
+ const server = createServer((socket) => {
36
+ body.pipe(socket).on('end', () => server.close())
37
+ })
38
+ server.on('error', (error) => {
39
+ reject(error)
40
+ })
41
+ server.listen({ port: 0, host: 'localhost' }, () => {
42
+ const { port } = server.address()
43
+ result.streamPort = port
44
+ resolve()
45
+ })
46
+ })
47
+ }
48
+
49
+ parentPort.postMessage(result)
@@ -3,6 +3,8 @@ import jwtDecode from 'jwt-decode'
3
3
 
4
4
  import { NETLIFYDEVERR, NETLIFYDEVLOG, error as errorExit, log } from '../../utils/command-helpers.mjs'
5
5
  import { CLOCKWORK_USERAGENT, getFunctionsDistPath, getInternalFunctionsDir } from '../../utils/functions/index.mjs'
6
+ import { headers as efHeaders } from '../edge-functions/headers.mjs'
7
+ import { getGeoLocation } from '../geo-location.mjs'
6
8
 
7
9
  import { handleBackgroundFunction, handleBackgroundFunctionResult } from './background.mjs'
8
10
  import { createFormSubmissionHandler } from './form-submissions-handler.mjs'
@@ -102,10 +104,15 @@ export const createHandler = function (options) {
102
104
  (prev, [key, value]) => ({ ...prev, [key]: Array.isArray(value) ? value : [value] }),
103
105
  {},
104
106
  )
105
- const headers = Object.entries({ ...request.headers, 'client-ip': [remoteAddress] }).reduce(
106
- (prev, [key, value]) => ({ ...prev, [key]: Array.isArray(value) ? value : [value] }),
107
- {},
108
- )
107
+
108
+ const geoLocation = await getGeoLocation({ ...options, mode: options.geo })
109
+
110
+ const headers = Object.entries({
111
+ ...request.headers,
112
+ 'client-ip': [remoteAddress],
113
+ 'x-nf-client-connection-ip': [remoteAddress],
114
+ [efHeaders.Geo]: JSON.stringify(geoLocation),
115
+ }).reduce((prev, [key, value]) => ({ ...prev, [key]: Array.isArray(value) ? value : [value] }), {})
109
116
  const rawQuery = new URLSearchParams(requestQuery).toString()
110
117
  const protocol = options.config?.dev?.https ? 'https' : 'http'
111
118
  const url = new URL(requestPath, `${protocol}://${request.get('host') || 'localhost'}`)
@@ -73,7 +73,7 @@ const formatLambdaLocalError = (err, acceptsHtml) =>
73
73
  errorMessage: err.errorMessage,
74
74
  trace: err.stackTrace,
75
75
  })
76
- : `${err.errorType}: ${err.errorMessage}\n ${err.stackTrace.join('\n ')}`
76
+ : `${err.errorType}: ${err.errorMessage}\n ${err.stackTrace?.join('\n ')}`
77
77
 
78
78
  const processRenderedResponse = async (err, request) => {
79
79
  const acceptsHtml = request.headers && request.headers.accept && request.headers.accept.includes('text/html')
@@ -102,7 +102,7 @@ const validateLambdaResponse = (lambdaResponse) => {
102
102
  }
103
103
  if (!Number(lambdaResponse.statusCode)) {
104
104
  return {
105
- error: `Your function response must have a numerical statusCode. You gave: $ ${lambdaResponse.statusCode}`,
105
+ error: `Your function response must have a numerical statusCode. You gave: ${lambdaResponse.statusCode}`,
106
106
  }
107
107
  }
108
108
  if (lambdaResponse.body && typeof lambdaResponse.body !== 'string' && !isStream(lambdaResponse.body)) {
@@ -10,7 +10,6 @@ import { env } from 'process'
10
10
  let execa
11
11
 
12
12
  if (env.NETLIFY_CLI_EXECA_PATH) {
13
- // eslint-disable-next-line import/no-dynamic-require
14
13
  const execaMock = await import(env.NETLIFY_CLI_EXECA_PATH)
15
14
  execa = execaMock.default
16
15
  } else {
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Allows us to check if a feature flag is enabled for a site.
3
+ * Due to versioning of the cli, and the desire to remove flags from
4
+ * our feature flag service when they should always evaluate to true,
5
+ * we can't just look for the presense of {featureFlagName: true}, as
6
+ * the absense of a flag should also evaluate to the flag being enabled.
7
+ * Instead, we return that the feature flag is enabled if it isn't
8
+ * specifically set to false in the response
9
+ * @param {*} siteInfo
10
+ * @param {string} flagName
11
+ *
12
+ * @returns {boolean}
13
+ */
14
+ export const isFeatureFlagEnabled = (flagName, siteInfo) => {
15
+ if (siteInfo.feature_flags && siteInfo.feature_flags[flagName] !== false) {
16
+ return true
17
+ }
18
+ return false
19
+ }