@netlify/dev 4.0.2 → 4.1.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/README.md CHANGED
@@ -15,12 +15,13 @@ Functions, Blobs, Static files, and Redirects.
15
15
  | Feature | Supported |
16
16
  | ---------------------- | --------- |
17
17
  | Functions | ✅ Yes |
18
- | Edge Functions | No |
18
+ | Edge Functions | Yes |
19
19
  | Blobs | ✅ Yes |
20
20
  | Cache API | ✅ Yes |
21
21
  | Redirects and Rewrites | ✅ Yes |
22
- | Headers | No |
22
+ | Headers | Yes |
23
23
  | Environment Variables | ✅ Yes |
24
+ | Image CDN | ❌ No |
24
25
 
25
26
  > Note: Missing features will be added incrementally. This module is **not** intended to be a full replacement for the
26
27
  > Netlify CLI.
@@ -45,15 +46,32 @@ You can use `@netlify/dev` to emulate the Netlify runtime in your own developmen
45
46
  import { NetlifyDev } from '@netlify/dev'
46
47
 
47
48
  const devServer = new NetlifyDev({
48
- functions: { enabled: true },
49
49
  blobs: { enabled: true },
50
+ edgeFunctions: { enabled: true },
51
+ environmentVariables: { enabled: true },
52
+ functions: { enabled: true },
50
53
  redirects: { enabled: true },
51
- staticFiles: { enabled: true },
54
+ staticFiles: {
55
+ enabled: true,
56
+ // OPTIONAL: additional directories containing static files to serve
57
+ // Your `projectRoot` (see below) and your site's `publish` dir are served by default
58
+ directories: ['public'],
59
+ },
60
+
61
+ // OPTIONAL: base dir (https://docs.netlify.com/configure-builds/overview/#definitions)
62
+ // Defaults to current working directory
63
+ projectRoot: 'site',
64
+ // OPTIONAL: if your local dev setup has its own HTTP server (e.g. Vite), set its address here
65
+ serverAddress: 'http://localhost:1234',
52
66
  })
53
67
 
54
68
  await devServer.start()
55
69
 
56
- const response = await devServer.handle(new Request('http://localhost:8888/path'))
70
+ const response = await devServer.handle(new Request('http://localhost:8888/path'), {
71
+ // An optional callback that will be called with every header (key and value) coming from header rules.
72
+ // See https://docs.netlify.com/routing/headers/
73
+ headersCollector: (key: string, value: string) => console.log(key, value),
74
+ })
57
75
 
58
76
  console.log(await response.text())
59
77
 
package/dist/main.cjs CHANGED
@@ -197,6 +197,46 @@ var isFile = async (path3) => {
197
197
  return false;
198
198
  };
199
199
 
200
+ // src/lib/reqres.ts
201
+ var import_node_stream = require("stream");
202
+ var normalizeHeaders = (headers) => {
203
+ const result = [];
204
+ for (const [key, value] of Object.entries(headers)) {
205
+ if (Array.isArray(value)) {
206
+ result.push([key, value.join(",")]);
207
+ } else if (typeof value === "string") {
208
+ result.push([key, value]);
209
+ }
210
+ }
211
+ return result;
212
+ };
213
+ var getNormalizedRequest = (input, requestID, removeBody) => {
214
+ const method = input.method.toUpperCase();
215
+ const headers = input.headers;
216
+ headers.set("x-nf-request-id", requestID);
217
+ return new Request(input.url, {
218
+ body: method === "GET" || method === "HEAD" || removeBody ? null : input.body,
219
+ // @ts-expect-error Not typed!
220
+ duplex: "half",
221
+ headers,
222
+ method
223
+ });
224
+ };
225
+ var getNormalizedRequestFromNodeRequest = (input, requestID, removeBody) => {
226
+ const { headers, url = "" } = input;
227
+ const origin = `http://${headers.host ?? "localhost"}`;
228
+ const fullUrl = new URL(url, origin);
229
+ const method = input.method?.toUpperCase() ?? "GET";
230
+ const body = input.method === "GET" || input.method === "HEAD" || removeBody ? null : import_node_stream.Readable.toWeb(input);
231
+ return new Request(fullUrl, {
232
+ body,
233
+ // @ts-expect-error Not typed!
234
+ duplex: "half",
235
+ headers: normalizeHeaders({ ...input.headers, "x-nf-request-id": requestID }),
236
+ method
237
+ });
238
+ };
239
+
200
240
  // src/lib/request_id.ts
201
241
  var import_ulid = require("ulid");
202
242
  var generateRequestID = () => (0, import_ulid.ulid)();
@@ -305,31 +345,37 @@ var NetlifyDev = class {
305
345
  this.#projectRoot = projectRoot;
306
346
  this.#staticHandlerAdditionalDirectories = options.staticFiles?.directories ?? [];
307
347
  }
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" };
348
+ async handleInEphemeralDirectory(matchRequest, getHandleRequest, destPath, options = {}) {
349
+ const edgeFunctionMatch = await this.#edgeFunctionsHandler?.match(matchRequest);
350
+ if (edgeFunctionMatch) {
351
+ return {
352
+ response: await edgeFunctionMatch.handle(getHandleRequest()),
353
+ type: "edge-function"
354
+ };
312
355
  }
313
- const functionMatch = await this.#functionsHandler?.match(request, destPath);
356
+ const functionMatch = await this.#functionsHandler?.match(matchRequest, destPath);
314
357
  if (functionMatch) {
315
358
  if (functionMatch.preferStatic) {
316
- const staticMatch2 = await this.#staticHandler?.match(request);
359
+ const staticMatch2 = await this.#staticHandler?.match(matchRequest);
317
360
  if (staticMatch2) {
318
361
  const response = await staticMatch2.handle();
319
- await this.#headersHandler?.apply(request, response, options.headersCollector);
362
+ await this.#headersHandler?.apply(matchRequest, response, options.headersCollector);
320
363
  return { response, type: "static" };
321
364
  }
322
365
  }
323
- return { response: await functionMatch.handle(request), type: "function" };
366
+ return { response: await functionMatch.handle(getHandleRequest()), type: "function" };
324
367
  }
325
- const redirectMatch = await this.#redirectsHandler?.match(request);
368
+ const redirectMatch = await this.#redirectsHandler?.match(matchRequest);
326
369
  if (redirectMatch) {
327
370
  const functionMatch2 = await this.#functionsHandler?.match(new Request(redirectMatch.target), destPath);
328
371
  if (functionMatch2 && !functionMatch2.preferStatic) {
329
- return { response: await functionMatch2.handle(request), type: "function" };
372
+ return {
373
+ response: await functionMatch2.handle(getHandleRequest()),
374
+ type: "function"
375
+ };
330
376
  }
331
377
  const response = await this.#redirectsHandler?.handle(
332
- request,
378
+ getHandleRequest(),
333
379
  redirectMatch,
334
380
  async (maybeStaticFile) => {
335
381
  const staticMatch2 = await this.#staticHandler?.match(maybeStaticFile);
@@ -347,17 +393,17 @@ var NetlifyDev = class {
347
393
  return { response, type: "redirect" };
348
394
  }
349
395
  }
350
- const { pathname } = new URL(request.url);
396
+ const { pathname } = new URL(matchRequest.url);
351
397
  if (pathname.startsWith("/.netlify/images")) {
352
398
  this.#logger.error(
353
399
  "The Netlify Image CDN is currently only supported in the Netlify CLI. Run `npx netlify dev` to get started."
354
400
  );
355
401
  return;
356
402
  }
357
- const staticMatch = await this.#staticHandler?.match(request);
403
+ const staticMatch = await this.#staticHandler?.match(matchRequest);
358
404
  if (staticMatch) {
359
405
  const response = await staticMatch.handle();
360
- await this.#headersHandler?.apply(request, response, options.headersCollector);
406
+ await this.#headersHandler?.apply(matchRequest, response, options.headersCollector);
361
407
  return { response, type: "static" };
362
408
  }
363
409
  }
@@ -383,12 +429,28 @@ var NetlifyDev = class {
383
429
  return result?.response;
384
430
  }
385
431
  async handleAndIntrospect(request, options = {}) {
432
+ await import_node_fs2.promises.mkdir(this.#functionsServePath, { recursive: true });
433
+ const destPath = await import_node_fs2.promises.mkdtemp(import_node_path2.default.join(this.#functionsServePath, `_`));
386
434
  const requestID = generateRequestID();
387
- request.headers.set("x-nf-request-id", requestID);
435
+ const matchRequest = getNormalizedRequest(request, requestID, true);
436
+ const getHandleRequest = () => getNormalizedRequest(request, requestID, false);
437
+ try {
438
+ return await this.handleInEphemeralDirectory(matchRequest, getHandleRequest, destPath, options);
439
+ } finally {
440
+ try {
441
+ await import_node_fs2.promises.rm(destPath, { force: true, recursive: true });
442
+ } catch {
443
+ }
444
+ }
445
+ }
446
+ async handleAndIntrospectNodeRequest(request, options = {}) {
388
447
  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}_`));
448
+ const destPath = await import_node_fs2.promises.mkdtemp(import_node_path2.default.join(this.#functionsServePath, `_`));
449
+ const requestID = generateRequestID();
450
+ const matchRequest = getNormalizedRequestFromNodeRequest(request, requestID, true);
451
+ const getHandleRequest = () => getNormalizedRequestFromNodeRequest(request, requestID, false);
390
452
  try {
391
- return await this.handleInEphemeralDirectory(request, destPath, options);
453
+ return await this.handleInEphemeralDirectory(matchRequest, getHandleRequest, destPath, options);
392
454
  } finally {
393
455
  try {
394
456
  await import_node_fs2.promises.rm(destPath, { force: true, recursive: true });
package/dist/main.d.cts CHANGED
@@ -1,3 +1,4 @@
1
+ import { IncomingMessage } from 'node:http';
1
2
  import { Logger } from '@netlify/dev-utils';
2
3
  import { HeadersCollector } from '@netlify/headers';
3
4
 
@@ -93,6 +94,10 @@ declare class NetlifyDev {
93
94
  response: Response;
94
95
  type: ResponseType;
95
96
  } | undefined>;
97
+ handleAndIntrospectNodeRequest(request: IncomingMessage, options?: HandleOptions): Promise<{
98
+ response: Response;
99
+ type: ResponseType;
100
+ } | undefined>;
96
101
  get siteIsLinked(): boolean;
97
102
  start(): Promise<{
98
103
  serverAddress: string | undefined;
package/dist/main.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { IncomingMessage } from 'node:http';
1
2
  import { Logger } from '@netlify/dev-utils';
2
3
  import { HeadersCollector } from '@netlify/headers';
3
4
 
@@ -93,6 +94,10 @@ declare class NetlifyDev {
93
94
  response: Response;
94
95
  type: ResponseType;
95
96
  } | undefined>;
97
+ handleAndIntrospectNodeRequest(request: IncomingMessage, options?: HandleOptions): Promise<{
98
+ response: Response;
99
+ type: ResponseType;
100
+ } | undefined>;
96
101
  get siteIsLinked(): boolean;
97
102
  start(): Promise<{
98
103
  serverAddress: string | undefined;
package/dist/main.js CHANGED
@@ -163,6 +163,46 @@ var isFile = async (path3) => {
163
163
  return false;
164
164
  };
165
165
 
166
+ // src/lib/reqres.ts
167
+ import { Readable } from "stream";
168
+ var normalizeHeaders = (headers) => {
169
+ const result = [];
170
+ for (const [key, value] of Object.entries(headers)) {
171
+ if (Array.isArray(value)) {
172
+ result.push([key, value.join(",")]);
173
+ } else if (typeof value === "string") {
174
+ result.push([key, value]);
175
+ }
176
+ }
177
+ return result;
178
+ };
179
+ var getNormalizedRequest = (input, requestID, removeBody) => {
180
+ const method = input.method.toUpperCase();
181
+ const headers = input.headers;
182
+ headers.set("x-nf-request-id", requestID);
183
+ return new Request(input.url, {
184
+ body: method === "GET" || method === "HEAD" || removeBody ? null : input.body,
185
+ // @ts-expect-error Not typed!
186
+ duplex: "half",
187
+ headers,
188
+ method
189
+ });
190
+ };
191
+ var getNormalizedRequestFromNodeRequest = (input, requestID, removeBody) => {
192
+ const { headers, url = "" } = input;
193
+ const origin = `http://${headers.host ?? "localhost"}`;
194
+ const fullUrl = new URL(url, origin);
195
+ const method = input.method?.toUpperCase() ?? "GET";
196
+ const body = input.method === "GET" || input.method === "HEAD" || removeBody ? null : Readable.toWeb(input);
197
+ return new Request(fullUrl, {
198
+ body,
199
+ // @ts-expect-error Not typed!
200
+ duplex: "half",
201
+ headers: normalizeHeaders({ ...input.headers, "x-nf-request-id": requestID }),
202
+ method
203
+ });
204
+ };
205
+
166
206
  // src/lib/request_id.ts
167
207
  import { ulid } from "ulid";
168
208
  var generateRequestID = () => ulid();
@@ -271,31 +311,37 @@ var NetlifyDev = class {
271
311
  this.#projectRoot = projectRoot;
272
312
  this.#staticHandlerAdditionalDirectories = options.staticFiles?.directories ?? [];
273
313
  }
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" };
314
+ async handleInEphemeralDirectory(matchRequest, getHandleRequest, destPath, options = {}) {
315
+ const edgeFunctionMatch = await this.#edgeFunctionsHandler?.match(matchRequest);
316
+ if (edgeFunctionMatch) {
317
+ return {
318
+ response: await edgeFunctionMatch.handle(getHandleRequest()),
319
+ type: "edge-function"
320
+ };
278
321
  }
279
- const functionMatch = await this.#functionsHandler?.match(request, destPath);
322
+ const functionMatch = await this.#functionsHandler?.match(matchRequest, destPath);
280
323
  if (functionMatch) {
281
324
  if (functionMatch.preferStatic) {
282
- const staticMatch2 = await this.#staticHandler?.match(request);
325
+ const staticMatch2 = await this.#staticHandler?.match(matchRequest);
283
326
  if (staticMatch2) {
284
327
  const response = await staticMatch2.handle();
285
- await this.#headersHandler?.apply(request, response, options.headersCollector);
328
+ await this.#headersHandler?.apply(matchRequest, response, options.headersCollector);
286
329
  return { response, type: "static" };
287
330
  }
288
331
  }
289
- return { response: await functionMatch.handle(request), type: "function" };
332
+ return { response: await functionMatch.handle(getHandleRequest()), type: "function" };
290
333
  }
291
- const redirectMatch = await this.#redirectsHandler?.match(request);
334
+ const redirectMatch = await this.#redirectsHandler?.match(matchRequest);
292
335
  if (redirectMatch) {
293
336
  const functionMatch2 = await this.#functionsHandler?.match(new Request(redirectMatch.target), destPath);
294
337
  if (functionMatch2 && !functionMatch2.preferStatic) {
295
- return { response: await functionMatch2.handle(request), type: "function" };
338
+ return {
339
+ response: await functionMatch2.handle(getHandleRequest()),
340
+ type: "function"
341
+ };
296
342
  }
297
343
  const response = await this.#redirectsHandler?.handle(
298
- request,
344
+ getHandleRequest(),
299
345
  redirectMatch,
300
346
  async (maybeStaticFile) => {
301
347
  const staticMatch2 = await this.#staticHandler?.match(maybeStaticFile);
@@ -313,17 +359,17 @@ var NetlifyDev = class {
313
359
  return { response, type: "redirect" };
314
360
  }
315
361
  }
316
- const { pathname } = new URL(request.url);
362
+ const { pathname } = new URL(matchRequest.url);
317
363
  if (pathname.startsWith("/.netlify/images")) {
318
364
  this.#logger.error(
319
365
  "The Netlify Image CDN is currently only supported in the Netlify CLI. Run `npx netlify dev` to get started."
320
366
  );
321
367
  return;
322
368
  }
323
- const staticMatch = await this.#staticHandler?.match(request);
369
+ const staticMatch = await this.#staticHandler?.match(matchRequest);
324
370
  if (staticMatch) {
325
371
  const response = await staticMatch.handle();
326
- await this.#headersHandler?.apply(request, response, options.headersCollector);
372
+ await this.#headersHandler?.apply(matchRequest, response, options.headersCollector);
327
373
  return { response, type: "static" };
328
374
  }
329
375
  }
@@ -349,12 +395,28 @@ var NetlifyDev = class {
349
395
  return result?.response;
350
396
  }
351
397
  async handleAndIntrospect(request, options = {}) {
398
+ await fs2.mkdir(this.#functionsServePath, { recursive: true });
399
+ const destPath = await fs2.mkdtemp(path2.join(this.#functionsServePath, `_`));
352
400
  const requestID = generateRequestID();
353
- request.headers.set("x-nf-request-id", requestID);
401
+ const matchRequest = getNormalizedRequest(request, requestID, true);
402
+ const getHandleRequest = () => getNormalizedRequest(request, requestID, false);
403
+ try {
404
+ return await this.handleInEphemeralDirectory(matchRequest, getHandleRequest, destPath, options);
405
+ } finally {
406
+ try {
407
+ await fs2.rm(destPath, { force: true, recursive: true });
408
+ } catch {
409
+ }
410
+ }
411
+ }
412
+ async handleAndIntrospectNodeRequest(request, options = {}) {
354
413
  await fs2.mkdir(this.#functionsServePath, { recursive: true });
355
- const destPath = await fs2.mkdtemp(path2.join(this.#functionsServePath, `${requestID}_`));
414
+ const destPath = await fs2.mkdtemp(path2.join(this.#functionsServePath, `_`));
415
+ const requestID = generateRequestID();
416
+ const matchRequest = getNormalizedRequestFromNodeRequest(request, requestID, true);
417
+ const getHandleRequest = () => getNormalizedRequestFromNodeRequest(request, requestID, false);
356
418
  try {
357
- return await this.handleInEphemeralDirectory(request, destPath, options);
419
+ return await this.handleInEphemeralDirectory(matchRequest, getHandleRequest, destPath, options);
358
420
  } finally {
359
421
  try {
360
422
  await fs2.rm(destPath, { force: true, recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/dev",
3
- "version": "4.0.2",
3
+ "version": "4.1.0",
4
4
  "description": "Emulation of the Netlify environment for local development",
5
5
  "type": "module",
6
6
  "engines": {
@@ -55,7 +55,7 @@
55
55
  "@netlify/blobs": "9.1.4",
56
56
  "@netlify/config": "^23.0.8",
57
57
  "@netlify/dev-utils": "3.1.0",
58
- "@netlify/edge-functions": "2.13.2",
58
+ "@netlify/edge-functions": "2.14.0",
59
59
  "@netlify/functions": "4.1.0",
60
60
  "@netlify/headers": "2.0.0",
61
61
  "@netlify/redirects": "3.0.0",