@netlify/dev 3.0.0 → 4.0.1

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.cjs CHANGED
@@ -38,7 +38,8 @@ var import_node_path2 = __toESM(require("path"), 1);
38
38
  var import_node_process2 = __toESM(require("process"), 1);
39
39
  var import_config = require("@netlify/config");
40
40
  var import_dev_utils = require("@netlify/dev-utils");
41
- var import_dev = require("@netlify/functions/dev");
41
+ var import_dev = require("@netlify/edge-functions/dev");
42
+ var import_dev2 = require("@netlify/functions/dev");
42
43
  var import_headers = require("@netlify/headers");
43
44
  var import_redirects = require("@netlify/redirects");
44
45
  var import_static = require("@netlify/static");
@@ -54,7 +55,7 @@ var injectEnvVariables = async ({
54
55
  }) => {
55
56
  const results = {};
56
57
  let variables = baseVariables;
57
- if (netlifyAPI && accountSlug) {
58
+ if (netlifyAPI && siteID && accountSlug) {
58
59
  variables = await getEnvelopeEnv({
59
60
  accountId: accountSlug,
60
61
  api: netlifyAPI,
@@ -70,7 +71,8 @@ var injectEnvVariables = async ({
70
71
  isInternal,
71
72
  originalValue: envAPI.get(key),
72
73
  overriddenSources,
73
- usedSource
74
+ usedSource,
75
+ value: variable.value
74
76
  };
75
77
  if (!existsInProcess || isInternal) {
76
78
  envAPI.set(key, variable.value);
@@ -195,6 +197,10 @@ var isFile = async (path3) => {
195
197
  return false;
196
198
  };
197
199
 
200
+ // src/lib/request_id.ts
201
+ var import_ulid = require("ulid");
202
+ var generateRequestID = () => (0, import_ulid.ulid)();
203
+
198
204
  // src/lib/runtime.ts
199
205
  var import_node_path = __toESM(require("path"), 1);
200
206
  var import_node_process = __toESM(require("process"), 1);
@@ -261,93 +267,98 @@ var NetlifyDev = class {
261
267
  #apiHost;
262
268
  #apiScheme;
263
269
  #apiToken;
270
+ #cleanupJobs;
271
+ #edgeFunctionsHandler;
272
+ #functionsHandler;
273
+ #functionsServePath;
264
274
  #config;
265
275
  #features;
276
+ #headersHandler;
266
277
  #logger;
267
278
  #projectRoot;
268
- #runtime;
279
+ #redirectsHandler;
280
+ #server;
269
281
  #siteID;
282
+ #staticHandler;
283
+ #staticHandlerAdditionalDirectories;
270
284
  constructor(options) {
271
285
  if (options.apiURL) {
272
286
  const apiURL = new URL(options.apiURL);
273
287
  this.#apiHost = apiURL.host;
274
288
  this.#apiScheme = apiURL.protocol.slice(0, -1);
275
289
  }
290
+ const projectRoot = options.projectRoot ?? import_node_process2.default.cwd();
276
291
  this.#apiToken = options.apiToken;
292
+ this.#cleanupJobs = [];
277
293
  this.#features = {
278
294
  blobs: options.blobs?.enabled !== false,
295
+ edgeFunctions: options.edgeFunctions?.enabled !== false,
279
296
  environmentVariables: options.environmentVariables?.enabled !== false,
280
297
  functions: options.functions?.enabled !== false,
281
298
  headers: options.headers?.enabled !== false,
282
299
  redirects: options.redirects?.enabled !== false,
283
300
  static: options.staticFiles?.enabled !== false
284
301
  };
302
+ this.#functionsServePath = import_node_path2.default.join(projectRoot, ".netlify", "functions-serve");
285
303
  this.#logger = options.logger ?? globalThis.console;
286
- this.#projectRoot = options.projectRoot ?? import_node_process2.default.cwd();
304
+ this.#server = options.serverAddress;
305
+ this.#projectRoot = projectRoot;
306
+ this.#staticHandlerAdditionalDirectories = options.staticFiles?.directories ?? [];
287
307
  }
288
- async handleInEphemeralDirectory(request, destPath) {
289
- const userFunctionsPath = this.#config?.config.functionsDirectory ?? import_node_path2.default.join(this.#projectRoot, "netlify/functions");
290
- const userFunctionsPathExists = await isDirectory(userFunctionsPath);
291
- const functions = this.#features.functions ? new import_dev.FunctionsHandler({
292
- config: this.#config,
293
- destPath,
294
- projectRoot: this.#projectRoot,
295
- settings: {},
296
- siteId: this.#siteID,
297
- timeouts: {},
298
- userFunctionsPath: userFunctionsPathExists ? userFunctionsPath : void 0
299
- }) : null;
300
- const headers = this.#features.headers ? new import_headers.HeadersHandler({
301
- configPath: this.#config?.configPath,
302
- configHeaders: this.#config?.config.headers,
303
- projectDir: this.#projectRoot,
304
- publishDir: this.#config?.config.build.publish ?? void 0,
305
- logger: this.#logger
306
- }) : { handle: async (_request, response) => response };
307
- const redirects = this.#features.redirects ? new import_redirects.RedirectsHandler({
308
- configPath: this.#config?.configPath,
309
- configRedirects: this.#config?.config.redirects,
310
- jwtRoleClaim: "",
311
- jwtSecret: "",
312
- notFoundHandler,
313
- projectDir: this.#projectRoot
314
- }) : null;
315
- const staticFiles = this.#features.static ? new import_static.StaticHandler({
316
- directory: this.#config?.config.build.publish ?? this.#projectRoot
317
- }) : null;
318
- const functionMatch = await functions?.match(request);
308
+ async handleInEphemeralDirectory(request, destPath, options = {}) {
309
+ const edgeFunctionResponse = await this.#edgeFunctionsHandler?.handle(request.clone());
310
+ if (edgeFunctionResponse) {
311
+ return { response: edgeFunctionResponse, type: "edge-function" };
312
+ }
313
+ const functionMatch = await this.#functionsHandler?.match(request, destPath);
319
314
  if (functionMatch) {
320
315
  if (functionMatch.preferStatic) {
321
- const staticMatch2 = await staticFiles?.match(request);
316
+ const staticMatch2 = await this.#staticHandler?.match(request);
322
317
  if (staticMatch2) {
323
318
  const response = await staticMatch2.handle();
324
- return headers.handle(request, response);
319
+ await this.#headersHandler?.apply(request, response, options.headersCollector);
320
+ return { response, type: "static" };
325
321
  }
326
322
  }
327
- return functionMatch.handle(request);
323
+ return { response: await functionMatch.handle(request), type: "function" };
328
324
  }
329
- const redirectMatch = await redirects?.match(request);
325
+ const redirectMatch = await this.#redirectsHandler?.match(request);
330
326
  if (redirectMatch) {
331
- const functionMatch2 = await functions?.match(new Request(redirectMatch.target));
327
+ const functionMatch2 = await this.#functionsHandler?.match(new Request(redirectMatch.target), destPath);
332
328
  if (functionMatch2 && !functionMatch2.preferStatic) {
333
- return functionMatch2.handle(request);
329
+ return { response: await functionMatch2.handle(request), type: "function" };
334
330
  }
335
- const response = await redirects?.handle(request, redirectMatch, async (maybeStaticFile) => {
336
- const staticMatch2 = await staticFiles?.match(maybeStaticFile);
337
- if (!staticMatch2) return;
338
- return async () => {
339
- const response2 = await staticMatch2.handle();
340
- return headers.handle(new Request(redirectMatch.target), response2);
341
- };
342
- });
331
+ const response = await this.#redirectsHandler?.handle(
332
+ request,
333
+ redirectMatch,
334
+ async (maybeStaticFile) => {
335
+ const staticMatch2 = await this.#staticHandler?.match(maybeStaticFile);
336
+ if (!staticMatch2) {
337
+ return;
338
+ }
339
+ return async () => {
340
+ const response2 = await staticMatch2.handle();
341
+ await this.#headersHandler?.apply(new Request(redirectMatch.target), response2, options.headersCollector);
342
+ return response2;
343
+ };
344
+ }
345
+ );
343
346
  if (response) {
344
- return response;
347
+ return { response, type: "redirect" };
345
348
  }
346
349
  }
347
- const staticMatch = await staticFiles?.match(request);
350
+ const { pathname } = new URL(request.url);
351
+ if (pathname.startsWith("/.netlify/images")) {
352
+ this.#logger.error(
353
+ "The Netlify Image CDN is currently only supported in the Netlify CLI. Run `npx netlify dev` to get started."
354
+ );
355
+ return;
356
+ }
357
+ const staticMatch = await this.#staticHandler?.match(request);
348
358
  if (staticMatch) {
349
359
  const response = await staticMatch.handle();
350
- return headers.handle(request, response);
360
+ await this.#headersHandler?.apply(request, response, options.headersCollector);
361
+ return { response, type: "static" };
351
362
  }
352
363
  }
353
364
  async getConfig() {
@@ -367,12 +378,17 @@ var NetlifyDev = class {
367
378
  });
368
379
  return config;
369
380
  }
370
- async handle(request) {
371
- const servePath = import_node_path2.default.join(this.#projectRoot, ".netlify", "functions-serve");
372
- await import_node_fs2.promises.mkdir(servePath, { recursive: true });
373
- const destPath = await import_node_fs2.promises.mkdtemp(import_node_path2.default.join(servePath, "_"));
381
+ async handle(request, options = {}) {
382
+ const result = await this.handleAndIntrospect(request, options);
383
+ return result?.response;
384
+ }
385
+ async handleAndIntrospect(request, options = {}) {
386
+ const requestID = generateRequestID();
387
+ request.headers.set("x-nf-request-id", requestID);
388
+ await import_node_fs2.promises.mkdir(this.#functionsServePath, { recursive: true });
389
+ const destPath = await import_node_fs2.promises.mkdtemp(import_node_path2.default.join(this.#functionsServePath, `${requestID}_`));
374
390
  try {
375
- return await this.handleInEphemeralDirectory(request, destPath);
391
+ return await this.handleInEphemeralDirectory(request, destPath, options);
376
392
  } finally {
377
393
  try {
378
394
  await import_node_fs2.promises.rm(destPath, { force: true, recursive: true });
@@ -397,9 +413,21 @@ var NetlifyDev = class {
397
413
  projectRoot: this.#projectRoot,
398
414
  siteID: siteID ?? "0"
399
415
  });
400
- this.#runtime = runtime;
401
- if (this.#features.environmentVariables && siteID) {
402
- await injectEnvVariables({
416
+ this.#cleanupJobs.push(() => runtime.stop());
417
+ let serverAddress;
418
+ if (typeof this.#server === "string") {
419
+ serverAddress = this.#server;
420
+ } else if (this.#features.edgeFunctions) {
421
+ const passthroughServer = new import_dev_utils.HTTPServer(async (req) => {
422
+ const res = await this.handle(req);
423
+ return res ?? new Response(null, { status: 404 });
424
+ });
425
+ this.#cleanupJobs.push(() => passthroughServer.stop());
426
+ serverAddress = await passthroughServer.start();
427
+ }
428
+ let envVariables = {};
429
+ if (this.#features.environmentVariables) {
430
+ envVariables = await injectEnvVariables({
403
431
  accountSlug: config?.siteInfo?.account_slug,
404
432
  baseVariables: config?.env || {},
405
433
  envAPI: runtime.env,
@@ -407,9 +435,74 @@ var NetlifyDev = class {
407
435
  siteID
408
436
  });
409
437
  }
438
+ if (this.#features.edgeFunctions && serverAddress !== void 0) {
439
+ const env = Object.entries(envVariables).reduce((acc, [key, variable]) => {
440
+ if (variable.usedSource === "account" || variable.usedSource === "addons" || variable.usedSource === "internal" || variable.usedSource === "ui" || variable.usedSource.startsWith(".env")) {
441
+ return {
442
+ ...acc,
443
+ [key]: variable.value
444
+ };
445
+ }
446
+ return acc;
447
+ }, {});
448
+ this.#edgeFunctionsHandler = new import_dev.EdgeFunctionsHandler({
449
+ configDeclarations: this.#config?.config.edge_functions ?? [],
450
+ directories: [this.#config?.config.build.edge_functions].filter(Boolean),
451
+ env,
452
+ geolocation: import_dev_utils.mockLocation,
453
+ logger: this.#logger,
454
+ originServerAddress: serverAddress,
455
+ siteID,
456
+ siteName: config?.siteInfo.name
457
+ });
458
+ }
459
+ if (this.#features.functions) {
460
+ const userFunctionsPath = this.#config?.config.functionsDirectory ?? import_node_path2.default.join(this.#projectRoot, "netlify/functions");
461
+ const userFunctionsPathExists = await isDirectory(userFunctionsPath);
462
+ this.#functionsHandler = new import_dev2.FunctionsHandler({
463
+ config: this.#config,
464
+ destPath: this.#functionsServePath,
465
+ geolocation: import_dev_utils.mockLocation,
466
+ projectRoot: this.#projectRoot,
467
+ settings: {},
468
+ siteId: this.#siteID,
469
+ timeouts: {},
470
+ userFunctionsPath: userFunctionsPathExists ? userFunctionsPath : void 0
471
+ });
472
+ }
473
+ if (this.#features.headers) {
474
+ this.#headersHandler = new import_headers.HeadersHandler({
475
+ configPath: this.#config?.configPath,
476
+ configHeaders: this.#config?.config.headers,
477
+ projectDir: this.#projectRoot,
478
+ publishDir: this.#config?.config.build.publish ?? void 0,
479
+ logger: this.#logger
480
+ });
481
+ }
482
+ if (this.#features.redirects) {
483
+ this.#redirectsHandler = new import_redirects.RedirectsHandler({
484
+ configPath: this.#config?.configPath,
485
+ configRedirects: this.#config?.config.redirects,
486
+ jwtRoleClaim: "",
487
+ jwtSecret: "",
488
+ notFoundHandler,
489
+ projectDir: this.#projectRoot
490
+ });
491
+ }
492
+ if (this.#features.static) {
493
+ this.#staticHandler = new import_static.StaticHandler({
494
+ directory: [
495
+ this.#config?.config.build.publish ?? this.#projectRoot,
496
+ ...this.#staticHandlerAdditionalDirectories
497
+ ]
498
+ });
499
+ }
500
+ return {
501
+ serverAddress
502
+ };
410
503
  }
411
504
  async stop() {
412
- await this.#runtime?.stop();
505
+ await Promise.allSettled(this.#cleanupJobs.map((task) => task()));
413
506
  }
414
507
  };
415
508
  // Annotate the CommonJS export names for ESM import in node:
package/dist/main.d.cts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Logger } from '@netlify/dev-utils';
2
+ import { HeadersCollector } from '@netlify/headers';
2
3
 
3
4
  interface Features {
4
5
  /**
@@ -7,7 +8,15 @@ interface Features {
7
8
  * {@link} https://docs.netlify.com/blobs/overview/
8
9
  */
9
10
  blobs?: {
10
- enabled: boolean;
11
+ enabled?: boolean;
12
+ };
13
+ /**
14
+ * Configuration options for environment variables.
15
+ *
16
+ * {@link} https://docs.netlify.com/edge-functions/overview/
17
+ */
18
+ edgeFunctions?: {
19
+ enabled?: boolean;
11
20
  };
12
21
  /**
13
22
  * Configuration options for environment variables.
@@ -15,7 +24,7 @@ interface Features {
15
24
  * {@link} https://docs.netlify.com/environment-variables/overview/
16
25
  */
17
26
  environmentVariables?: {
18
- enabled: boolean;
27
+ enabled?: boolean;
19
28
  };
20
29
  /**
21
30
  * Configuration options for Netlify Functions.
@@ -23,7 +32,7 @@ interface Features {
23
32
  * {@link} https://docs.netlify.com/functions/overview/
24
33
  */
25
34
  functions?: {
26
- enabled: boolean;
35
+ enabled?: boolean;
27
36
  };
28
37
  /**
29
38
  * Configuration options for Netlify response headers.
@@ -31,7 +40,7 @@ interface Features {
31
40
  * {@link} https://docs.netlify.com/routing/headers/
32
41
  */
33
42
  headers?: {
34
- enabled: boolean;
43
+ enabled?: boolean;
35
44
  };
36
45
  /**
37
46
  * Configuration options for Netlify redirects and rewrites.
@@ -39,13 +48,23 @@ interface Features {
39
48
  * {@link} https://docs.netlify.com/routing/redirects/
40
49
  */
41
50
  redirects?: {
42
- enabled: boolean;
51
+ enabled?: boolean;
43
52
  };
53
+ /**
54
+ * If your local development setup has its own HTTP server (e.g. Vite), set
55
+ * its address here.
56
+ */
57
+ serverAddress?: string;
44
58
  /**
45
59
  * Configuration options for serving static files.
46
60
  */
47
61
  staticFiles?: {
48
- enabled: boolean;
62
+ enabled?: boolean;
63
+ /**
64
+ * Additional list of directories where static files can be found. The
65
+ * `publish` directory configured on your site will be used automatically.
66
+ */
67
+ directories?: string[];
49
68
  };
50
69
  }
51
70
  interface NetlifyDevOptions extends Features {
@@ -54,15 +73,31 @@ interface NetlifyDevOptions extends Features {
54
73
  logger?: Logger;
55
74
  projectRoot?: string;
56
75
  }
76
+ interface HandleOptions {
77
+ /**
78
+ * An optional callback that will be called with every header (key and value)
79
+ * coming from header rules.
80
+ *
81
+ * {@link} https://docs.netlify.com/routing/headers/
82
+ */
83
+ headersCollector?: HeadersCollector;
84
+ }
85
+ type ResponseType = 'edge-function' | 'function' | 'redirect' | 'static';
57
86
  declare class NetlifyDev {
58
87
  #private;
59
88
  constructor(options: NetlifyDevOptions);
60
89
  private handleInEphemeralDirectory;
61
90
  private getConfig;
62
- handle(request: Request): Promise<Response | undefined>;
91
+ handle(request: Request, options?: HandleOptions): Promise<Response | undefined>;
92
+ handleAndIntrospect(request: Request, options?: HandleOptions): Promise<{
93
+ response: Response;
94
+ type: ResponseType;
95
+ } | undefined>;
63
96
  get siteIsLinked(): boolean;
64
- start(): Promise<void>;
97
+ start(): Promise<{
98
+ serverAddress: string | undefined;
99
+ }>;
65
100
  stop(): Promise<void>;
66
101
  }
67
102
 
68
- export { type Features, NetlifyDev };
103
+ export { type Features, NetlifyDev, type ResponseType };
package/dist/main.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Logger } from '@netlify/dev-utils';
2
+ import { HeadersCollector } from '@netlify/headers';
2
3
 
3
4
  interface Features {
4
5
  /**
@@ -7,7 +8,15 @@ interface Features {
7
8
  * {@link} https://docs.netlify.com/blobs/overview/
8
9
  */
9
10
  blobs?: {
10
- enabled: boolean;
11
+ enabled?: boolean;
12
+ };
13
+ /**
14
+ * Configuration options for environment variables.
15
+ *
16
+ * {@link} https://docs.netlify.com/edge-functions/overview/
17
+ */
18
+ edgeFunctions?: {
19
+ enabled?: boolean;
11
20
  };
12
21
  /**
13
22
  * Configuration options for environment variables.
@@ -15,7 +24,7 @@ interface Features {
15
24
  * {@link} https://docs.netlify.com/environment-variables/overview/
16
25
  */
17
26
  environmentVariables?: {
18
- enabled: boolean;
27
+ enabled?: boolean;
19
28
  };
20
29
  /**
21
30
  * Configuration options for Netlify Functions.
@@ -23,7 +32,7 @@ interface Features {
23
32
  * {@link} https://docs.netlify.com/functions/overview/
24
33
  */
25
34
  functions?: {
26
- enabled: boolean;
35
+ enabled?: boolean;
27
36
  };
28
37
  /**
29
38
  * Configuration options for Netlify response headers.
@@ -31,7 +40,7 @@ interface Features {
31
40
  * {@link} https://docs.netlify.com/routing/headers/
32
41
  */
33
42
  headers?: {
34
- enabled: boolean;
43
+ enabled?: boolean;
35
44
  };
36
45
  /**
37
46
  * Configuration options for Netlify redirects and rewrites.
@@ -39,13 +48,23 @@ interface Features {
39
48
  * {@link} https://docs.netlify.com/routing/redirects/
40
49
  */
41
50
  redirects?: {
42
- enabled: boolean;
51
+ enabled?: boolean;
43
52
  };
53
+ /**
54
+ * If your local development setup has its own HTTP server (e.g. Vite), set
55
+ * its address here.
56
+ */
57
+ serverAddress?: string;
44
58
  /**
45
59
  * Configuration options for serving static files.
46
60
  */
47
61
  staticFiles?: {
48
- enabled: boolean;
62
+ enabled?: boolean;
63
+ /**
64
+ * Additional list of directories where static files can be found. The
65
+ * `publish` directory configured on your site will be used automatically.
66
+ */
67
+ directories?: string[];
49
68
  };
50
69
  }
51
70
  interface NetlifyDevOptions extends Features {
@@ -54,15 +73,31 @@ interface NetlifyDevOptions extends Features {
54
73
  logger?: Logger;
55
74
  projectRoot?: string;
56
75
  }
76
+ interface HandleOptions {
77
+ /**
78
+ * An optional callback that will be called with every header (key and value)
79
+ * coming from header rules.
80
+ *
81
+ * {@link} https://docs.netlify.com/routing/headers/
82
+ */
83
+ headersCollector?: HeadersCollector;
84
+ }
85
+ type ResponseType = 'edge-function' | 'function' | 'redirect' | 'static';
57
86
  declare class NetlifyDev {
58
87
  #private;
59
88
  constructor(options: NetlifyDevOptions);
60
89
  private handleInEphemeralDirectory;
61
90
  private getConfig;
62
- handle(request: Request): Promise<Response | undefined>;
91
+ handle(request: Request, options?: HandleOptions): Promise<Response | undefined>;
92
+ handleAndIntrospect(request: Request, options?: HandleOptions): Promise<{
93
+ response: Response;
94
+ type: ResponseType;
95
+ } | undefined>;
63
96
  get siteIsLinked(): boolean;
64
- start(): Promise<void>;
97
+ start(): Promise<{
98
+ serverAddress: string | undefined;
99
+ }>;
65
100
  stop(): Promise<void>;
66
101
  }
67
102
 
68
- export { type Features, NetlifyDev };
103
+ export { type Features, NetlifyDev, type ResponseType };
package/dist/main.js CHANGED
@@ -3,7 +3,8 @@ import { promises as fs2 } from "fs";
3
3
  import path2 from "path";
4
4
  import process2 from "process";
5
5
  import { resolveConfig } from "@netlify/config";
6
- import { ensureNetlifyIgnore, getAPIToken, LocalState } from "@netlify/dev-utils";
6
+ import { ensureNetlifyIgnore, getAPIToken, mockLocation, LocalState, HTTPServer } from "@netlify/dev-utils";
7
+ import { EdgeFunctionsHandler } from "@netlify/edge-functions/dev";
7
8
  import { FunctionsHandler } from "@netlify/functions/dev";
8
9
  import { HeadersHandler } from "@netlify/headers";
9
10
  import { RedirectsHandler } from "@netlify/redirects";
@@ -20,7 +21,7 @@ var injectEnvVariables = async ({
20
21
  }) => {
21
22
  const results = {};
22
23
  let variables = baseVariables;
23
- if (netlifyAPI && accountSlug) {
24
+ if (netlifyAPI && siteID && accountSlug) {
24
25
  variables = await getEnvelopeEnv({
25
26
  accountId: accountSlug,
26
27
  api: netlifyAPI,
@@ -36,7 +37,8 @@ var injectEnvVariables = async ({
36
37
  isInternal,
37
38
  originalValue: envAPI.get(key),
38
39
  overriddenSources,
39
- usedSource
40
+ usedSource,
41
+ value: variable.value
40
42
  };
41
43
  if (!existsInProcess || isInternal) {
42
44
  envAPI.set(key, variable.value);
@@ -161,6 +163,10 @@ var isFile = async (path3) => {
161
163
  return false;
162
164
  };
163
165
 
166
+ // src/lib/request_id.ts
167
+ import { ulid } from "ulid";
168
+ var generateRequestID = () => ulid();
169
+
164
170
  // src/lib/runtime.ts
165
171
  import path from "path";
166
172
  import process from "process";
@@ -227,93 +233,98 @@ var NetlifyDev = class {
227
233
  #apiHost;
228
234
  #apiScheme;
229
235
  #apiToken;
236
+ #cleanupJobs;
237
+ #edgeFunctionsHandler;
238
+ #functionsHandler;
239
+ #functionsServePath;
230
240
  #config;
231
241
  #features;
242
+ #headersHandler;
232
243
  #logger;
233
244
  #projectRoot;
234
- #runtime;
245
+ #redirectsHandler;
246
+ #server;
235
247
  #siteID;
248
+ #staticHandler;
249
+ #staticHandlerAdditionalDirectories;
236
250
  constructor(options) {
237
251
  if (options.apiURL) {
238
252
  const apiURL = new URL(options.apiURL);
239
253
  this.#apiHost = apiURL.host;
240
254
  this.#apiScheme = apiURL.protocol.slice(0, -1);
241
255
  }
256
+ const projectRoot = options.projectRoot ?? process2.cwd();
242
257
  this.#apiToken = options.apiToken;
258
+ this.#cleanupJobs = [];
243
259
  this.#features = {
244
260
  blobs: options.blobs?.enabled !== false,
261
+ edgeFunctions: options.edgeFunctions?.enabled !== false,
245
262
  environmentVariables: options.environmentVariables?.enabled !== false,
246
263
  functions: options.functions?.enabled !== false,
247
264
  headers: options.headers?.enabled !== false,
248
265
  redirects: options.redirects?.enabled !== false,
249
266
  static: options.staticFiles?.enabled !== false
250
267
  };
268
+ this.#functionsServePath = path2.join(projectRoot, ".netlify", "functions-serve");
251
269
  this.#logger = options.logger ?? globalThis.console;
252
- this.#projectRoot = options.projectRoot ?? process2.cwd();
270
+ this.#server = options.serverAddress;
271
+ this.#projectRoot = projectRoot;
272
+ this.#staticHandlerAdditionalDirectories = options.staticFiles?.directories ?? [];
253
273
  }
254
- async handleInEphemeralDirectory(request, destPath) {
255
- const userFunctionsPath = this.#config?.config.functionsDirectory ?? path2.join(this.#projectRoot, "netlify/functions");
256
- const userFunctionsPathExists = await isDirectory(userFunctionsPath);
257
- const functions = this.#features.functions ? new FunctionsHandler({
258
- config: this.#config,
259
- destPath,
260
- projectRoot: this.#projectRoot,
261
- settings: {},
262
- siteId: this.#siteID,
263
- timeouts: {},
264
- userFunctionsPath: userFunctionsPathExists ? userFunctionsPath : void 0
265
- }) : null;
266
- const headers = this.#features.headers ? new HeadersHandler({
267
- configPath: this.#config?.configPath,
268
- configHeaders: this.#config?.config.headers,
269
- projectDir: this.#projectRoot,
270
- publishDir: this.#config?.config.build.publish ?? void 0,
271
- logger: this.#logger
272
- }) : { handle: async (_request, response) => response };
273
- const redirects = this.#features.redirects ? new RedirectsHandler({
274
- configPath: this.#config?.configPath,
275
- configRedirects: this.#config?.config.redirects,
276
- jwtRoleClaim: "",
277
- jwtSecret: "",
278
- notFoundHandler,
279
- projectDir: this.#projectRoot
280
- }) : null;
281
- const staticFiles = this.#features.static ? new StaticHandler({
282
- directory: this.#config?.config.build.publish ?? this.#projectRoot
283
- }) : null;
284
- const functionMatch = await functions?.match(request);
274
+ async handleInEphemeralDirectory(request, destPath, options = {}) {
275
+ const edgeFunctionResponse = await this.#edgeFunctionsHandler?.handle(request.clone());
276
+ if (edgeFunctionResponse) {
277
+ return { response: edgeFunctionResponse, type: "edge-function" };
278
+ }
279
+ const functionMatch = await this.#functionsHandler?.match(request, destPath);
285
280
  if (functionMatch) {
286
281
  if (functionMatch.preferStatic) {
287
- const staticMatch2 = await staticFiles?.match(request);
282
+ const staticMatch2 = await this.#staticHandler?.match(request);
288
283
  if (staticMatch2) {
289
284
  const response = await staticMatch2.handle();
290
- return headers.handle(request, response);
285
+ await this.#headersHandler?.apply(request, response, options.headersCollector);
286
+ return { response, type: "static" };
291
287
  }
292
288
  }
293
- return functionMatch.handle(request);
289
+ return { response: await functionMatch.handle(request), type: "function" };
294
290
  }
295
- const redirectMatch = await redirects?.match(request);
291
+ const redirectMatch = await this.#redirectsHandler?.match(request);
296
292
  if (redirectMatch) {
297
- const functionMatch2 = await functions?.match(new Request(redirectMatch.target));
293
+ const functionMatch2 = await this.#functionsHandler?.match(new Request(redirectMatch.target), destPath);
298
294
  if (functionMatch2 && !functionMatch2.preferStatic) {
299
- return functionMatch2.handle(request);
295
+ return { response: await functionMatch2.handle(request), type: "function" };
300
296
  }
301
- const response = await redirects?.handle(request, redirectMatch, async (maybeStaticFile) => {
302
- const staticMatch2 = await staticFiles?.match(maybeStaticFile);
303
- if (!staticMatch2) return;
304
- return async () => {
305
- const response2 = await staticMatch2.handle();
306
- return headers.handle(new Request(redirectMatch.target), response2);
307
- };
308
- });
297
+ const response = await this.#redirectsHandler?.handle(
298
+ request,
299
+ redirectMatch,
300
+ async (maybeStaticFile) => {
301
+ const staticMatch2 = await this.#staticHandler?.match(maybeStaticFile);
302
+ if (!staticMatch2) {
303
+ return;
304
+ }
305
+ return async () => {
306
+ const response2 = await staticMatch2.handle();
307
+ await this.#headersHandler?.apply(new Request(redirectMatch.target), response2, options.headersCollector);
308
+ return response2;
309
+ };
310
+ }
311
+ );
309
312
  if (response) {
310
- return response;
313
+ return { response, type: "redirect" };
311
314
  }
312
315
  }
313
- const staticMatch = await staticFiles?.match(request);
316
+ const { pathname } = new URL(request.url);
317
+ if (pathname.startsWith("/.netlify/images")) {
318
+ this.#logger.error(
319
+ "The Netlify Image CDN is currently only supported in the Netlify CLI. Run `npx netlify dev` to get started."
320
+ );
321
+ return;
322
+ }
323
+ const staticMatch = await this.#staticHandler?.match(request);
314
324
  if (staticMatch) {
315
325
  const response = await staticMatch.handle();
316
- return headers.handle(request, response);
326
+ await this.#headersHandler?.apply(request, response, options.headersCollector);
327
+ return { response, type: "static" };
317
328
  }
318
329
  }
319
330
  async getConfig() {
@@ -333,12 +344,17 @@ var NetlifyDev = class {
333
344
  });
334
345
  return config;
335
346
  }
336
- async handle(request) {
337
- const servePath = path2.join(this.#projectRoot, ".netlify", "functions-serve");
338
- await fs2.mkdir(servePath, { recursive: true });
339
- const destPath = await fs2.mkdtemp(path2.join(servePath, "_"));
347
+ async handle(request, options = {}) {
348
+ const result = await this.handleAndIntrospect(request, options);
349
+ return result?.response;
350
+ }
351
+ async handleAndIntrospect(request, options = {}) {
352
+ const requestID = generateRequestID();
353
+ request.headers.set("x-nf-request-id", requestID);
354
+ await fs2.mkdir(this.#functionsServePath, { recursive: true });
355
+ const destPath = await fs2.mkdtemp(path2.join(this.#functionsServePath, `${requestID}_`));
340
356
  try {
341
- return await this.handleInEphemeralDirectory(request, destPath);
357
+ return await this.handleInEphemeralDirectory(request, destPath, options);
342
358
  } finally {
343
359
  try {
344
360
  await fs2.rm(destPath, { force: true, recursive: true });
@@ -363,9 +379,21 @@ var NetlifyDev = class {
363
379
  projectRoot: this.#projectRoot,
364
380
  siteID: siteID ?? "0"
365
381
  });
366
- this.#runtime = runtime;
367
- if (this.#features.environmentVariables && siteID) {
368
- await injectEnvVariables({
382
+ this.#cleanupJobs.push(() => runtime.stop());
383
+ let serverAddress;
384
+ if (typeof this.#server === "string") {
385
+ serverAddress = this.#server;
386
+ } else if (this.#features.edgeFunctions) {
387
+ const passthroughServer = new HTTPServer(async (req) => {
388
+ const res = await this.handle(req);
389
+ return res ?? new Response(null, { status: 404 });
390
+ });
391
+ this.#cleanupJobs.push(() => passthroughServer.stop());
392
+ serverAddress = await passthroughServer.start();
393
+ }
394
+ let envVariables = {};
395
+ if (this.#features.environmentVariables) {
396
+ envVariables = await injectEnvVariables({
369
397
  accountSlug: config?.siteInfo?.account_slug,
370
398
  baseVariables: config?.env || {},
371
399
  envAPI: runtime.env,
@@ -373,9 +401,74 @@ var NetlifyDev = class {
373
401
  siteID
374
402
  });
375
403
  }
404
+ if (this.#features.edgeFunctions && serverAddress !== void 0) {
405
+ const env = Object.entries(envVariables).reduce((acc, [key, variable]) => {
406
+ if (variable.usedSource === "account" || variable.usedSource === "addons" || variable.usedSource === "internal" || variable.usedSource === "ui" || variable.usedSource.startsWith(".env")) {
407
+ return {
408
+ ...acc,
409
+ [key]: variable.value
410
+ };
411
+ }
412
+ return acc;
413
+ }, {});
414
+ this.#edgeFunctionsHandler = new EdgeFunctionsHandler({
415
+ configDeclarations: this.#config?.config.edge_functions ?? [],
416
+ directories: [this.#config?.config.build.edge_functions].filter(Boolean),
417
+ env,
418
+ geolocation: mockLocation,
419
+ logger: this.#logger,
420
+ originServerAddress: serverAddress,
421
+ siteID,
422
+ siteName: config?.siteInfo.name
423
+ });
424
+ }
425
+ if (this.#features.functions) {
426
+ const userFunctionsPath = this.#config?.config.functionsDirectory ?? path2.join(this.#projectRoot, "netlify/functions");
427
+ const userFunctionsPathExists = await isDirectory(userFunctionsPath);
428
+ this.#functionsHandler = new FunctionsHandler({
429
+ config: this.#config,
430
+ destPath: this.#functionsServePath,
431
+ geolocation: mockLocation,
432
+ projectRoot: this.#projectRoot,
433
+ settings: {},
434
+ siteId: this.#siteID,
435
+ timeouts: {},
436
+ userFunctionsPath: userFunctionsPathExists ? userFunctionsPath : void 0
437
+ });
438
+ }
439
+ if (this.#features.headers) {
440
+ this.#headersHandler = new HeadersHandler({
441
+ configPath: this.#config?.configPath,
442
+ configHeaders: this.#config?.config.headers,
443
+ projectDir: this.#projectRoot,
444
+ publishDir: this.#config?.config.build.publish ?? void 0,
445
+ logger: this.#logger
446
+ });
447
+ }
448
+ if (this.#features.redirects) {
449
+ this.#redirectsHandler = new RedirectsHandler({
450
+ configPath: this.#config?.configPath,
451
+ configRedirects: this.#config?.config.redirects,
452
+ jwtRoleClaim: "",
453
+ jwtSecret: "",
454
+ notFoundHandler,
455
+ projectDir: this.#projectRoot
456
+ });
457
+ }
458
+ if (this.#features.static) {
459
+ this.#staticHandler = new StaticHandler({
460
+ directory: [
461
+ this.#config?.config.build.publish ?? this.#projectRoot,
462
+ ...this.#staticHandlerAdditionalDirectories
463
+ ]
464
+ });
465
+ }
466
+ return {
467
+ serverAddress
468
+ };
376
469
  }
377
470
  async stop() {
378
- await this.#runtime?.stop();
471
+ await Promise.allSettled(this.#cleanupJobs.map((task) => task()));
379
472
  }
380
473
  };
381
474
  export {
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@netlify/dev",
3
- "version": "3.0.0",
3
+ "version": "4.0.1",
4
4
  "description": "Emulation of the Netlify environment for local development",
5
5
  "type": "module",
6
6
  "engines": {
7
- "node": "^18.14.0 || >=20"
7
+ "node": ">=20.6.1"
8
8
  },
9
9
  "main": "./dist/main.cjs",
10
10
  "module": "./dist/main.js",
@@ -46,19 +46,21 @@
46
46
  },
47
47
  "author": "Netlify Inc.",
48
48
  "devDependencies": {
49
- "@netlify/api": "^14.0.2",
50
- "@netlify/types": "2.0.0",
49
+ "@netlify/api": "^14.0.3",
50
+ "@netlify/types": "2.0.1",
51
51
  "tsup": "^8.0.0",
52
52
  "vitest": "^3.0.0"
53
53
  },
54
54
  "dependencies": {
55
- "@netlify/blobs": "9.1.3",
56
- "@netlify/config": "^23.0.7",
57
- "@netlify/dev-utils": "3.0.0",
58
- "@netlify/functions": "4.0.0",
59
- "@netlify/headers": "1.0.0",
60
- "@netlify/redirects": "2.0.0",
61
- "@netlify/runtime": "3.0.0",
62
- "@netlify/static": "2.0.0"
55
+ "@netlify/blobs": "9.1.4",
56
+ "@netlify/config": "^23.0.8",
57
+ "@netlify/dev-utils": "3.1.0",
58
+ "@netlify/edge-functions": "2.13.1",
59
+ "@netlify/functions": "4.1.0",
60
+ "@netlify/headers": "2.0.0",
61
+ "@netlify/redirects": "3.0.0",
62
+ "@netlify/runtime": "4.0.0",
63
+ "@netlify/static": "3.0.0",
64
+ "ulid": "^3.0.0"
63
65
  }
64
66
  }