netlify-cli 16.5.1 → 16.6.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 +12761 -11221
- package/package.json +5 -5
- package/src/lib/functions/form-submissions-handler.mjs +6 -1
- package/src/lib/functions/netlify-function.mjs +52 -2
- package/src/lib/functions/registry.mjs +308 -37
- package/src/lib/functions/runtimes/js/builders/zisi.mjs +3 -1
- package/src/lib/functions/runtimes/js/index.mjs +1 -7
- package/src/lib/functions/server.mjs +20 -2
- package/src/lib/functions/synchronous.mjs +72 -16
- package/src/utils/command-helpers.mjs +7 -1
- package/src/utils/functions/functions.mjs +6 -0
- package/src/utils/proxy.mjs +16 -16
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "netlify-cli",
|
|
3
3
|
"description": "Netlify command line tool",
|
|
4
|
-
"version": "16.
|
|
4
|
+
"version": "16.6.0",
|
|
5
5
|
"author": "Netlify Inc.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -44,13 +44,13 @@
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@bugsnag/js": "7.20.2",
|
|
46
46
|
"@fastify/static": "6.10.2",
|
|
47
|
-
"@netlify/build": "29.
|
|
48
|
-
"@netlify/build-info": "7.
|
|
47
|
+
"@netlify/build": "29.22.1",
|
|
48
|
+
"@netlify/build-info": "7.10.0",
|
|
49
49
|
"@netlify/config": "20.9.0",
|
|
50
50
|
"@netlify/edge-bundler": "9.1.0",
|
|
51
51
|
"@netlify/local-functions-proxy": "1.1.1",
|
|
52
|
-
"@netlify/serverless-functions-api": "1.
|
|
53
|
-
"@netlify/zip-it-and-ship-it": "9.
|
|
52
|
+
"@netlify/serverless-functions-api": "1.8.0",
|
|
53
|
+
"@netlify/zip-it-and-ship-it": "9.23.0",
|
|
54
54
|
"@octokit/rest": "19.0.13",
|
|
55
55
|
"ansi-escapes": "6.2.0",
|
|
56
56
|
"ansi-styles": "6.2.1",
|
|
@@ -29,7 +29,12 @@ const getFormHandler = function ({ functionsRegistry }) {
|
|
|
29
29
|
|
|
30
30
|
export const createFormSubmissionHandler = function ({ functionsRegistry, siteUrl }) {
|
|
31
31
|
return async function formSubmissionHandler(req, res, next) {
|
|
32
|
-
if (
|
|
32
|
+
if (
|
|
33
|
+
req.url.startsWith('/.netlify/') ||
|
|
34
|
+
req.method !== 'POST' ||
|
|
35
|
+
(await functionsRegistry.getFunctionForURLPath(req.url, req.method))
|
|
36
|
+
)
|
|
37
|
+
return next()
|
|
33
38
|
|
|
34
39
|
const fakeRequest = new Readable({
|
|
35
40
|
read() {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
+
import { basename, extname } from 'path'
|
|
2
3
|
import { version as nodeVersion } from 'process'
|
|
3
4
|
|
|
4
5
|
import CronParser from 'cron-parser'
|
|
@@ -7,6 +8,7 @@ import semver from 'semver'
|
|
|
7
8
|
import { error as errorExit } from '../../utils/command-helpers.mjs'
|
|
8
9
|
import { BACKGROUND } from '../../utils/functions/get-functions.mjs'
|
|
9
10
|
|
|
11
|
+
const TYPESCRIPT_EXTENSIONS = new Set(['.cts', '.mts', '.ts'])
|
|
10
12
|
const V2_MIN_NODE_VERSION = '18.0.0'
|
|
11
13
|
|
|
12
14
|
// Returns a new set with all elements of `setA` that don't exist in `setB`.
|
|
@@ -57,6 +59,35 @@ export default class NetlifyFunction {
|
|
|
57
59
|
this.srcFiles = new Set()
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
get filename() {
|
|
63
|
+
if (!this.buildData?.mainFile) {
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return basename(this.buildData.mainFile)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getRecommendedExtension() {
|
|
71
|
+
if (this.buildData?.runtimeAPIVersion !== 2) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const extension = this.buildData?.mainFile ? extname(this.buildData.mainFile) : undefined
|
|
76
|
+
const moduleFormat = this.buildData?.outputModuleFormat
|
|
77
|
+
|
|
78
|
+
if (moduleFormat === 'esm') {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (extension === '.ts') {
|
|
83
|
+
return '.mts'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (extension === '.js') {
|
|
87
|
+
return '.mjs'
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
60
91
|
hasValidName() {
|
|
61
92
|
// same as https://github.com/netlify/bitballoon/blob/fbd7881e6c8e8c48e7a0145da4ee26090c794108/app/models/deploy.rb#L482
|
|
62
93
|
// eslint-disable-next-line unicorn/better-regex
|
|
@@ -73,6 +104,14 @@ export default class NetlifyFunction {
|
|
|
73
104
|
return !(this.buildData?.runtimeAPIVersion === 2 && semver.lt(nodeVersion, V2_MIN_NODE_VERSION))
|
|
74
105
|
}
|
|
75
106
|
|
|
107
|
+
isTypeScript() {
|
|
108
|
+
if (this.filename === null) {
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return TYPESCRIPT_EXTENSIONS.has(extname(this.filename))
|
|
113
|
+
}
|
|
114
|
+
|
|
76
115
|
async getNextRun() {
|
|
77
116
|
if (!(await this.isScheduled())) {
|
|
78
117
|
return null
|
|
@@ -111,7 +150,7 @@ export default class NetlifyFunction {
|
|
|
111
150
|
throw new Error(
|
|
112
151
|
`Function requires Node.js version ${V2_MIN_NODE_VERSION} or above, but ${nodeVersion.slice(
|
|
113
152
|
1,
|
|
114
|
-
)} is installed. Refer to https://ntl.fyi/functions-
|
|
153
|
+
)} is installed. Refer to https://ntl.fyi/functions-runtime for information on how to update.`,
|
|
115
154
|
)
|
|
116
155
|
}
|
|
117
156
|
|
|
@@ -123,6 +162,12 @@ export default class NetlifyFunction {
|
|
|
123
162
|
}
|
|
124
163
|
}
|
|
125
164
|
|
|
165
|
+
async getBuildData() {
|
|
166
|
+
await this.buildQueue
|
|
167
|
+
|
|
168
|
+
return this.buildData
|
|
169
|
+
}
|
|
170
|
+
|
|
126
171
|
// Compares a new set of source files against a previous one, returning an
|
|
127
172
|
// object with two Sets, one with added and the other with deleted files.
|
|
128
173
|
getSrcFilesDiff(newSrcFiles) {
|
|
@@ -167,7 +212,8 @@ export default class NetlifyFunction {
|
|
|
167
212
|
async matchURLPath(rawPath, method) {
|
|
168
213
|
await this.buildQueue
|
|
169
214
|
|
|
170
|
-
|
|
215
|
+
let path = rawPath !== '/' && rawPath.endsWith('/') ? rawPath.slice(0, -1) : rawPath
|
|
216
|
+
path = path.toLowerCase()
|
|
171
217
|
const { routes = [] } = this.buildData
|
|
172
218
|
return routes.find(({ expression, literal, methods }) => {
|
|
173
219
|
if (methods.length !== 0 && !methods.includes(method)) {
|
|
@@ -188,6 +234,10 @@ export default class NetlifyFunction {
|
|
|
188
234
|
})
|
|
189
235
|
}
|
|
190
236
|
|
|
237
|
+
get runtimeAPIVersion() {
|
|
238
|
+
return this.buildData?.runtimeAPIVersion ?? 1
|
|
239
|
+
}
|
|
240
|
+
|
|
191
241
|
get url() {
|
|
192
242
|
// This line fixes the issue here https://github.com/netlify/cli/issues/4116
|
|
193
243
|
// Not sure why `settings.port` was used here nor does a valid reference exist.
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import { mkdir } from 'fs/promises'
|
|
3
|
-
import {
|
|
3
|
+
import { createRequire } from 'module'
|
|
4
|
+
import { basename, extname, isAbsolute, join, resolve } from 'path'
|
|
4
5
|
import { env } from 'process'
|
|
5
6
|
|
|
6
7
|
import { listFunctions } from '@netlify/zip-it-and-ship-it'
|
|
7
8
|
import extractZip from 'extract-zip'
|
|
8
9
|
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
chalk,
|
|
12
|
+
log,
|
|
13
|
+
getTerminalLink,
|
|
14
|
+
NETLIFYDEVERR,
|
|
15
|
+
NETLIFYDEVLOG,
|
|
16
|
+
NETLIFYDEVWARN,
|
|
17
|
+
warn,
|
|
18
|
+
watchDebounced,
|
|
19
|
+
} from '../../utils/command-helpers.mjs'
|
|
10
20
|
import { INTERNAL_FUNCTIONS_FOLDER, SERVE_FUNCTIONS_FOLDER } from '../../utils/functions/functions.mjs'
|
|
11
21
|
import { BACKGROUND_FUNCTIONS_WARNING } from '../log.mjs'
|
|
12
22
|
import { getPathInProject } from '../settings.mjs'
|
|
@@ -14,10 +24,25 @@ import { getPathInProject } from '../settings.mjs'
|
|
|
14
24
|
import NetlifyFunction from './netlify-function.mjs'
|
|
15
25
|
import runtimes from './runtimes/index.mjs'
|
|
16
26
|
|
|
27
|
+
export const DEFAULT_FUNCTION_URL_EXPRESSION = /^\/.netlify\/(functions|builders)\/([^/]+).*/
|
|
28
|
+
const TYPES_PACKAGE = '@netlify/functions'
|
|
17
29
|
const ZIP_EXTENSION = '.zip'
|
|
18
30
|
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {"buildError" | "extracted" | "loaded" | "missing-types-package" | "reloaded" | "reloading" | "removed"} FunctionEvent
|
|
33
|
+
*/
|
|
34
|
+
|
|
19
35
|
export class FunctionsRegistry {
|
|
20
|
-
constructor({
|
|
36
|
+
constructor({
|
|
37
|
+
capabilities,
|
|
38
|
+
config,
|
|
39
|
+
debug = false,
|
|
40
|
+
isConnected = false,
|
|
41
|
+
logLambdaCompat,
|
|
42
|
+
projectRoot,
|
|
43
|
+
settings,
|
|
44
|
+
timeouts,
|
|
45
|
+
}) {
|
|
21
46
|
this.capabilities = capabilities
|
|
22
47
|
this.config = config
|
|
23
48
|
this.debug = debug
|
|
@@ -26,25 +51,78 @@ export class FunctionsRegistry {
|
|
|
26
51
|
this.timeouts = timeouts
|
|
27
52
|
this.settings = settings
|
|
28
53
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
54
|
+
/**
|
|
55
|
+
* An object to be shared among all functions in the registry. It can be
|
|
56
|
+
* used to cache the results of the build function — e.g. it's used in
|
|
57
|
+
* the `memoizedBuild` method in the JavaScript runtime.
|
|
58
|
+
*
|
|
59
|
+
* @type {Record<string, unknown>}
|
|
60
|
+
*/
|
|
32
61
|
this.buildCache = {}
|
|
33
62
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
63
|
+
/**
|
|
64
|
+
* File watchers for parent directories where functions live — i.e. the
|
|
65
|
+
* ones supplied to `scan()`. This is a Map because in the future we
|
|
66
|
+
* might have several function directories.
|
|
67
|
+
*
|
|
68
|
+
* @type {Map<string, Awaited<ReturnType<watchDebounced>>>}
|
|
69
|
+
*/
|
|
37
70
|
this.directoryWatchers = new Map()
|
|
38
71
|
|
|
39
|
-
|
|
40
|
-
|
|
72
|
+
/**
|
|
73
|
+
* The functions held by the registry
|
|
74
|
+
*
|
|
75
|
+
* @type {Map<string, NetlifyFunction>}
|
|
76
|
+
*/
|
|
41
77
|
this.functions = new Map()
|
|
42
78
|
|
|
43
|
-
|
|
44
|
-
|
|
79
|
+
/**
|
|
80
|
+
* File watchers for function files. Maps function names to objects built
|
|
81
|
+
* by the `watchDebounced` utility.
|
|
82
|
+
*
|
|
83
|
+
* @type {Map<string, Awaited<ReturnType<watchDebounced>>>}
|
|
84
|
+
*/
|
|
45
85
|
this.functionWatchers = new Map()
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Keeps track of whether we've checked whether `TYPES_PACKAGE` is
|
|
89
|
+
* installed.
|
|
90
|
+
*/
|
|
91
|
+
this.hasCheckedTypesPackage = false
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Whether to log V1 functions as using the "Lambda compatibility mode"
|
|
95
|
+
*
|
|
96
|
+
* @type {boolean}
|
|
97
|
+
*/
|
|
98
|
+
this.logLambdaCompat = Boolean(logLambdaCompat)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
checkTypesPackage() {
|
|
102
|
+
if (this.hasCheckedTypesPackage) {
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.hasCheckedTypesPackage = true
|
|
107
|
+
|
|
108
|
+
const require = createRequire(this.projectRoot)
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
require.resolve(TYPES_PACKAGE, { paths: [this.projectRoot] })
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error?.code === 'MODULE_NOT_FOUND') {
|
|
114
|
+
FunctionsRegistry.logEvent('missing-types-package', {})
|
|
115
|
+
}
|
|
116
|
+
}
|
|
46
117
|
}
|
|
47
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Runs before `scan` and calls any `onDirectoryScan` hooks defined by the
|
|
121
|
+
* runtime before the directory is read. This gives runtime the opportunity
|
|
122
|
+
* to run additional logic when a directory is scanned.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} directory
|
|
125
|
+
*/
|
|
48
126
|
static async prepareDirectoryScan(directory) {
|
|
49
127
|
await mkdir(directory, { recursive: true })
|
|
50
128
|
|
|
@@ -62,23 +140,45 @@ export class FunctionsRegistry {
|
|
|
62
140
|
)
|
|
63
141
|
}
|
|
64
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Builds a function and sets up the appropriate file watchers so that any
|
|
145
|
+
* changes will trigger another build.
|
|
146
|
+
*
|
|
147
|
+
* @param {NetlifyFunction} func
|
|
148
|
+
* @param {boolean} [firstLoad ]
|
|
149
|
+
* @returns
|
|
150
|
+
*/
|
|
65
151
|
async buildFunctionAndWatchFiles(func, firstLoad = false) {
|
|
66
152
|
if (!firstLoad) {
|
|
67
|
-
|
|
153
|
+
FunctionsRegistry.logEvent('reloading', { func })
|
|
68
154
|
}
|
|
69
155
|
|
|
70
156
|
const { error: buildError, includedFiles, srcFilesDiff } = await func.build({ cache: this.buildCache })
|
|
71
157
|
|
|
72
158
|
if (buildError) {
|
|
73
|
-
|
|
74
|
-
`${NETLIFYDEVERR} ${chalk.red('Failed to load')} function ${chalk.yellow(func.displayName)}: ${
|
|
75
|
-
buildError.message
|
|
76
|
-
}`,
|
|
77
|
-
)
|
|
159
|
+
FunctionsRegistry.logEvent('buildError', { func })
|
|
78
160
|
} else {
|
|
79
|
-
const
|
|
161
|
+
const event = firstLoad ? 'loaded' : 'reloaded'
|
|
162
|
+
const recommendedExtension = func.getRecommendedExtension()
|
|
163
|
+
|
|
164
|
+
if (recommendedExtension) {
|
|
165
|
+
const { filename } = func
|
|
166
|
+
const newFilename = filename ? `${basename(filename, extname(filename))}${recommendedExtension}` : null
|
|
167
|
+
const action = newFilename
|
|
168
|
+
? `rename the function file to ${chalk.underline(
|
|
169
|
+
newFilename,
|
|
170
|
+
)}. Refer to https://ntl.fyi/functions-runtime for more information`
|
|
171
|
+
: `refer to https://ntl.fyi/functions-runtime`
|
|
172
|
+
const warning = `The function is using the legacy CommonJS format. To start using ES modules, ${action}.`
|
|
173
|
+
|
|
174
|
+
FunctionsRegistry.logEvent(event, { func, warnings: [warning] })
|
|
175
|
+
} else {
|
|
176
|
+
FunctionsRegistry.logEvent(event, { func })
|
|
177
|
+
}
|
|
178
|
+
}
|
|
80
179
|
|
|
81
|
-
|
|
180
|
+
if (func.isTypeScript()) {
|
|
181
|
+
this.checkTypesPackage()
|
|
82
182
|
}
|
|
83
183
|
|
|
84
184
|
// If the build hasn't resulted in any files being added or removed, there
|
|
@@ -107,7 +207,6 @@ export class FunctionsRegistry {
|
|
|
107
207
|
// we create a new watcher and watch them.
|
|
108
208
|
if (srcFilesDiff.added.size !== 0) {
|
|
109
209
|
const filesToWatch = [...srcFilesDiff.added, ...includedFiles]
|
|
110
|
-
|
|
111
210
|
const newWatcher = await watchDebounced(filesToWatch, {
|
|
112
211
|
onChange: () => {
|
|
113
212
|
this.buildFunctionAndWatchFiles(func, false)
|
|
@@ -118,11 +217,53 @@ export class FunctionsRegistry {
|
|
|
118
217
|
}
|
|
119
218
|
}
|
|
120
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Returns a function by name.
|
|
222
|
+
*
|
|
223
|
+
* @param {string} name
|
|
224
|
+
*/
|
|
121
225
|
get(name) {
|
|
122
226
|
return this.functions.get(name)
|
|
123
227
|
}
|
|
124
228
|
|
|
125
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Returns the first function in the registry that matches a given URL path
|
|
231
|
+
* and an HTTP method. If no match is found, `undefined` is returned.
|
|
232
|
+
*
|
|
233
|
+
* @param {string} url
|
|
234
|
+
* @param {string} method
|
|
235
|
+
*/
|
|
236
|
+
async getFunctionForURLPath(url, method) {
|
|
237
|
+
// We're constructing a URL object just so that we can extract the path from
|
|
238
|
+
// the incoming URL. It doesn't really matter that we don't have the actual
|
|
239
|
+
// local URL with the correct port.
|
|
240
|
+
const urlPath = new URL(url, 'http://localhost').pathname
|
|
241
|
+
const defaultURLMatch = urlPath.match(DEFAULT_FUNCTION_URL_EXPRESSION)
|
|
242
|
+
|
|
243
|
+
if (defaultURLMatch) {
|
|
244
|
+
const func = this.get(defaultURLMatch[2])
|
|
245
|
+
|
|
246
|
+
if (!func) {
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const { routes = [] } = await func.getBuildData()
|
|
251
|
+
|
|
252
|
+
if (routes.length !== 0) {
|
|
253
|
+
const paths = routes.map((route) => chalk.underline(route.pattern)).join(', ')
|
|
254
|
+
|
|
255
|
+
warn(
|
|
256
|
+
`Function ${chalk.yellow(func.name)} cannot be invoked on ${chalk.underline(
|
|
257
|
+
urlPath,
|
|
258
|
+
)}, because the function has the following URL paths defined: ${paths}`,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return { func, route: null }
|
|
265
|
+
}
|
|
266
|
+
|
|
126
267
|
for (const func of this.functions.values()) {
|
|
127
268
|
const route = await func.matchURLPath(urlPath, method)
|
|
128
269
|
|
|
@@ -132,7 +273,90 @@ export class FunctionsRegistry {
|
|
|
132
273
|
}
|
|
133
274
|
}
|
|
134
275
|
|
|
135
|
-
|
|
276
|
+
/**
|
|
277
|
+
* Logs an event associated with functions.
|
|
278
|
+
*
|
|
279
|
+
* @param {FunctionEvent} event
|
|
280
|
+
* @param {object} data
|
|
281
|
+
* @param {NetlifyFunction} [data.func]
|
|
282
|
+
* @param {string[]} [data.warnings]
|
|
283
|
+
* @returns
|
|
284
|
+
*/
|
|
285
|
+
static logEvent(event, { func, warnings = [] }) {
|
|
286
|
+
let warningsText = ''
|
|
287
|
+
|
|
288
|
+
if (warnings.length !== 0) {
|
|
289
|
+
warningsText = ` with warnings:\n${warnings.map((warning) => ` - ${warning}`).join('\n')}`
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (event === 'buildError') {
|
|
293
|
+
log(
|
|
294
|
+
`${NETLIFYDEVERR} ${chalk.red('Failed to load')} function ${chalk.yellow(func?.displayName)}: ${
|
|
295
|
+
func?.buildError?.message
|
|
296
|
+
}`,
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (event === 'extracted') {
|
|
301
|
+
log(
|
|
302
|
+
`${NETLIFYDEVLOG} ${chalk.green('Extracted')} function ${chalk.yellow(func?.displayName)} from ${
|
|
303
|
+
func?.mainFile
|
|
304
|
+
}.`,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (event === 'loaded') {
|
|
311
|
+
const icon = warningsText ? NETLIFYDEVWARN : NETLIFYDEVLOG
|
|
312
|
+
const color = warningsText ? chalk.yellow : chalk.green
|
|
313
|
+
const mode =
|
|
314
|
+
func?.runtimeAPIVersion === 1 && this.logLambdaCompat
|
|
315
|
+
? ` in ${getTerminalLink('Lambda compatibility mode', 'https://ntl.fyi/lambda-compat')}`
|
|
316
|
+
: ''
|
|
317
|
+
|
|
318
|
+
log(`${icon} ${color('Loaded')} function ${chalk.yellow(func?.displayName)}${mode}${warningsText}`)
|
|
319
|
+
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (event === 'missing-types-package') {
|
|
324
|
+
log(
|
|
325
|
+
`${NETLIFYDEVWARN} For a better experience with TypeScript functions, consider installing the ${chalk.underline(
|
|
326
|
+
TYPES_PACKAGE,
|
|
327
|
+
)} package. Refer to https://ntl-fyi/function-types for more information.`,
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (event === 'reloaded') {
|
|
332
|
+
const icon = warningsText ? NETLIFYDEVWARN : NETLIFYDEVLOG
|
|
333
|
+
const color = warningsText ? chalk.yellow : chalk.green
|
|
334
|
+
|
|
335
|
+
log(`${icon} ${color('Reloaded')} function ${chalk.yellow(func?.displayName)}${warningsText}`)
|
|
336
|
+
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (event === 'reloading') {
|
|
341
|
+
log(`${NETLIFYDEVLOG} ${chalk.magenta('Reloading')} function ${chalk.yellow(func?.displayName)}...`)
|
|
342
|
+
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (event === 'removed') {
|
|
347
|
+
log(`${NETLIFYDEVLOG} ${chalk.magenta('Removed')} function ${chalk.yellow(func?.displayName)}`)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Adds a function to the registry
|
|
353
|
+
*
|
|
354
|
+
* @param {string} name
|
|
355
|
+
* @param {NetlifyFunction} funcBeforeHook
|
|
356
|
+
* @param {boolean} [isReload]
|
|
357
|
+
* @returns
|
|
358
|
+
*/
|
|
359
|
+
async registerFunction(name, funcBeforeHook, isReload = false) {
|
|
136
360
|
const { runtime } = funcBeforeHook
|
|
137
361
|
|
|
138
362
|
// The `onRegister` hook allows runtimes to modify the function before it's
|
|
@@ -160,7 +384,7 @@ export class FunctionsRegistry {
|
|
|
160
384
|
const unzippedDirectory = await this.unzipFunction(func)
|
|
161
385
|
|
|
162
386
|
if (this.debug) {
|
|
163
|
-
|
|
387
|
+
FunctionsRegistry.logEvent('extracted', { func })
|
|
164
388
|
}
|
|
165
389
|
|
|
166
390
|
func.mainFile = join(unzippedDirectory, `${func.name}.js`)
|
|
@@ -168,15 +392,27 @@ export class FunctionsRegistry {
|
|
|
168
392
|
|
|
169
393
|
this.functions.set(name, func)
|
|
170
394
|
|
|
171
|
-
this.buildFunctionAndWatchFiles(func,
|
|
395
|
+
this.buildFunctionAndWatchFiles(func, !isReload)
|
|
172
396
|
}
|
|
173
397
|
|
|
174
|
-
|
|
398
|
+
/**
|
|
399
|
+
* A proxy to zip-it-and-ship-it's `listFunctions` method. It exists just so
|
|
400
|
+
* that we can mock it in tests.
|
|
401
|
+
* @param {Parameters<listFunctions>} args
|
|
402
|
+
* @returns
|
|
403
|
+
*/
|
|
175
404
|
// eslint-disable-next-line class-methods-use-this
|
|
176
405
|
async listFunctions(...args) {
|
|
177
406
|
return await listFunctions(...args)
|
|
178
407
|
}
|
|
179
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Takes a list of directories and scans for functions. It keeps tracks of
|
|
411
|
+
* any functions in those directories that we've previously seen, and takes
|
|
412
|
+
* care of registering and unregistering functions as they come and go.
|
|
413
|
+
*
|
|
414
|
+
* @param {string[]} relativeDirs
|
|
415
|
+
*/
|
|
180
416
|
async scan(relativeDirs) {
|
|
181
417
|
const directories = relativeDirs.filter(Boolean).map((dir) => (isAbsolute(dir) ? dir : join(this.projectRoot, dir)))
|
|
182
418
|
|
|
@@ -199,15 +435,16 @@ export class FunctionsRegistry {
|
|
|
199
435
|
// the previous list but are missing from the new one. We unregister them.
|
|
200
436
|
const deletedFunctions = [...this.functions.values()].filter((oldFunc) => {
|
|
201
437
|
const isFound = functions.some(
|
|
202
|
-
(newFunc) => newFunc.name === oldFunc.name && newFunc.
|
|
438
|
+
(newFunc) => newFunc.name === oldFunc.name && newFunc.mainFile === oldFunc.mainFile,
|
|
203
439
|
)
|
|
204
440
|
|
|
205
441
|
return !isFound
|
|
206
442
|
})
|
|
207
443
|
|
|
208
|
-
await Promise.all(deletedFunctions.map((func) => this.unregisterFunction(func
|
|
444
|
+
await Promise.all(deletedFunctions.map((func) => this.unregisterFunction(func)))
|
|
209
445
|
|
|
210
|
-
|
|
446
|
+
const deletedFunctionNames = new Set(deletedFunctions.map((func) => func.name))
|
|
447
|
+
const addedFunctions = await Promise.all(
|
|
211
448
|
// zip-it-and-ship-it returns an array sorted based on which extension should have precedence,
|
|
212
449
|
// where the last ones precede the previous ones. This is why
|
|
213
450
|
// we reverse the array so we get the right functions precedence in the CLI.
|
|
@@ -238,16 +475,38 @@ export class FunctionsRegistry {
|
|
|
238
475
|
settings: this.settings,
|
|
239
476
|
})
|
|
240
477
|
|
|
241
|
-
|
|
478
|
+
// If a function we're registering was also unregistered in this run,
|
|
479
|
+
// then it was a rename. Let's flag it as such so that the messaging
|
|
480
|
+
// is adjusted accordingly.
|
|
481
|
+
const isReload = deletedFunctionNames.has(name)
|
|
482
|
+
|
|
483
|
+
await this.registerFunction(name, func, isReload)
|
|
484
|
+
|
|
485
|
+
return func
|
|
242
486
|
}),
|
|
243
487
|
)
|
|
488
|
+
const addedFunctionNames = new Set(addedFunctions.filter(Boolean).map((func) => func?.name))
|
|
489
|
+
|
|
490
|
+
deletedFunctions.forEach((func) => {
|
|
491
|
+
// If a function we've unregistered was also registered in this run, then
|
|
492
|
+
// it was a rename that we've already logged. Nothing to do in this case.
|
|
493
|
+
if (addedFunctionNames.has(func.name)) {
|
|
494
|
+
return
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
FunctionsRegistry.logEvent('removed', { func })
|
|
498
|
+
})
|
|
244
499
|
|
|
245
500
|
await Promise.all(directories.map((path) => this.setupDirectoryWatcher(path)))
|
|
246
501
|
}
|
|
247
502
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
503
|
+
/**
|
|
504
|
+
* Creates a watcher that looks at files being added or removed from a
|
|
505
|
+
* functions directory. It doesn't care about files being changed, because
|
|
506
|
+
* those will be handled by each functions' watcher.
|
|
507
|
+
*
|
|
508
|
+
* @param {string} directory
|
|
509
|
+
*/
|
|
251
510
|
async setupDirectoryWatcher(directory) {
|
|
252
511
|
if (this.directoryWatchers.has(directory)) {
|
|
253
512
|
return
|
|
@@ -266,18 +525,30 @@ export class FunctionsRegistry {
|
|
|
266
525
|
this.directoryWatchers.set(directory, watcher)
|
|
267
526
|
}
|
|
268
527
|
|
|
269
|
-
|
|
270
|
-
|
|
528
|
+
/**
|
|
529
|
+
* Removes a function from the registry and closes its file watchers.
|
|
530
|
+
*
|
|
531
|
+
* @param {NetlifyFunction} func
|
|
532
|
+
*/
|
|
533
|
+
async unregisterFunction(func) {
|
|
534
|
+
const { name } = func
|
|
271
535
|
|
|
272
|
-
|
|
536
|
+
this.functions.delete(name)
|
|
273
537
|
|
|
274
538
|
const watcher = this.functionWatchers.get(name)
|
|
275
539
|
|
|
276
540
|
if (watcher) {
|
|
277
541
|
await watcher.close()
|
|
278
542
|
}
|
|
543
|
+
|
|
544
|
+
this.functionWatchers.delete(name)
|
|
279
545
|
}
|
|
280
546
|
|
|
547
|
+
/**
|
|
548
|
+
* Takes a zipped function and extracts its contents to an internal directory.
|
|
549
|
+
*
|
|
550
|
+
* @param {NetlifyFunction} func
|
|
551
|
+
*/
|
|
281
552
|
async unzipFunction(func) {
|
|
282
553
|
const targetDirectory = resolve(
|
|
283
554
|
this.projectRoot,
|
|
@@ -57,6 +57,8 @@ const buildFunction = async ({
|
|
|
57
57
|
entryFilename,
|
|
58
58
|
includedFiles,
|
|
59
59
|
inputs,
|
|
60
|
+
mainFile,
|
|
61
|
+
outputModuleFormat,
|
|
60
62
|
path: functionPath,
|
|
61
63
|
routes,
|
|
62
64
|
runtimeAPIVersion,
|
|
@@ -82,7 +84,7 @@ const buildFunction = async ({
|
|
|
82
84
|
|
|
83
85
|
clearFunctionsCache(targetDirectory)
|
|
84
86
|
|
|
85
|
-
return { buildPath, includedFiles, routes, runtimeAPIVersion, srcFiles, schedule }
|
|
87
|
+
return { buildPath, includedFiles, outputModuleFormat, mainFile, routes, runtimeAPIVersion, srcFiles, schedule }
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
/**
|
|
@@ -4,7 +4,6 @@ import { pathToFileURL } from 'url'
|
|
|
4
4
|
import { Worker } from 'worker_threads'
|
|
5
5
|
|
|
6
6
|
import lambdaLocal from 'lambda-local'
|
|
7
|
-
import winston from 'winston'
|
|
8
7
|
|
|
9
8
|
import detectNetlifyLambdaBuilder from './builders/netlify-lambda.mjs'
|
|
10
9
|
import detectZisiBuilder, { parseFunctionForMetadata } from './builders/zisi.mjs'
|
|
@@ -14,12 +13,7 @@ export const name = 'js'
|
|
|
14
13
|
|
|
15
14
|
let netlifyLambdaDetectorCache
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
levels: winston.config.npm.levels,
|
|
19
|
-
transports: [new winston.transports.Console({ level: 'warn' })],
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
lambdaLocal.setLogger(logger)
|
|
16
|
+
lambdaLocal.getLogger().level = 'alert'
|
|
23
17
|
|
|
24
18
|
// The netlify-lambda builder can't be enabled or disabled on a per-function
|
|
25
19
|
// basis and its detection mechanism is also quite expensive, so we detect
|