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/npm-shrinkwrap.json +168 -169
- package/package.json +9 -8
- package/src/commands/base-command.mjs +1 -0
- package/src/commands/dev/dev.mjs +6 -2
- package/src/commands/functions/functions-create.mjs +0 -4
- package/src/commands/functions/functions-invoke.mjs +1 -1
- package/src/commands/functions/functions-serve.mjs +5 -1
- package/src/commands/recipes/common.mjs +2 -2
- package/src/commands/serve/serve.mjs +4 -0
- package/src/lib/functions/runtimes/js/builders/zisi.mjs +66 -23
- package/src/lib/functions/runtimes/js/constants.mjs +1 -0
- package/src/lib/functions/runtimes/js/index.mjs +50 -7
- package/src/lib/functions/runtimes/js/worker.mjs +49 -0
- package/src/lib/functions/server.mjs +11 -4
- package/src/lib/functions/synchronous.mjs +2 -2
- package/src/utils/execa.mjs +0 -1
- package/src/utils/feature-flags.mjs +19 -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": "15.
|
|
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.
|
|
48
|
-
"@netlify/build-info": "7.0.
|
|
49
|
-
"@netlify/config": "20.
|
|
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/
|
|
54
|
-
"@
|
|
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.
|
|
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.
|
|
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'
|
package/src/commands/dev/dev.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
95
|
+
export const parseFunctionForMetadata = async ({ config, mainFile, projectRoot }) => {
|
|
80
96
|
const { listFunction } = await import('@netlify/zip-it-and-ship-it')
|
|
81
|
-
|
|
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
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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({
|
|
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, {
|
|
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
|
|
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 =
|
|
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
|
-
|
|
106
|
-
|
|
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
|
|
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: $
|
|
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)) {
|
package/src/utils/execa.mjs
CHANGED
|
@@ -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
|
+
}
|