netlify-cli 16.0.3 → 16.2.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 +715 -711
- package/package.json +8 -8
- package/src/commands/base-command.mjs +1 -1
- package/src/commands/dev/dev.mjs +2 -1
- package/src/commands/serve/serve.mjs +3 -1
- package/src/lib/edge-functions/bootstrap.mjs +1 -1
- package/src/lib/edge-functions/registry.mjs +28 -13
- package/src/lib/functions/netlify-function.mjs +22 -0
- package/src/lib/functions/registry.mjs +10 -0
- package/src/lib/functions/runtimes/js/builders/zisi.mjs +2 -1
- package/src/lib/functions/server.mjs +19 -5
- package/src/utils/headers.mjs +1 -0
- package/src/utils/proxy-server.mjs +3 -0
- package/src/utils/proxy.mjs +72 -12
- package/src/utils/run-build.mjs +1 -1
- package/src/utils/telemetry/telemetry.mjs +20 -4
- package/src/utils/validation.mjs +4 -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": "16.0
|
|
4
|
+
"version": "16.2.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.20.
|
|
48
|
-
"@netlify/build-info": "7.7.
|
|
47
|
+
"@netlify/build": "29.20.8",
|
|
48
|
+
"@netlify/build-info": "7.7.4",
|
|
49
49
|
"@netlify/config": "20.8.0",
|
|
50
|
-
"@netlify/edge-bundler": "8.
|
|
50
|
+
"@netlify/edge-bundler": "8.19.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.7.3",
|
|
53
|
+
"@netlify/zip-it-and-ship-it": "9.17.0",
|
|
54
54
|
"@octokit/rest": "19.0.13",
|
|
55
55
|
"ansi-escapes": "6.2.0",
|
|
56
56
|
"ansi-styles": "6.2.1",
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"isexe": "2.0.0",
|
|
107
107
|
"jsonwebtoken": "9.0.1",
|
|
108
108
|
"jwt-decode": "3.1.2",
|
|
109
|
-
"lambda-local": "2.1.
|
|
109
|
+
"lambda-local": "2.1.2",
|
|
110
110
|
"listr": "0.14.3",
|
|
111
111
|
"locate-path": "7.2.0",
|
|
112
112
|
"lodash": "4.17.21",
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
"multiparty": "4.2.3",
|
|
117
117
|
"netlify": "13.1.10",
|
|
118
118
|
"netlify-headers-parser": "7.1.2",
|
|
119
|
-
"netlify-redirect-parser": "14.
|
|
119
|
+
"netlify-redirect-parser": "14.2.0",
|
|
120
120
|
"netlify-redirector": "0.4.0",
|
|
121
121
|
"node-fetch": "2.6.12",
|
|
122
122
|
"node-version-alias": "3.4.1",
|
|
@@ -574,7 +574,7 @@ export default class BaseCommand extends Command {
|
|
|
574
574
|
// configuration file and the API
|
|
575
575
|
// ==================================================
|
|
576
576
|
const cachedConfig = await actionCommand.getConfig({
|
|
577
|
-
cwd: this.jsWorkspaceRoot || this.workingDir,
|
|
577
|
+
cwd: flags.cwd ? this.workingDir : this.jsWorkspaceRoot || this.workingDir,
|
|
578
578
|
repositoryRoot: rootDir,
|
|
579
579
|
packagePath: this.workspacePackage,
|
|
580
580
|
// The config flag needs to be resolved from the actual process working directory
|
package/src/commands/dev/dev.mjs
CHANGED
|
@@ -161,7 +161,7 @@ const dev = async (options, command) => {
|
|
|
161
161
|
},
|
|
162
162
|
})
|
|
163
163
|
|
|
164
|
-
await startFunctionsServer({
|
|
164
|
+
const functionsRegistry = await startFunctionsServer({
|
|
165
165
|
api,
|
|
166
166
|
command,
|
|
167
167
|
config,
|
|
@@ -217,6 +217,7 @@ const dev = async (options, command) => {
|
|
|
217
217
|
geolocationMode: options.geo,
|
|
218
218
|
geoCountry: options.country,
|
|
219
219
|
accountId,
|
|
220
|
+
functionsRegistry,
|
|
220
221
|
})
|
|
221
222
|
|
|
222
223
|
if (devConfig.autoLaunch !== false) {
|
|
@@ -93,7 +93,7 @@ const serve = async (options, command) => {
|
|
|
93
93
|
options,
|
|
94
94
|
})
|
|
95
95
|
|
|
96
|
-
await startFunctionsServer({
|
|
96
|
+
const functionsRegistry = await startFunctionsServer({
|
|
97
97
|
api,
|
|
98
98
|
command,
|
|
99
99
|
config,
|
|
@@ -132,7 +132,9 @@ const serve = async (options, command) => {
|
|
|
132
132
|
addonsUrls,
|
|
133
133
|
config,
|
|
134
134
|
configPath: configPathOverride,
|
|
135
|
+
debug: options.debug,
|
|
135
136
|
env,
|
|
137
|
+
functionsRegistry,
|
|
136
138
|
geolocationMode: options.geo,
|
|
137
139
|
geoCountry: options.country,
|
|
138
140
|
getUpdatedConfig,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { env } from 'process'
|
|
2
2
|
|
|
3
|
-
const latestBootstrapURL = 'https://
|
|
3
|
+
const latestBootstrapURL = 'https://64e7783fce8cfe0008496c72--edge.netlify.com/bootstrap/index-combined.ts'
|
|
4
4
|
|
|
5
5
|
export const getBootstrapURL = () => env.NETLIFY_EDGE_BOOTSTRAP || latestBootstrapURL
|
|
@@ -318,23 +318,38 @@ export class EdgeFunctionsRegistry {
|
|
|
318
318
|
functions: this.#functions,
|
|
319
319
|
featureFlags,
|
|
320
320
|
})
|
|
321
|
-
const invocationMetadata = {
|
|
322
|
-
function_config: manifest.function_config,
|
|
323
|
-
routes: manifest.routes.map((route) => ({ function: route.function, pattern: route.pattern })),
|
|
324
|
-
}
|
|
325
321
|
const routes = [...manifest.routes, ...manifest.post_cache_routes].map((route) => ({
|
|
326
322
|
...route,
|
|
327
323
|
pattern: new RegExp(route.pattern),
|
|
328
324
|
}))
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
.
|
|
325
|
+
|
|
326
|
+
/** @type string[] */
|
|
327
|
+
const functionNames = []
|
|
328
|
+
|
|
329
|
+
/** @type number[] */
|
|
330
|
+
const routeIndexes = []
|
|
331
|
+
|
|
332
|
+
routes.forEach((route, index) => {
|
|
333
|
+
if (!route.pattern.test(urlPath)) {
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const isExcluded = manifest.function_config[route.function]?.excluded_patterns?.some((pattern) =>
|
|
338
|
+
new RegExp(pattern).test(urlPath),
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
if (isExcluded) {
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
functionNames.push(route.function)
|
|
346
|
+
routeIndexes.push(index)
|
|
347
|
+
})
|
|
348
|
+
const invocationMetadata = {
|
|
349
|
+
function_config: manifest.function_config,
|
|
350
|
+
req_routes: routeIndexes,
|
|
351
|
+
routes: manifest.routes.map((route) => ({ function: route.function, path: route.path, pattern: route.pattern })),
|
|
352
|
+
}
|
|
338
353
|
const orphanedDeclarations = this.#matchURLPathAgainstOrphanedDeclarations(urlPath)
|
|
339
354
|
|
|
340
355
|
return { functionNames, invocationMetadata, orphanedDeclarations }
|
|
@@ -158,6 +158,28 @@ export default class NetlifyFunction {
|
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
async matchURLPath(rawPath) {
|
|
162
|
+
await this.buildQueue
|
|
163
|
+
|
|
164
|
+
const path = (rawPath.endsWith('/') ? rawPath.slice(0, -1) : rawPath).toLowerCase()
|
|
165
|
+
const { routes = [] } = this.buildData
|
|
166
|
+
const isMatch = routes.some(({ expression, literal }) => {
|
|
167
|
+
if (literal !== undefined) {
|
|
168
|
+
return path === literal
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (expression !== undefined) {
|
|
172
|
+
const regex = new RegExp(expression)
|
|
173
|
+
|
|
174
|
+
return regex.test(path)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return false
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
return isMatch
|
|
181
|
+
}
|
|
182
|
+
|
|
161
183
|
get url() {
|
|
162
184
|
// This line fixes the issue here https://github.com/netlify/cli/issues/4116
|
|
163
185
|
// Not sure why `settings.port` was used here nor does a valid reference exist.
|
|
@@ -122,6 +122,16 @@ export class FunctionsRegistry {
|
|
|
122
122
|
return this.functions.get(name)
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
async getFunctionForURLPath(urlPath) {
|
|
126
|
+
for (const func of this.functions.values()) {
|
|
127
|
+
const isMatch = await func.matchURLPath(urlPath)
|
|
128
|
+
|
|
129
|
+
if (isMatch) {
|
|
130
|
+
return func
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
125
135
|
async registerFunction(name, funcBeforeHook) {
|
|
126
136
|
const { runtime } = funcBeforeHook
|
|
127
137
|
|
|
@@ -58,6 +58,7 @@ const buildFunction = async ({
|
|
|
58
58
|
includedFiles,
|
|
59
59
|
inputs,
|
|
60
60
|
path: functionPath,
|
|
61
|
+
routes,
|
|
61
62
|
runtimeAPIVersion,
|
|
62
63
|
schedule,
|
|
63
64
|
} = await memoizedBuild({
|
|
@@ -81,7 +82,7 @@ const buildFunction = async ({
|
|
|
81
82
|
|
|
82
83
|
clearFunctionsCache(targetDirectory)
|
|
83
84
|
|
|
84
|
-
return { buildPath, includedFiles, runtimeAPIVersion, srcFiles, schedule }
|
|
85
|
+
return { buildPath, includedFiles, routes, runtimeAPIVersion, srcFiles, schedule }
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
/**
|
|
@@ -7,6 +7,7 @@ import jwtDecode from 'jwt-decode'
|
|
|
7
7
|
|
|
8
8
|
import { NETLIFYDEVERR, NETLIFYDEVLOG, error as errorExit, log } from '../../utils/command-helpers.mjs'
|
|
9
9
|
import { CLOCKWORK_USERAGENT, getFunctionsDistPath, getInternalFunctionsDir } from '../../utils/functions/index.mjs'
|
|
10
|
+
import { NFFunctionName } from '../../utils/headers.mjs'
|
|
10
11
|
import { headers as efHeaders } from '../edge-functions/headers.mjs'
|
|
11
12
|
import { getGeoLocation } from '../geo-location.mjs'
|
|
12
13
|
|
|
@@ -55,9 +56,20 @@ export const createHandler = function (options) {
|
|
|
55
56
|
const { functionsRegistry } = options
|
|
56
57
|
|
|
57
58
|
return async function handler(request, response) {
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
// If this header is set, it means we've already matched a function and we
|
|
60
|
+
// can just grab its name directly. We delete the header from the request
|
|
61
|
+
// because we don't want to expose it to user code.
|
|
62
|
+
let functionName = request.header(NFFunctionName)
|
|
63
|
+
delete request.headers[NFFunctionName]
|
|
64
|
+
|
|
65
|
+
// If we didn't match a function with a custom route, let's try to match
|
|
66
|
+
// using the fixed URL format.
|
|
67
|
+
if (!functionName) {
|
|
68
|
+
const cleanPath = request.path.replace(/^\/.netlify\/(functions|builders)/, '')
|
|
69
|
+
|
|
70
|
+
functionName = cleanPath.split('/').find(Boolean)
|
|
71
|
+
}
|
|
72
|
+
|
|
61
73
|
const func = functionsRegistry.get(functionName)
|
|
62
74
|
|
|
63
75
|
if (func === undefined) {
|
|
@@ -231,7 +243,7 @@ const getFunctionsServer = (options) => {
|
|
|
231
243
|
* @param {*} options.site
|
|
232
244
|
* @param {string} options.siteUrl
|
|
233
245
|
* @param {*} options.timeouts
|
|
234
|
-
* @returns
|
|
246
|
+
* @returns {Promise<import('./registry.mjs').FunctionsRegistry | undefined>}
|
|
235
247
|
*/
|
|
236
248
|
export const startFunctionsServer = async (options) => {
|
|
237
249
|
const { capabilities, command, config, debug, loadDistFunctions, settings, site, siteUrl, timeouts } = options
|
|
@@ -272,9 +284,11 @@ export const startFunctionsServer = async (options) => {
|
|
|
272
284
|
|
|
273
285
|
await functionsRegistry.scan(functionsDirectories)
|
|
274
286
|
|
|
275
|
-
const server =
|
|
287
|
+
const server = getFunctionsServer(Object.assign(options, { functionsRegistry }))
|
|
276
288
|
|
|
277
289
|
await startWebServer({ server, settings, debug })
|
|
290
|
+
|
|
291
|
+
return functionsRegistry
|
|
278
292
|
}
|
|
279
293
|
|
|
280
294
|
/**
|
package/src/utils/headers.mjs
CHANGED
|
@@ -52,6 +52,7 @@ export const generateInspectSettings = (edgeInspect, edgeInspectBrk) => {
|
|
|
52
52
|
* @param {*} params.siteInfo
|
|
53
53
|
* @param {string} params.projectDir
|
|
54
54
|
* @param {import('./state-config.mjs').default} params.state
|
|
55
|
+
* @param {import('../lib/functions/registry.mjs').FunctionsRegistry=} params.functionsRegistry
|
|
55
56
|
* @returns
|
|
56
57
|
*/
|
|
57
58
|
export const startProxyServer = async ({
|
|
@@ -61,6 +62,7 @@ export const startProxyServer = async ({
|
|
|
61
62
|
configPath,
|
|
62
63
|
debug,
|
|
63
64
|
env,
|
|
65
|
+
functionsRegistry,
|
|
64
66
|
geoCountry,
|
|
65
67
|
geolocationMode,
|
|
66
68
|
getUpdatedConfig,
|
|
@@ -78,6 +80,7 @@ export const startProxyServer = async ({
|
|
|
78
80
|
configPath: configPath || site.configPath,
|
|
79
81
|
debug,
|
|
80
82
|
env,
|
|
83
|
+
functionsRegistry,
|
|
81
84
|
geolocationMode,
|
|
82
85
|
geoCountry,
|
|
83
86
|
getUpdatedConfig,
|
package/src/utils/proxy.mjs
CHANGED
|
@@ -31,7 +31,7 @@ import renderErrorTemplate from '../lib/render-error-template.mjs'
|
|
|
31
31
|
|
|
32
32
|
import { NETLIFYDEVLOG, NETLIFYDEVWARN, log, chalk } from './command-helpers.mjs'
|
|
33
33
|
import createStreamPromise from './create-stream-promise.mjs'
|
|
34
|
-
import { headersForPath, parseHeaders, NFRequestID } from './headers.mjs'
|
|
34
|
+
import { headersForPath, parseHeaders, NFFunctionName, NFRequestID } from './headers.mjs'
|
|
35
35
|
import { generateRequestID } from './request-id.mjs'
|
|
36
36
|
import { createRewriter, onChanges } from './rules-proxy.mjs'
|
|
37
37
|
import { signRedirect } from './sign-redirect.mjs'
|
|
@@ -75,19 +75,35 @@ const formatEdgeFunctionError = (errorBuffer, acceptsHtml) => {
|
|
|
75
75
|
})
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
/**
|
|
79
|
+
* @param {string} url
|
|
80
|
+
*/
|
|
81
|
+
function isInternal(url) {
|
|
79
82
|
return url.startsWith('/.netlify/')
|
|
80
83
|
}
|
|
81
|
-
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {boolean|number|undefined} functionsPort
|
|
87
|
+
* @param {string} url
|
|
88
|
+
*/
|
|
89
|
+
function isFunction(functionsPort, url) {
|
|
82
90
|
return functionsPort && url.match(/^\/.netlify\/(functions|builders)\/.+/)
|
|
83
91
|
}
|
|
84
92
|
|
|
85
|
-
|
|
86
|
-
|
|
93
|
+
/**
|
|
94
|
+
* @param {Record<string, string>} addonsUrls
|
|
95
|
+
* @param {http.IncomingMessage} req
|
|
96
|
+
*/
|
|
97
|
+
function getAddonUrl(addonsUrls, req) {
|
|
98
|
+
const matches = req.url?.match(/^\/.netlify\/([^/]+)(\/.*)/)
|
|
87
99
|
const addonUrl = matches && addonsUrls[matches[1]]
|
|
88
100
|
return addonUrl ? `${addonUrl}${matches[2]}` : null
|
|
89
101
|
}
|
|
90
102
|
|
|
103
|
+
/**
|
|
104
|
+
* @param {string} pathname
|
|
105
|
+
* @param {string} publicFolder
|
|
106
|
+
*/
|
|
91
107
|
const getStatic = async function (pathname, publicFolder) {
|
|
92
108
|
const alternatives = [pathname, ...alternativePathsFor(pathname)].map((filePath) =>
|
|
93
109
|
path.resolve(publicFolder, filePath.slice(1)),
|
|
@@ -165,7 +181,7 @@ const alternativePathsFor = function (url) {
|
|
|
165
181
|
return paths
|
|
166
182
|
}
|
|
167
183
|
|
|
168
|
-
const serveRedirect = async function ({ env, match, options, proxy, req, res, siteInfo }) {
|
|
184
|
+
const serveRedirect = async function ({ env, functionsRegistry, match, options, proxy, req, res, siteInfo }) {
|
|
169
185
|
if (!match) return proxy.web(req, res, options)
|
|
170
186
|
|
|
171
187
|
options = options || req.proxyOptions || {}
|
|
@@ -198,6 +214,7 @@ const serveRedirect = async function ({ env, match, options, proxy, req, res, si
|
|
|
198
214
|
if (isFunction(options.functionsPort, req.url)) {
|
|
199
215
|
return proxy.web(req, res, { target: options.functionsServer })
|
|
200
216
|
}
|
|
217
|
+
|
|
201
218
|
const urlForAddons = getAddonUrl(options.addonsUrls, req)
|
|
202
219
|
if (urlForAddons) {
|
|
203
220
|
return handleAddonUrl({ req, res, addonUrl: urlForAddons })
|
|
@@ -311,22 +328,28 @@ const serveRedirect = async function ({ env, match, options, proxy, req, res, si
|
|
|
311
328
|
return proxy.web(req, res, { target: options.functionsServer })
|
|
312
329
|
}
|
|
313
330
|
|
|
331
|
+
const functionWithCustomRoute = functionsRegistry && (await functionsRegistry.getFunctionForURLPath(destURL))
|
|
314
332
|
const destStaticFile = await getStatic(dest.pathname, options.publicFolder)
|
|
315
333
|
let statusValue
|
|
316
|
-
if (
|
|
334
|
+
if (
|
|
335
|
+
match.force ||
|
|
336
|
+
(!staticFile && ((!options.framework && destStaticFile) || isInternal(destURL) || functionWithCustomRoute))
|
|
337
|
+
) {
|
|
317
338
|
req.url = destStaticFile ? destStaticFile + dest.search : destURL
|
|
318
339
|
const { status } = match
|
|
319
340
|
statusValue = status
|
|
320
341
|
console.log(`${NETLIFYDEVLOG} Rewrote URL to`, req.url)
|
|
321
342
|
}
|
|
322
343
|
|
|
323
|
-
if (isFunction(options.functionsPort, req.url)) {
|
|
344
|
+
if (isFunction(options.functionsPort, req.url) || functionWithCustomRoute) {
|
|
345
|
+
const functionHeaders = functionWithCustomRoute ? { [NFFunctionName]: functionWithCustomRoute.name } : {}
|
|
324
346
|
const url = reqToURL(req, originalURL)
|
|
325
347
|
req.headers['x-netlify-original-pathname'] = url.pathname
|
|
326
348
|
req.headers['x-netlify-original-search'] = url.search
|
|
327
349
|
|
|
328
|
-
return proxy.web(req, res, { target: options.functionsServer })
|
|
350
|
+
return proxy.web(req, res, { headers: functionHeaders, target: options.functionsServer })
|
|
329
351
|
}
|
|
352
|
+
|
|
330
353
|
const addonUrl = getAddonUrl(options.addonsUrls, req)
|
|
331
354
|
if (addonUrl) {
|
|
332
355
|
return handleAddonUrl({ req, res, addonUrl })
|
|
@@ -418,12 +441,22 @@ const initializeProxy = async function ({ configPath, distDir, env, host, port,
|
|
|
418
441
|
}
|
|
419
442
|
|
|
420
443
|
if (proxyRes.statusCode === 404 || proxyRes.statusCode === 403) {
|
|
444
|
+
// If a request for `/path` has failed, we'll a few variations like
|
|
445
|
+
// `/path/index.html` to mimic the CDN behavior.
|
|
421
446
|
if (req.alternativePaths && req.alternativePaths.length !== 0) {
|
|
422
447
|
req.url = req.alternativePaths.shift()
|
|
423
448
|
return proxy.web(req, res, req.proxyOptions)
|
|
424
449
|
}
|
|
450
|
+
|
|
451
|
+
// The request has failed but we might still have a matching redirect
|
|
452
|
+
// rule (without `force`) that should kick in. This is how we mimic the
|
|
453
|
+
// file shadowing behavior from the CDN.
|
|
425
454
|
if (req.proxyOptions && req.proxyOptions.match) {
|
|
426
455
|
return serveRedirect({
|
|
456
|
+
// We don't want to match functions at this point because any redirects
|
|
457
|
+
// to functions will have already been processed, so we don't supply a
|
|
458
|
+
// functions registry to `serveRedirect`.
|
|
459
|
+
functionsRegistry: null,
|
|
427
460
|
req,
|
|
428
461
|
res,
|
|
429
462
|
proxy: handlers,
|
|
@@ -437,7 +470,19 @@ const initializeProxy = async function ({ configPath, distDir, env, host, port,
|
|
|
437
470
|
|
|
438
471
|
if (req.proxyOptions.staticFile && isRedirect({ status: proxyRes.statusCode }) && proxyRes.headers.location) {
|
|
439
472
|
req.url = proxyRes.headers.location
|
|
440
|
-
return serveRedirect({
|
|
473
|
+
return serveRedirect({
|
|
474
|
+
// We don't want to match functions at this point because any redirects
|
|
475
|
+
// to functions will have already been processed, so we don't supply a
|
|
476
|
+
// functions registry to `serveRedirect`.
|
|
477
|
+
functionsRegistry: null,
|
|
478
|
+
req,
|
|
479
|
+
res,
|
|
480
|
+
proxy: handlers,
|
|
481
|
+
match: null,
|
|
482
|
+
options: req.proxyOptions,
|
|
483
|
+
siteInfo,
|
|
484
|
+
env,
|
|
485
|
+
})
|
|
441
486
|
}
|
|
442
487
|
|
|
443
488
|
const responseData = []
|
|
@@ -535,7 +580,7 @@ const initializeProxy = async function ({ configPath, distDir, env, host, port,
|
|
|
535
580
|
}
|
|
536
581
|
|
|
537
582
|
const onRequest = async (
|
|
538
|
-
{ addonsUrls, edgeFunctionsProxy, env, functionsServer, proxy, rewriter, settings, siteInfo },
|
|
583
|
+
{ addonsUrls, edgeFunctionsProxy, env, functionsRegistry, functionsServer, proxy, rewriter, settings, siteInfo },
|
|
539
584
|
req,
|
|
540
585
|
res,
|
|
541
586
|
) => {
|
|
@@ -549,9 +594,22 @@ const onRequest = async (
|
|
|
549
594
|
return proxy.web(req, res, { target: edgeFunctionsProxyURL })
|
|
550
595
|
}
|
|
551
596
|
|
|
597
|
+
// Does the request match a function on the fixed URL path?
|
|
552
598
|
if (isFunction(settings.functionsPort, req.url)) {
|
|
553
599
|
return proxy.web(req, res, { target: functionsServer })
|
|
554
600
|
}
|
|
601
|
+
|
|
602
|
+
// Does the request match a function on a custom URL path?
|
|
603
|
+
const functionMatch = functionsRegistry ? await functionsRegistry.getFunctionForURLPath(req.url) : null
|
|
604
|
+
|
|
605
|
+
if (functionMatch) {
|
|
606
|
+
// Setting an internal header with the function name so that we don't
|
|
607
|
+
// have to match the URL again in the functions server.
|
|
608
|
+
const headers = { [NFFunctionName]: functionMatch.name }
|
|
609
|
+
|
|
610
|
+
return proxy.web(req, res, { headers, target: functionsServer })
|
|
611
|
+
}
|
|
612
|
+
|
|
555
613
|
const addonUrl = getAddonUrl(addonsUrls, req)
|
|
556
614
|
if (addonUrl) {
|
|
557
615
|
return handleAddonUrl({ req, res, addonUrl })
|
|
@@ -575,7 +633,7 @@ const onRequest = async (
|
|
|
575
633
|
// We don't want to generate an ETag for 3xx redirects.
|
|
576
634
|
req[shouldGenerateETag] = ({ statusCode }) => statusCode < 300 || statusCode >= 400
|
|
577
635
|
|
|
578
|
-
return serveRedirect({ req, res, proxy, match, options, siteInfo, env })
|
|
636
|
+
return serveRedirect({ req, res, proxy, match, options, siteInfo, env, functionsRegistry })
|
|
579
637
|
}
|
|
580
638
|
|
|
581
639
|
// The request will be served by the framework server, which means we want to
|
|
@@ -612,6 +670,7 @@ export const startProxy = async function ({
|
|
|
612
670
|
configPath,
|
|
613
671
|
debug,
|
|
614
672
|
env,
|
|
673
|
+
functionsRegistry,
|
|
615
674
|
geoCountry,
|
|
616
675
|
geolocationMode,
|
|
617
676
|
getUpdatedConfig,
|
|
@@ -665,6 +724,7 @@ export const startProxy = async function ({
|
|
|
665
724
|
rewriter,
|
|
666
725
|
settings,
|
|
667
726
|
addonsUrls,
|
|
727
|
+
functionsRegistry,
|
|
668
728
|
functionsServer,
|
|
669
729
|
edgeFunctionsProxy,
|
|
670
730
|
siteInfo,
|
package/src/utils/run-build.mjs
CHANGED
|
@@ -80,7 +80,7 @@ export const runNetlifyBuild = async ({ command, env = {}, options, settings, ti
|
|
|
80
80
|
const devCommand = async (settingsOverrides = {}) => {
|
|
81
81
|
let cwd = command.workingDir
|
|
82
82
|
|
|
83
|
-
if (command.project.workspace?.packages.length) {
|
|
83
|
+
if (!options.cwd && command.project.workspace?.packages.length) {
|
|
84
84
|
cwd = join(command.project.jsWorkspaceRoot, settings.baseDirectory || '')
|
|
85
85
|
}
|
|
86
86
|
|
|
@@ -13,14 +13,18 @@ import isValidEventName from './validation.mjs'
|
|
|
13
13
|
|
|
14
14
|
const dirPath = dirname(fileURLToPath(import.meta.url))
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* @param {'track' | 'identify'} type
|
|
18
|
+
* @param {object} payload
|
|
19
|
+
*/
|
|
20
|
+
function send(type, payload) {
|
|
17
21
|
const requestFile = join(dirPath, 'request.mjs')
|
|
18
22
|
const options = JSON.stringify({
|
|
19
23
|
data: payload,
|
|
20
24
|
type,
|
|
21
25
|
})
|
|
22
26
|
|
|
23
|
-
const args = [process.execPath, [requestFile, options]]
|
|
27
|
+
const args = /** @type {const} */ ([process.execPath, [requestFile, options]])
|
|
24
28
|
if (process.env.NETLIFY_TEST_TELEMETRY_WAIT === 'true') {
|
|
25
29
|
return execa(...args, {
|
|
26
30
|
stdio: 'inherit',
|
|
@@ -46,7 +50,12 @@ const eventConfig = {
|
|
|
46
50
|
],
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Tracks a custom event with the provided payload
|
|
55
|
+
* @param {string} eventName
|
|
56
|
+
* @param {{status?: string, duration?: number, [key: string]: unknown}} [payload]
|
|
57
|
+
*/
|
|
58
|
+
export async function track(eventName, payload = {}) {
|
|
50
59
|
if (isCI) {
|
|
51
60
|
return
|
|
52
61
|
}
|
|
@@ -83,7 +92,14 @@ export const track = async function (eventName, payload = {}) {
|
|
|
83
92
|
return send('track', defaultData)
|
|
84
93
|
}
|
|
85
94
|
|
|
86
|
-
|
|
95
|
+
/**
|
|
96
|
+
* @param {object} payload
|
|
97
|
+
* @param {string} payload.name
|
|
98
|
+
* @param {string} payload.email
|
|
99
|
+
* @param {string} payload.userId
|
|
100
|
+
* @returns
|
|
101
|
+
*/
|
|
102
|
+
export async function identify(payload) {
|
|
87
103
|
if (isCI) {
|
|
88
104
|
return
|
|
89
105
|
}
|
package/src/utils/validation.mjs
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import { BANG, chalk } from './command-helpers.mjs'
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} exampleCommand
|
|
6
|
+
* @returns {(value:string, previous: unknown) => unknown}
|
|
7
|
+
*/
|
|
4
8
|
export const getGeoCountryArgParser = (exampleCommand) => (arg) => {
|
|
5
9
|
// Validate that the arg passed is two letters only for country
|
|
6
10
|
// See https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes
|