@netlify/functions-dev 1.0.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/dist/main.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { DevEventHandler, Geolocation } from '@netlify/dev-utils';
2
+ import { Manifest } from '@netlify/zip-it-and-ship-it';
3
+ import { EnvironmentContext } from '@netlify/blobs';
4
+
5
+ interface FunctionRegistryOptions {
6
+ blobsContext?: EnvironmentContext;
7
+ destPath: string;
8
+ config: any;
9
+ debug?: boolean;
10
+ eventHandler?: DevEventHandler;
11
+ frameworksAPIFunctionsPath?: string;
12
+ internalFunctionsPath?: string;
13
+ manifest?: Manifest;
14
+ projectRoot: string;
15
+ settings: any;
16
+ timeouts: any;
17
+ watch?: boolean;
18
+ }
19
+
20
+ interface FunctionMatch {
21
+ handle: (req: Request) => Promise<Response>;
22
+ preferStatic: boolean;
23
+ }
24
+ type FunctionsHandlerOptions = FunctionRegistryOptions & {
25
+ accountId?: string;
26
+ geolocation: Geolocation;
27
+ siteId?: string;
28
+ userFunctionsPath?: string;
29
+ };
30
+ declare class FunctionsHandler {
31
+ private accountID?;
32
+ private buildCache;
33
+ private geolocation;
34
+ private globalBuildDirectory;
35
+ private registry;
36
+ private scan;
37
+ private siteID?;
38
+ constructor({ accountId, geolocation, siteId, userFunctionsPath, ...registryOptions }: FunctionsHandlerOptions);
39
+ private invoke;
40
+ match(request: Request, buildDirectory?: string): Promise<FunctionMatch | undefined>;
41
+ }
42
+
43
+ export { type FunctionMatch, FunctionsHandler };
package/dist/main.js ADDED
@@ -0,0 +1,1112 @@
1
+ // src/main.ts
2
+ import { Buffer as Buffer2 } from "buffer";
3
+
4
+ // src/registry.ts
5
+ import { stat } from "fs/promises";
6
+ import { createRequire as createRequire2 } from "module";
7
+ import { basename as basename2, extname as extname2, isAbsolute, join, resolve } from "path";
8
+ import { env } from "process";
9
+ import { watchDebounced } from "@netlify/dev-utils";
10
+ import { listFunctions } from "@netlify/zip-it-and-ship-it";
11
+ import extractZip from "extract-zip";
12
+
13
+ // src/function.ts
14
+ import { basename, extname } from "path";
15
+ import { version as nodeVersion } from "process";
16
+ import { headers as netlifyHeaders, renderFunctionErrorPage } from "@netlify/dev-utils";
17
+ import CronParser from "cron-parser";
18
+ import semver from "semver";
19
+ var BACKGROUND_FUNCTION_SUFFIX = "-background";
20
+ var TYPESCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".cts", ".mts", ".ts"]);
21
+ var V2_MIN_NODE_VERSION = "18.14.0";
22
+ var difference = (setA, setB) => new Set([...setA].filter((item) => !setB.has(item)));
23
+ var getNextRun = function(schedule) {
24
+ const cron = CronParser.parseExpression(schedule, {
25
+ tz: "Etc/UTC"
26
+ });
27
+ return cron.next().toDate();
28
+ };
29
+ var getBlobsEventProperty = (context) => ({
30
+ primary_region: context.primaryRegion,
31
+ url: context.edgeURL,
32
+ url_uncached: context.edgeURL,
33
+ token: context.token
34
+ });
35
+ var NetlifyFunction = class {
36
+ name;
37
+ mainFile;
38
+ displayName;
39
+ schedule;
40
+ runtime;
41
+ blobsContext;
42
+ config;
43
+ directory;
44
+ projectRoot;
45
+ settings;
46
+ timeoutBackground;
47
+ timeoutSynchronous;
48
+ // Determines whether this is a background function based on the function
49
+ // name.
50
+ isBackground;
51
+ buildQueue;
52
+ buildData;
53
+ buildError = null;
54
+ // List of the function's source files. This starts out as an empty set
55
+ // and will get populated on every build.
56
+ srcFiles = /* @__PURE__ */ new Set();
57
+ excludedRoutes;
58
+ routes;
59
+ constructor({
60
+ blobsContext,
61
+ config,
62
+ directory,
63
+ displayName,
64
+ excludedRoutes,
65
+ mainFile,
66
+ name,
67
+ projectRoot,
68
+ routes,
69
+ runtime,
70
+ settings,
71
+ timeoutBackground,
72
+ timeoutSynchronous
73
+ }) {
74
+ this.blobsContext = blobsContext;
75
+ this.config = config;
76
+ this.directory = directory;
77
+ this.excludedRoutes = excludedRoutes;
78
+ this.mainFile = mainFile;
79
+ this.name = name;
80
+ this.displayName = displayName ?? name;
81
+ this.projectRoot = projectRoot;
82
+ this.routes = routes;
83
+ this.runtime = runtime;
84
+ this.timeoutBackground = timeoutBackground;
85
+ this.timeoutSynchronous = timeoutSynchronous;
86
+ this.settings = settings;
87
+ this.isBackground = name.endsWith(BACKGROUND_FUNCTION_SUFFIX);
88
+ const functionConfig = config.functions?.[name];
89
+ this.schedule = functionConfig?.schedule;
90
+ this.srcFiles = /* @__PURE__ */ new Set();
91
+ }
92
+ get filename() {
93
+ if (!this.buildData?.mainFile) {
94
+ return null;
95
+ }
96
+ return basename(this.buildData.mainFile);
97
+ }
98
+ getRecommendedExtension() {
99
+ if (this.buildData?.runtimeAPIVersion !== 2) {
100
+ return;
101
+ }
102
+ const extension = this.buildData?.mainFile ? extname(this.buildData.mainFile) : void 0;
103
+ const moduleFormat = this.buildData?.outputModuleFormat;
104
+ if (moduleFormat === "esm") {
105
+ return;
106
+ }
107
+ if (extension === ".ts") {
108
+ return ".mts";
109
+ }
110
+ if (extension === ".js") {
111
+ return ".mjs";
112
+ }
113
+ }
114
+ hasValidName() {
115
+ return /^[A-Za-z0-9_-]+$/.test(this.name);
116
+ }
117
+ async isScheduled() {
118
+ await this.buildQueue;
119
+ return Boolean(this.schedule);
120
+ }
121
+ isSupported() {
122
+ return !(this.buildData?.runtimeAPIVersion === 2 && semver.lt(nodeVersion, V2_MIN_NODE_VERSION));
123
+ }
124
+ isTypeScript() {
125
+ if (this.filename === null) {
126
+ return false;
127
+ }
128
+ return TYPESCRIPT_EXTENSIONS.has(extname(this.filename));
129
+ }
130
+ async getNextRun() {
131
+ if (!await this.isScheduled()) {
132
+ return null;
133
+ }
134
+ return getNextRun(this.schedule);
135
+ }
136
+ // The `build` method transforms source files into invocable functions. Its
137
+ // return value is an object with:
138
+ //
139
+ // - `srcFilesDiff`: Files that were added and removed since the last time
140
+ // the function was built.
141
+ async build({ buildDirectory, cache }) {
142
+ this.buildQueue = this.runtime.getBuildFunction({
143
+ config: this.config,
144
+ directory: this.directory,
145
+ func: this,
146
+ projectRoot: this.projectRoot,
147
+ targetDirectory: buildDirectory
148
+ }).then((buildFunction2) => buildFunction2({ cache }));
149
+ try {
150
+ const buildData = await this.buildQueue;
151
+ if (buildData === void 0) {
152
+ throw new Error(`Could not build function ${this.name}`);
153
+ }
154
+ const { includedFiles = [], routes, schedule, srcFiles } = buildData;
155
+ const srcFilesSet = new Set(srcFiles);
156
+ const srcFilesDiff = this.getSrcFilesDiff(srcFilesSet);
157
+ this.buildData = buildData;
158
+ this.buildError = null;
159
+ this.routes = routes;
160
+ this.srcFiles = srcFilesSet;
161
+ this.schedule = schedule || this.schedule;
162
+ if (!this.isSupported()) {
163
+ throw new Error(
164
+ `Function requires Node.js version ${V2_MIN_NODE_VERSION} or above, but ${nodeVersion.slice(
165
+ 1
166
+ )} is installed. Refer to https://ntl.fyi/functions-runtime for information on how to update.`
167
+ );
168
+ }
169
+ return { includedFiles, srcFilesDiff };
170
+ } catch (error) {
171
+ if (error instanceof Error) {
172
+ this.buildError = error;
173
+ }
174
+ return { error };
175
+ }
176
+ }
177
+ formatError(rawError, acceptsHTML) {
178
+ const error = this.normalizeError(rawError);
179
+ if (acceptsHTML) {
180
+ return JSON.stringify({
181
+ ...error,
182
+ stackTrace: void 0,
183
+ trace: error.stackTrace
184
+ });
185
+ }
186
+ return `${error.errorType}: ${error.errorMessage}
187
+ ${error.stackTrace.join("\n")}`;
188
+ }
189
+ async getBuildData() {
190
+ await this.buildQueue;
191
+ return this.buildData;
192
+ }
193
+ // Compares a new set of source files against a previous one, returning an
194
+ // object with two Sets, one with added and the other with deleted files.
195
+ getSrcFilesDiff(newSrcFiles) {
196
+ const added = difference(newSrcFiles, this.srcFiles);
197
+ const deleted = difference(this.srcFiles, newSrcFiles);
198
+ return {
199
+ added,
200
+ deleted
201
+ };
202
+ }
203
+ async handleError(rawError, acceptsHTML) {
204
+ const errorString = typeof rawError === "string" ? rawError : this.formatError(rawError, acceptsHTML);
205
+ const status = 500;
206
+ if (acceptsHTML) {
207
+ const body = await renderFunctionErrorPage(errorString, "function");
208
+ return new Response(body, {
209
+ headers: {
210
+ "Content-Type": "text/html"
211
+ },
212
+ status
213
+ });
214
+ }
215
+ return new Response(errorString, { status });
216
+ }
217
+ // Invokes the function and returns its response object.
218
+ async invoke({ buildCache = {}, buildDirectory, clientContext = {}, request, route }) {
219
+ if (buildDirectory) {
220
+ await this.build({ buildDirectory, cache: buildCache });
221
+ } else {
222
+ await this.buildQueue;
223
+ }
224
+ if (this.buildError) {
225
+ throw this.buildError;
226
+ }
227
+ const timeout = this.isBackground ? this.timeoutBackground : this.timeoutSynchronous;
228
+ const environment = {};
229
+ if (this.blobsContext) {
230
+ const payload = JSON.stringify(getBlobsEventProperty(this.blobsContext));
231
+ request.headers.set(netlifyHeaders.BlobsInfo, Buffer.from(payload).toString("base64"));
232
+ }
233
+ try {
234
+ return await this.runtime.invokeFunction({
235
+ context: clientContext,
236
+ environment,
237
+ func: this,
238
+ request,
239
+ route,
240
+ timeout
241
+ });
242
+ } catch (error) {
243
+ const acceptsHTML = request.headers.get("accept")?.includes("text/html");
244
+ return await this.handleError(error, Boolean(acceptsHTML));
245
+ }
246
+ }
247
+ /**
248
+ * Matches all routes agains the incoming request. If a match is found, then the matched route is returned.
249
+ * @returns matched route
250
+ */
251
+ async matchURLPath(rawPath, method) {
252
+ let path2 = rawPath !== "/" && rawPath.endsWith("/") ? rawPath.slice(0, -1) : rawPath;
253
+ path2 = path2.toLowerCase();
254
+ const { excludedRoutes = [], routes = [] } = this;
255
+ const matchingRoute = routes.find((route) => {
256
+ if (route.methods && route.methods.length !== 0 && !route.methods.includes(method)) {
257
+ return false;
258
+ }
259
+ if ("literal" in route && route.literal !== void 0) {
260
+ return path2 === route.literal;
261
+ }
262
+ if ("expression" in route && route.expression !== void 0) {
263
+ const regex = new RegExp(route.expression);
264
+ return regex.test(path2);
265
+ }
266
+ return false;
267
+ });
268
+ if (!matchingRoute) {
269
+ return;
270
+ }
271
+ const isExcluded = excludedRoutes.some((excludedRoute) => {
272
+ if ("literal" in excludedRoute && excludedRoute.literal !== void 0) {
273
+ return path2 === excludedRoute.literal;
274
+ }
275
+ if ("expression" in excludedRoute && excludedRoute.expression !== void 0) {
276
+ const regex = new RegExp(excludedRoute.expression);
277
+ return regex.test(path2);
278
+ }
279
+ return false;
280
+ });
281
+ if (isExcluded) {
282
+ return;
283
+ }
284
+ return matchingRoute;
285
+ }
286
+ normalizeError(error) {
287
+ if (error instanceof Error) {
288
+ const normalizedError = {
289
+ errorMessage: error.message,
290
+ errorType: error.name,
291
+ stackTrace: error.stack ? error.stack.split("\n") : []
292
+ };
293
+ if ("code" in error && error.code === "ERR_REQUIRE_ESM") {
294
+ return {
295
+ ...normalizedError,
296
+ errorMessage: "a CommonJS file cannot import ES modules. Consider switching your function to ES modules. For more information, refer to https://ntl.fyi/functions-runtime."
297
+ };
298
+ }
299
+ return normalizedError;
300
+ }
301
+ const stackTrace = error.stackTrace.map((line) => ` at ${line}`);
302
+ return {
303
+ errorType: error.errorType,
304
+ errorMessage: error.errorMessage,
305
+ stackTrace
306
+ };
307
+ }
308
+ get runtimeAPIVersion() {
309
+ return this.buildData?.runtimeAPIVersion ?? 1;
310
+ }
311
+ setRoutes(routes) {
312
+ if (this.buildData) {
313
+ this.buildData.routes = routes;
314
+ }
315
+ }
316
+ get url() {
317
+ const port = this.settings.port || this.settings.functionsPort;
318
+ const protocol = this.settings.https ? "https" : "http";
319
+ const url = new URL(`/.netlify/functions/${this.name}`, `${protocol}://localhost:${port}`);
320
+ return url.href;
321
+ }
322
+ };
323
+
324
+ // src/runtimes/nodejs/index.ts
325
+ import { createConnection } from "net";
326
+ import { pathToFileURL } from "url";
327
+ import { Worker } from "worker_threads";
328
+ import lambdaLocal from "lambda-local";
329
+
330
+ // src/runtimes/nodejs/builder.ts
331
+ import { writeFile } from "fs/promises";
332
+ import { createRequire } from "module";
333
+ import path from "path";
334
+ import { memoize } from "@netlify/dev-utils";
335
+ import { zipFunction, listFunction } from "@netlify/zip-it-and-ship-it";
336
+ import decache from "decache";
337
+ import { readPackageUp } from "read-package-up";
338
+ import sourceMapSupport from "source-map-support";
339
+
340
+ // src/runtimes/nodejs/config.ts
341
+ var normalizeFunctionsConfig = ({
342
+ functionsConfig = {},
343
+ projectRoot,
344
+ siteEnv = {}
345
+ }) => Object.entries(functionsConfig).reduce(
346
+ (result, [pattern, config]) => ({
347
+ ...result,
348
+ [pattern]: {
349
+ externalNodeModules: config.external_node_modules,
350
+ includedFiles: config.included_files,
351
+ includedFilesBasePath: projectRoot,
352
+ ignoredNodeModules: config.ignored_node_modules,
353
+ nodeBundler: config.node_bundler === "esbuild" ? "esbuild_zisi" : config.node_bundler,
354
+ nodeVersion: siteEnv.AWS_LAMBDA_JS_RUNTIME,
355
+ processDynamicNodeImports: true,
356
+ schedule: config.schedule,
357
+ zipGo: true
358
+ }
359
+ }),
360
+ {}
361
+ );
362
+
363
+ // src/runtimes/nodejs/builder.ts
364
+ var require2 = createRequire(import.meta.url);
365
+ var addFunctionsConfigDefaults = (config) => ({
366
+ ...config,
367
+ "*": {
368
+ nodeSourcemap: true,
369
+ ...config["*"]
370
+ }
371
+ });
372
+ var buildFunction = async ({
373
+ cache,
374
+ config,
375
+ directory,
376
+ featureFlags,
377
+ func,
378
+ hasTypeModule,
379
+ projectRoot,
380
+ targetDirectory
381
+ }) => {
382
+ const zipOptions = {
383
+ archiveFormat: "none",
384
+ basePath: projectRoot,
385
+ config,
386
+ featureFlags: { ...featureFlags, zisi_functions_api_v2: true }
387
+ };
388
+ const functionDirectory = path.dirname(func.mainFile);
389
+ const entryPath = functionDirectory === directory ? func.mainFile : functionDirectory;
390
+ const buildResult = await memoize({
391
+ cache,
392
+ cacheKey: `zisi-${entryPath}`,
393
+ command: () => zipFunction(entryPath, targetDirectory, zipOptions)
394
+ });
395
+ if (!buildResult) {
396
+ return;
397
+ }
398
+ const {
399
+ entryFilename,
400
+ excludedRoutes,
401
+ includedFiles,
402
+ inputs,
403
+ mainFile,
404
+ outputModuleFormat,
405
+ path: functionPath,
406
+ routes,
407
+ runtimeAPIVersion,
408
+ schedule
409
+ } = buildResult;
410
+ const srcFiles = (inputs ?? []).filter((inputPath) => !inputPath.includes(`${path.sep}node_modules${path.sep}`));
411
+ const buildPath = path.join(functionPath, entryFilename);
412
+ if (hasTypeModule) {
413
+ await writeFile(
414
+ path.join(functionPath, `package.json`),
415
+ JSON.stringify({
416
+ type: "commonjs"
417
+ })
418
+ );
419
+ }
420
+ clearFunctionsCache(targetDirectory);
421
+ return {
422
+ buildPath,
423
+ excludedRoutes,
424
+ includedFiles,
425
+ outputModuleFormat,
426
+ mainFile,
427
+ routes,
428
+ runtimeAPIVersion,
429
+ srcFiles,
430
+ schedule,
431
+ targetDirectory
432
+ };
433
+ };
434
+ var parseFunctionForMetadata = async ({ config, mainFile, projectRoot }) => await listFunction(mainFile, {
435
+ config: netlifyConfigToZisiConfig(config.functions, projectRoot),
436
+ featureFlags: { zisi_functions_api_v2: true },
437
+ parseISC: true
438
+ });
439
+ var clearFunctionsCache = (functionsPath) => {
440
+ Object.keys(require2.cache).filter((key) => key.startsWith(functionsPath)).forEach(decache);
441
+ };
442
+ var netlifyConfigToZisiConfig = (functionsConfig, projectRoot) => addFunctionsConfigDefaults(normalizeFunctionsConfig({ functionsConfig, projectRoot }));
443
+ var getNoopBuilder = async ({ directory, func, metadata }) => {
444
+ const functionDirectory = path.dirname(func.mainFile);
445
+ const srcFiles = functionDirectory === directory ? [func.mainFile] : [functionDirectory];
446
+ const build = async () => ({
447
+ buildPath: "",
448
+ excludedRoutes: [],
449
+ includedFiles: [],
450
+ mainFile: func.mainFile,
451
+ outputModuleFormat: "cjs",
452
+ routes: [],
453
+ runtimeAPIVersion: func.runtimeAPIVersion,
454
+ schedule: metadata.schedule,
455
+ srcFiles
456
+ });
457
+ return {
458
+ build,
459
+ builderName: ""
460
+ };
461
+ };
462
+ var getZISIBuilder = async ({
463
+ config,
464
+ directory,
465
+ func,
466
+ metadata,
467
+ projectRoot,
468
+ targetDirectory
469
+ }) => {
470
+ const functionsConfig = netlifyConfigToZisiConfig(config.functions, projectRoot);
471
+ const packageJson = await readPackageUp({ cwd: path.dirname(func.mainFile) });
472
+ const hasTypeModule = Boolean(packageJson && packageJson.packageJson.type === "module");
473
+ const featureFlags = {};
474
+ if (metadata.runtimeAPIVersion === 2) {
475
+ featureFlags.zisi_pure_esm = true;
476
+ featureFlags.zisi_pure_esm_mjs = true;
477
+ } else {
478
+ const mustTranspile = [".mjs", ".ts", ".mts", ".cts"].includes(path.extname(func.mainFile));
479
+ const mustUseEsbuild = hasTypeModule || mustTranspile;
480
+ if (mustUseEsbuild && !functionsConfig["*"].nodeBundler) {
481
+ functionsConfig["*"].nodeBundler = "esbuild";
482
+ }
483
+ const { nodeBundler } = functionsConfig["*"];
484
+ const isUsingEsbuild = nodeBundler === "esbuild_zisi" || nodeBundler === "esbuild";
485
+ if (!isUsingEsbuild) {
486
+ return null;
487
+ }
488
+ }
489
+ sourceMapSupport.install();
490
+ return {
491
+ build: ({ cache = {} }) => buildFunction({
492
+ cache,
493
+ config: functionsConfig,
494
+ directory,
495
+ func,
496
+ projectRoot,
497
+ targetDirectory,
498
+ hasTypeModule,
499
+ featureFlags
500
+ }),
501
+ builderName: "zip-it-and-ship-it"
502
+ };
503
+ };
504
+
505
+ // src/runtimes/nodejs/lambda.ts
506
+ import { shouldBase64Encode } from "@netlify/dev-utils";
507
+ var headersObjectFromWebHeaders = (webHeaders) => {
508
+ const headers = {};
509
+ const multiValueHeaders = {};
510
+ webHeaders.forEach((value, key) => {
511
+ headers[key] = value;
512
+ multiValueHeaders[key] = value.split(",").map((value2) => value2.trim());
513
+ });
514
+ return {
515
+ headers,
516
+ multiValueHeaders
517
+ };
518
+ };
519
+ var webHeadersFromHeadersObject = (headersObject) => {
520
+ const headers = new Headers();
521
+ Object.entries(headersObject ?? {}).forEach(([name, value]) => {
522
+ if (value !== void 0) {
523
+ headers.set(name.toLowerCase(), value.toString());
524
+ }
525
+ });
526
+ return headers;
527
+ };
528
+ var lambdaEventFromWebRequest = async (request, route) => {
529
+ const url = new URL(request.url);
530
+ const queryStringParameters = {};
531
+ const multiValueQueryStringParameters = {};
532
+ url.searchParams.forEach((value, key) => {
533
+ queryStringParameters[key] = queryStringParameters[key] ? `${queryStringParameters[key]},${value}` : value;
534
+ multiValueQueryStringParameters[key] = [...multiValueQueryStringParameters[key] ?? [], value];
535
+ });
536
+ const { headers, multiValueHeaders } = headersObjectFromWebHeaders(request.headers);
537
+ const body = await request.text() || null;
538
+ return {
539
+ rawUrl: url.toString(),
540
+ rawQuery: url.search,
541
+ path: url.pathname,
542
+ httpMethod: request.method,
543
+ headers,
544
+ multiValueHeaders,
545
+ queryStringParameters,
546
+ multiValueQueryStringParameters,
547
+ body,
548
+ isBase64Encoded: shouldBase64Encode(request.headers.get("content-type") ?? ""),
549
+ route
550
+ };
551
+ };
552
+ var webResponseFromLambdaResponse = async (lambdaResponse) => {
553
+ return new Response(lambdaResponse.body, {
554
+ headers: webHeadersFromHeadersObject(lambdaResponse.headers),
555
+ status: lambdaResponse.statusCode
556
+ });
557
+ };
558
+
559
+ // src/runtimes/nodejs/index.ts
560
+ var BLOBS_CONTEXT_VARIABLE = "NETLIFY_BLOBS_CONTEXT";
561
+ lambdaLocal.getLogger().level = "alert";
562
+ var nodeJSRuntime = {
563
+ getBuildFunction: async ({ config, directory, func, projectRoot, targetDirectory }) => {
564
+ const metadata = await parseFunctionForMetadata({ mainFile: func.mainFile, config, projectRoot });
565
+ const zisiBuilder = await getZISIBuilder({ config, directory, func, metadata, projectRoot, targetDirectory });
566
+ if (zisiBuilder) {
567
+ return zisiBuilder.build;
568
+ }
569
+ const noopBuilder = await getNoopBuilder({ config, directory, func, metadata, projectRoot, targetDirectory });
570
+ return noopBuilder.build;
571
+ },
572
+ invokeFunction: async ({ context, environment, func, request, route, timeout }) => {
573
+ const event = await lambdaEventFromWebRequest(request, route);
574
+ const buildData = await func.getBuildData();
575
+ if (buildData?.runtimeAPIVersion !== 2) {
576
+ const lambdaResponse2 = await invokeFunctionDirectly({ context, event, func, timeout });
577
+ return webResponseFromLambdaResponse(lambdaResponse2);
578
+ }
579
+ const workerData = {
580
+ clientContext: JSON.stringify(context),
581
+ environment,
582
+ event,
583
+ // If a function builder has defined a `buildPath` property, we use it.
584
+ // Otherwise, we'll invoke the function's main file.
585
+ // Because we use import() we have to use file:// URLs for Windows.
586
+ entryFilePath: pathToFileURL(buildData?.buildPath ?? func.mainFile).href,
587
+ timeoutMs: timeout * 1e3
588
+ };
589
+ const worker = new Worker(workerURL, { workerData });
590
+ const lambdaResponse = await new Promise((resolve2, reject) => {
591
+ worker.on("message", (result) => {
592
+ if (result?.streamPort) {
593
+ const client = createConnection(
594
+ {
595
+ port: result.streamPort,
596
+ host: "localhost"
597
+ },
598
+ () => {
599
+ result.body = client;
600
+ resolve2(result);
601
+ }
602
+ );
603
+ client.on("error", reject);
604
+ } else {
605
+ resolve2(result);
606
+ }
607
+ });
608
+ worker.on("error", reject);
609
+ });
610
+ return webResponseFromLambdaResponse(lambdaResponse);
611
+ }
612
+ };
613
+ var workerURL = new URL("worker.js", import.meta.url);
614
+ var invokeFunctionDirectly = async ({
615
+ context,
616
+ event,
617
+ func,
618
+ timeout
619
+ }) => {
620
+ const buildData = await func.getBuildData();
621
+ const lambdaPath = buildData?.buildPath ?? func.mainFile;
622
+ const result = await lambdaLocal.execute({
623
+ clientContext: JSON.stringify(context),
624
+ environment: {
625
+ // We've set the Blobs context on the parent process, which means it will
626
+ // be available to the Lambda. This would be inconsistent with production
627
+ // where only V2 functions get the context injected. To fix it, unset the
628
+ // context variable before invoking the function.
629
+ // This has the side-effect of also removing the variable from `process.env`.
630
+ [BLOBS_CONTEXT_VARIABLE]: void 0
631
+ },
632
+ event,
633
+ lambdaPath,
634
+ timeoutMs: timeout * 1e3,
635
+ verboseLevel: 3,
636
+ esm: lambdaPath.endsWith(".mjs")
637
+ });
638
+ return result;
639
+ };
640
+
641
+ // src/runtimes/index.ts
642
+ var runtimes = {
643
+ js: nodeJSRuntime
644
+ };
645
+
646
+ // src/registry.ts
647
+ var DEFAULT_FUNCTION_URL_EXPRESSION = /^\/.netlify\/(functions|builders)\/([^/]+).*/;
648
+ var TYPES_PACKAGE = "@netlify/functions";
649
+ var FunctionsRegistry = class {
650
+ /**
651
+ * Context object for Netlify Blobs
652
+ */
653
+ blobsContext;
654
+ /**
655
+ * The functions held by the registry
656
+ */
657
+ functions = /* @__PURE__ */ new Map();
658
+ /**
659
+ * File watchers for function files. Maps function names to objects built
660
+ * by the `watchDebounced` utility.
661
+ */
662
+ functionWatchers = /* @__PURE__ */ new Map();
663
+ /**
664
+ * Keeps track of whether we've checked whether `TYPES_PACKAGE` is
665
+ * installed.
666
+ */
667
+ hasCheckedTypesPackage = false;
668
+ buildCache;
669
+ config;
670
+ debug;
671
+ destPath;
672
+ directoryWatchers;
673
+ handleEvent;
674
+ frameworksAPIFunctionsPath;
675
+ internalFunctionsPath;
676
+ manifest;
677
+ projectRoot;
678
+ timeouts;
679
+ settings;
680
+ watch;
681
+ constructor({
682
+ blobsContext,
683
+ config,
684
+ debug = false,
685
+ destPath,
686
+ eventHandler,
687
+ frameworksAPIFunctionsPath,
688
+ internalFunctionsPath,
689
+ manifest,
690
+ projectRoot,
691
+ settings,
692
+ timeouts,
693
+ watch
694
+ }) {
695
+ this.blobsContext = blobsContext;
696
+ this.config = config;
697
+ this.debug = debug;
698
+ this.destPath = destPath;
699
+ this.frameworksAPIFunctionsPath = frameworksAPIFunctionsPath;
700
+ this.handleEvent = eventHandler ?? (() => {
701
+ });
702
+ this.internalFunctionsPath = internalFunctionsPath;
703
+ this.projectRoot = projectRoot;
704
+ this.timeouts = timeouts;
705
+ this.settings = settings;
706
+ this.watch = watch === true;
707
+ this.buildCache = {};
708
+ this.directoryWatchers = /* @__PURE__ */ new Map();
709
+ this.manifest = manifest;
710
+ }
711
+ async checkTypesPackage() {
712
+ if (this.hasCheckedTypesPackage) {
713
+ return;
714
+ }
715
+ this.hasCheckedTypesPackage = true;
716
+ const require3 = createRequire2(this.projectRoot);
717
+ try {
718
+ require3.resolve(TYPES_PACKAGE, { paths: [this.projectRoot] });
719
+ } catch (error) {
720
+ if (error?.code === "MODULE_NOT_FOUND") {
721
+ this.handleEvent({ name: "FunctionMissingTypesPackageEvent" });
722
+ }
723
+ }
724
+ }
725
+ /**
726
+ * Builds a function and sets up the appropriate file watchers so that any
727
+ * changes will trigger another build.
728
+ */
729
+ async buildFunctionAndWatchFiles(func, firstLoad = false) {
730
+ if (!firstLoad) {
731
+ this.handleEvent({ function: func, name: "FunctionReloadingEvent" });
732
+ }
733
+ const {
734
+ error: buildError,
735
+ includedFiles,
736
+ srcFilesDiff
737
+ } = await func.build({ buildDirectory: this.destPath, cache: this.buildCache });
738
+ if (buildError) {
739
+ this.handleEvent({ function: func, name: "FunctionBuildErrorEvent" });
740
+ } else {
741
+ this.handleEvent({ firstLoad, function: func, name: "FunctionLoadedEvent" });
742
+ }
743
+ if (func.isTypeScript()) {
744
+ this.checkTypesPackage();
745
+ }
746
+ if (!srcFilesDiff) {
747
+ return;
748
+ }
749
+ if (!this.watch) {
750
+ return;
751
+ }
752
+ const watcher = this.functionWatchers.get(func.name);
753
+ if (watcher) {
754
+ srcFilesDiff.deleted.forEach((path2) => {
755
+ watcher.unwatch(path2);
756
+ });
757
+ srcFilesDiff.added.forEach((path2) => {
758
+ watcher.add(path2);
759
+ });
760
+ return;
761
+ }
762
+ if (srcFilesDiff.added.size !== 0) {
763
+ const filesToWatch = [...srcFilesDiff.added, ...includedFiles];
764
+ const newWatcher = await watchDebounced(filesToWatch, {
765
+ onChange: () => {
766
+ this.buildFunctionAndWatchFiles(func, false);
767
+ }
768
+ });
769
+ this.functionWatchers.set(func.name, newWatcher);
770
+ }
771
+ }
772
+ set eventHandler(handler) {
773
+ this.handleEvent = handler;
774
+ }
775
+ /**
776
+ * Returns a function by name.
777
+ */
778
+ get(name) {
779
+ return this.functions.get(name);
780
+ }
781
+ /**
782
+ * Looks for the first function that matches a given URL path. If a match is
783
+ * found, returns an object with the function and the route. If the URL path
784
+ * matches the default functions URL (i.e. can only be for a function) but no
785
+ * function with the given name exists, returns an object with the function
786
+ * and the route set to `null`. Otherwise, `undefined` is returned,
787
+ */
788
+ async getFunctionForURLPath(urlPath, method) {
789
+ const url = new URL(`http://localhost${urlPath}`);
790
+ const defaultURLMatch = DEFAULT_FUNCTION_URL_EXPRESSION.exec(url.pathname);
791
+ if (defaultURLMatch) {
792
+ const func = this.get(defaultURLMatch[2]);
793
+ if (!func) {
794
+ return { func: null, route: null };
795
+ }
796
+ const { routes = [] } = func;
797
+ if (routes.length !== 0) {
798
+ this.handleEvent({
799
+ function: func,
800
+ name: "FunctionNotInvokableOnPathEvent",
801
+ urlPath
802
+ });
803
+ return;
804
+ }
805
+ return { func, route: null };
806
+ }
807
+ for (const func of this.functions.values()) {
808
+ const route = await func.matchURLPath(url.pathname, method);
809
+ if (route) {
810
+ return { func, route };
811
+ }
812
+ }
813
+ }
814
+ isInternalFunction(func) {
815
+ if (this.internalFunctionsPath && func.mainFile.includes(this.internalFunctionsPath)) {
816
+ return true;
817
+ }
818
+ if (this.frameworksAPIFunctionsPath && func.mainFile.includes(this.frameworksAPIFunctionsPath)) {
819
+ return true;
820
+ }
821
+ return false;
822
+ }
823
+ /**
824
+ * Adds a function to the registry
825
+ */
826
+ async registerFunction(name, func, isReload = false) {
827
+ this.handleEvent({ function: func, name: "FunctionRegisteredEvent" });
828
+ if (extname2(func.mainFile) === ".zip") {
829
+ const unzippedDirectory = await this.unzipFunction(func);
830
+ const manifestEntry = (this.manifest?.functions || []).find((manifestFunc) => manifestFunc.name === func.name);
831
+ if (!manifestEntry) {
832
+ return;
833
+ }
834
+ if (this.debug) {
835
+ this.handleEvent({ function: func, name: "FunctionExtractedEvent" });
836
+ }
837
+ func.setRoutes(manifestEntry?.routes);
838
+ try {
839
+ const v2EntryPointPath = join(unzippedDirectory, "___netlify-entry-point.mjs");
840
+ await stat(v2EntryPointPath);
841
+ func.mainFile = v2EntryPointPath;
842
+ } catch {
843
+ func.mainFile = join(unzippedDirectory, basename2(manifestEntry.mainFile));
844
+ }
845
+ } else if (this.watch) {
846
+ this.buildFunctionAndWatchFiles(func, !isReload);
847
+ }
848
+ this.functions.set(name, func);
849
+ }
850
+ /**
851
+ * A proxy to zip-it-and-ship-it's `listFunctions` method. It exists just so
852
+ * that we can mock it in tests.
853
+ */
854
+ async listFunctions(...args) {
855
+ return await listFunctions(...args);
856
+ }
857
+ /**
858
+ * Takes a list of directories and scans for functions. It keeps tracks of
859
+ * any functions in those directories that we've previously seen, and takes
860
+ * care of registering and unregistering functions as they come and go.
861
+ */
862
+ async scan(relativeDirs) {
863
+ const directories = relativeDirs.filter((dir) => Boolean(dir)).map((dir) => isAbsolute(dir) ? dir : join(this.projectRoot, dir));
864
+ if (directories.length === 0) {
865
+ return;
866
+ }
867
+ const functions = await this.listFunctions(directories, {
868
+ featureFlags: {
869
+ buildRustSource: env.NETLIFY_EXPERIMENTAL_BUILD_RUST_SOURCE === "true"
870
+ },
871
+ configFileDirectories: [this.internalFunctionsPath].filter(Boolean),
872
+ config: this.config.functions,
873
+ parseISC: true
874
+ });
875
+ const ignoredFunctions = new Set(
876
+ functions.filter(
877
+ (func) => this.isInternalFunction(func) && this.functions.has(func.name) && !this.isInternalFunction(this.functions.get(func.name))
878
+ ).map((func) => func.name)
879
+ );
880
+ const deletedFunctions = [...this.functions.values()].filter((oldFunc) => {
881
+ const isFound = functions.some(
882
+ (newFunc) => ignoredFunctions.has(newFunc.name) || newFunc.name === oldFunc.name && newFunc.mainFile === oldFunc.mainFile
883
+ );
884
+ return !isFound;
885
+ });
886
+ await Promise.all(deletedFunctions.map((func) => this.unregisterFunction(func)));
887
+ const deletedFunctionNames = new Set(deletedFunctions.map((func) => func.name));
888
+ const addedFunctions = await Promise.all(
889
+ // zip-it-and-ship-it returns an array sorted based on which extension should have precedence,
890
+ // where the last ones precede the previous ones. This is why
891
+ // we reverse the array so we get the right functions precedence in the CLI.
892
+ functions.reverse().map(async ({ displayName, excludedRoutes, mainFile, name, routes, runtime: runtimeName }) => {
893
+ if (ignoredFunctions.has(name)) {
894
+ return;
895
+ }
896
+ const runtime = runtimes[runtimeName];
897
+ if (runtime === void 0) {
898
+ return;
899
+ }
900
+ if (this.functions.has(name)) {
901
+ return;
902
+ }
903
+ const directory = directories.find((directory2) => mainFile.startsWith(directory2));
904
+ if (directory === void 0) {
905
+ return;
906
+ }
907
+ const func = new NetlifyFunction({
908
+ blobsContext: this.blobsContext,
909
+ config: this.config,
910
+ directory,
911
+ displayName,
912
+ excludedRoutes,
913
+ mainFile,
914
+ name,
915
+ projectRoot: this.projectRoot,
916
+ routes,
917
+ runtime,
918
+ settings: this.settings,
919
+ timeoutBackground: this.timeouts.backgroundFunctions,
920
+ timeoutSynchronous: this.timeouts.syncFunctions
921
+ });
922
+ const isReload = deletedFunctionNames.has(name);
923
+ await this.registerFunction(name, func, isReload);
924
+ return func;
925
+ })
926
+ );
927
+ const addedFunctionNames = new Set(addedFunctions.filter(Boolean).map((func) => func?.name));
928
+ deletedFunctions.forEach(async (func) => {
929
+ if (addedFunctionNames.has(func.name)) {
930
+ return;
931
+ }
932
+ this.handleEvent({ function: func, name: "FunctionRemovedEvent" });
933
+ });
934
+ if (this.watch) {
935
+ await Promise.all(directories.map((path2) => this.setupDirectoryWatcher(path2)));
936
+ }
937
+ }
938
+ /**
939
+ * Creates a watcher that looks at files being added or removed from a
940
+ * functions directory. It doesn't care about files being changed, because
941
+ * those will be handled by each functions' watcher.
942
+ */
943
+ async setupDirectoryWatcher(directory) {
944
+ if (this.directoryWatchers.has(directory)) {
945
+ return;
946
+ }
947
+ const watcher = await watchDebounced(directory, {
948
+ depth: 1,
949
+ onAdd: () => {
950
+ this.scan([directory]);
951
+ },
952
+ onUnlink: () => {
953
+ this.scan([directory]);
954
+ }
955
+ });
956
+ this.directoryWatchers.set(directory, watcher);
957
+ }
958
+ /**
959
+ * Removes a function from the registry and closes its file watchers.
960
+ */
961
+ async unregisterFunction(func) {
962
+ const { name } = func;
963
+ this.functions.delete(name);
964
+ const watcher = this.functionWatchers.get(name);
965
+ if (watcher) {
966
+ await watcher.close();
967
+ }
968
+ this.functionWatchers.delete(name);
969
+ }
970
+ /**
971
+ * Takes a zipped function and extracts its contents to an internal directory.
972
+ */
973
+ async unzipFunction(func) {
974
+ const targetDirectory = resolve(this.projectRoot, this.destPath, ".unzipped", func.name);
975
+ await extractZip(func.mainFile, { dir: targetDirectory });
976
+ return targetDirectory;
977
+ }
978
+ };
979
+
980
+ // src/server/client-context.ts
981
+ import { jwtDecode } from "jwt-decode";
982
+ var buildClientContext = (headers) => {
983
+ if (!headers.authorization) return;
984
+ const parts = headers.authorization.split(" ");
985
+ if (parts.length !== 2 || parts[0] !== "Bearer") return;
986
+ const identity = {
987
+ url: "https://netlify-dev-locally-emulated-identity.netlify.app/.netlify/identity",
988
+ // {
989
+ // "source": "netlify dev",
990
+ // "testData": "NETLIFY_DEV_LOCALLY_EMULATED_IDENTITY"
991
+ // }
992
+ token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb3VyY2UiOiJuZXRsaWZ5IGRldiIsInRlc3REYXRhIjoiTkVUTElGWV9ERVZfTE9DQUxMWV9FTVVMQVRFRF9JREVOVElUWSJ9.2eSDqUOZAOBsx39FHFePjYj12k0LrxldvGnlvDu3GMI"
993
+ };
994
+ try {
995
+ const user = jwtDecode(parts[1]);
996
+ const netlifyContext = JSON.stringify({
997
+ identity,
998
+ user
999
+ });
1000
+ return {
1001
+ identity,
1002
+ user,
1003
+ custom: {
1004
+ netlify: Buffer.from(netlifyContext).toString("base64")
1005
+ }
1006
+ };
1007
+ } catch {
1008
+ }
1009
+ };
1010
+
1011
+ // src/main.ts
1012
+ var CLOCKWORK_USERAGENT = "Netlify Clockwork";
1013
+ var UNLINKED_SITE_MOCK_ID = "unlinked";
1014
+ var FunctionsHandler = class {
1015
+ accountID;
1016
+ buildCache;
1017
+ geolocation;
1018
+ globalBuildDirectory;
1019
+ registry;
1020
+ scan;
1021
+ siteID;
1022
+ constructor({ accountId, geolocation, siteId, userFunctionsPath, ...registryOptions }) {
1023
+ const registry = new FunctionsRegistry(registryOptions);
1024
+ this.accountID = accountId;
1025
+ this.buildCache = {};
1026
+ this.geolocation = geolocation;
1027
+ this.globalBuildDirectory = registryOptions.destPath;
1028
+ this.registry = registry;
1029
+ this.scan = registry.scan([userFunctionsPath]);
1030
+ this.siteID = siteId;
1031
+ }
1032
+ async invoke(request, route, func, buildDirectory) {
1033
+ let remoteAddress = request.headers.get("x-forwarded-for") || "";
1034
+ remoteAddress = remoteAddress.split(remoteAddress.includes(".") ? ":" : ",").pop()?.trim() ?? "";
1035
+ request.headers.set("x-nf-client-connection-ip", remoteAddress);
1036
+ if (this.accountID) {
1037
+ request.headers.set("x-nf-account-id", this.accountID);
1038
+ }
1039
+ request.headers.set("x-nf-site-id", this.siteID ?? UNLINKED_SITE_MOCK_ID);
1040
+ request.headers.set("x-nf-geo", Buffer2.from(JSON.stringify(this.geolocation)).toString("base64"));
1041
+ const { headers: headersObject } = headersObjectFromWebHeaders(request.headers);
1042
+ const clientContext = buildClientContext(headersObject) || {};
1043
+ if (func.isBackground) {
1044
+ await func.invoke({
1045
+ buildCache: this.buildCache,
1046
+ buildDirectory: buildDirectory ?? this.globalBuildDirectory,
1047
+ request,
1048
+ route
1049
+ });
1050
+ return new Response(null, { status: 202 });
1051
+ }
1052
+ if (await func.isScheduled()) {
1053
+ const newRequest = new Request(request, {
1054
+ ...request,
1055
+ method: "POST"
1056
+ });
1057
+ newRequest.headers.set("user-agent", CLOCKWORK_USERAGENT);
1058
+ newRequest.headers.set("x-nf-event", "schedule");
1059
+ return await func.invoke({
1060
+ buildCache: this.buildCache,
1061
+ buildDirectory: buildDirectory ?? this.globalBuildDirectory,
1062
+ clientContext,
1063
+ request: newRequest,
1064
+ route
1065
+ });
1066
+ }
1067
+ return await func.invoke({
1068
+ buildCache: this.buildCache,
1069
+ buildDirectory: buildDirectory ?? this.globalBuildDirectory,
1070
+ clientContext,
1071
+ request,
1072
+ route
1073
+ });
1074
+ }
1075
+ async match(request, buildDirectory) {
1076
+ await this.scan;
1077
+ const url = new URL(request.url);
1078
+ const match = await this.registry.getFunctionForURLPath(url.pathname, request.method);
1079
+ if (!match) {
1080
+ return;
1081
+ }
1082
+ const functionName = match?.func?.name;
1083
+ if (!functionName) {
1084
+ return;
1085
+ }
1086
+ const matchingRoute = match.route?.pattern;
1087
+ const func = this.registry.get(functionName);
1088
+ if (func === void 0) {
1089
+ return {
1090
+ handle: async () => new Response("Function not found...", {
1091
+ status: 404
1092
+ }),
1093
+ preferStatic: false
1094
+ };
1095
+ }
1096
+ if (!func.hasValidName()) {
1097
+ return {
1098
+ handle: async () => new Response("Function name should consist only of alphanumeric characters, hyphen & underscores.", {
1099
+ status: 400
1100
+ }),
1101
+ preferStatic: false
1102
+ };
1103
+ }
1104
+ return {
1105
+ handle: (request2) => this.invoke(request2, matchingRoute, func, buildDirectory),
1106
+ preferStatic: match.route?.prefer_static ?? false
1107
+ };
1108
+ }
1109
+ };
1110
+ export {
1111
+ FunctionsHandler
1112
+ };
package/dist/worker.js ADDED
@@ -0,0 +1,106 @@
1
+ // @ts-check
2
+ // This is a JavaScript file because we need to locate it at runtime using the
3
+ // `Worker` API and using a `.ts` complicates things. To make it type-safe,
4
+ // we use JSDoc annotations.
5
+ import { createServer } from 'node:net'
6
+ import process from 'node:process'
7
+ import { isMainThread, workerData, parentPort } from 'node:worker_threads'
8
+
9
+ import { isStream } from 'is-stream'
10
+ import lambdaLocal from 'lambda-local'
11
+ import sourceMapSupport from 'source-map-support'
12
+
13
+ // https://github.com/nodejs/undici/blob/a36e299d544863c5ade17d4090181be894366024/lib/web/fetch/constants.js#L6
14
+ const nullBodyStatus = new Set([101, 204, 205, 304])
15
+
16
+ /**
17
+ * @typedef HandlerResponse
18
+ * @type {import('../../../src/function/handler_response.js').HandlerResponse}
19
+ */
20
+
21
+ /**
22
+ * @typedef WorkerResult
23
+ * @type {HandlerResponse & { streamPort?: number }}
24
+ */
25
+
26
+ if (isMainThread) {
27
+ throw new Error(`Do not import "${import.meta.url}" in the main thread.`)
28
+ }
29
+
30
+ sourceMapSupport.install()
31
+
32
+ lambdaLocal.getLogger().level = 'alert'
33
+
34
+ const { clientContext, entryFilePath, environment = {}, event, timeoutMs } = workerData
35
+
36
+ // Injecting into the environment any properties passed in by the parent.
37
+ for (const key in environment) {
38
+ process.env[key] = environment[key]
39
+ }
40
+ const lambdaFunc = await import(entryFilePath)
41
+ const invocationResult = /** @type {HandlerResponse} */ (
42
+ await lambdaLocal.execute({
43
+ clientContext,
44
+ event,
45
+ lambdaFunc,
46
+ region: 'dev',
47
+ timeoutMs,
48
+ verboseLevel: 3,
49
+ })
50
+ )
51
+
52
+ /**
53
+ * When the result body is a stream and result status code allow to have a body,
54
+ * open up a http server that proxies back to the main thread and resolve with server port.
55
+ * Otherwise, resolve with undefined.
56
+ *
57
+ * @param {HandlerResponse} invocationResult
58
+ * @returns {Promise<number | undefined>}
59
+ */
60
+ async function getStreamPortForStreamingResponse(invocationResult) {
61
+ // if we don't have result or result's body is not a stream, we do not need a stream port
62
+ if (!invocationResult || !isStream(invocationResult.body)) {
63
+ return undefined
64
+ }
65
+
66
+ const { body } = invocationResult
67
+
68
+ delete invocationResult.body
69
+
70
+ // For streaming responses, lambda-local always returns a result with body stream.
71
+ // We need to discard it if result's status code does not allow response to have a body.
72
+ const shouldNotHaveABody = nullBodyStatus.has(invocationResult.statusCode)
73
+ if (shouldNotHaveABody) {
74
+ return undefined
75
+ }
76
+
77
+ // create a server that will proxy the body stream back to the main thread
78
+ return await new Promise((resolve, reject) => {
79
+ const server = createServer((socket) => {
80
+ body.pipe(socket).on('end', () => server.close())
81
+ })
82
+ server.on('error', (error) => {
83
+ reject(error)
84
+ })
85
+ server.listen({ port: 0, host: 'localhost' }, () => {
86
+ const address = server.address()
87
+
88
+ /** @type {number | undefined} */
89
+ let streamPort
90
+ if (address && typeof address !== 'string') {
91
+ streamPort = address.port
92
+ }
93
+
94
+ resolve(streamPort)
95
+ })
96
+ })
97
+ }
98
+
99
+ const streamPort = await getStreamPortForStreamingResponse(invocationResult)
100
+
101
+ if (parentPort) {
102
+ /** @type {WorkerResult} */
103
+ const message = { ...invocationResult, streamPort }
104
+
105
+ parentPort.postMessage(message)
106
+ }
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@netlify/functions-dev",
3
+ "main": "./dist/main.js",
4
+ "types": "./dist/main.d.ts",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "import": {
9
+ "types": "./dist/main.d.ts",
10
+ "default": "./dist/main.js"
11
+ }
12
+ }
13
+ },
14
+ "version": "1.0.0",
15
+ "description": "Local dev emulation of Netlify Functions",
16
+ "files": [
17
+ "dist/**/*.js",
18
+ "dist/**/*.mjs",
19
+ "dist/**/*.d.ts",
20
+ "dist/**/*.d.mts"
21
+ ],
22
+ "scripts": {
23
+ "dev": "tsup-node --watch",
24
+ "build": "tsup-node",
25
+ "prepack": "npm run build",
26
+ "test": "vitest run",
27
+ "test:dev": "vitest"
28
+ },
29
+ "keywords": [
30
+ "netlify",
31
+ "functions",
32
+ "serverless"
33
+ ],
34
+ "license": "MIT",
35
+ "repository": "netlify/primitives",
36
+ "bugs": {
37
+ "url": "https://github.com/netlify/primitives/issues"
38
+ },
39
+ "author": "Netlify Inc.",
40
+ "dependencies": {
41
+ "@netlify/blobs": "10.1.0",
42
+ "@netlify/dev-utils": "4.3.0",
43
+ "@netlify/functions": "5.0.0",
44
+ "@netlify/zip-it-and-ship-it": "^14.1.3",
45
+ "cron-parser": "^4.9.0",
46
+ "decache": "^4.6.2",
47
+ "extract-zip": "^2.0.1",
48
+ "is-stream": "^4.0.1",
49
+ "jwt-decode": "^4.0.0",
50
+ "lambda-local": "^2.2.0",
51
+ "read-package-up": "^11.0.0",
52
+ "semver": "^7.6.3",
53
+ "source-map-support": "^0.5.21"
54
+ },
55
+ "devDependencies": {
56
+ "@types/semver": "^7.5.8",
57
+ "@types/source-map-support": "^0.5.10",
58
+ "npm-run-all2": "^5.0.0",
59
+ "tsup": "^8.0.2",
60
+ "vitest": "^3.0.0"
61
+ },
62
+ "engines": {
63
+ "node": ">=20.6.1"
64
+ }
65
+ }