@netlify/dev 4.0.1 → 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 +23 -5
- package/dist/main.cjs +79 -17
- package/dist/main.d.cts +5 -0
- package/dist/main.d.ts +5 -0
- package/dist/main.js +79 -17
- package/package.json +2 -2
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 |
|
|
18
|
+
| Edge Functions | ✅ Yes |
|
|
19
19
|
| Blobs | ✅ Yes |
|
|
20
20
|
| Cache API | ✅ Yes |
|
|
21
21
|
| Redirects and Rewrites | ✅ Yes |
|
|
22
|
-
| Headers |
|
|
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: {
|
|
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(
|
|
309
|
-
const
|
|
310
|
-
if (
|
|
311
|
-
return {
|
|
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(
|
|
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(
|
|
359
|
+
const staticMatch2 = await this.#staticHandler?.match(matchRequest);
|
|
317
360
|
if (staticMatch2) {
|
|
318
361
|
const response = await staticMatch2.handle();
|
|
319
|
-
await this.#headersHandler?.apply(
|
|
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(
|
|
366
|
+
return { response: await functionMatch.handle(getHandleRequest()), type: "function" };
|
|
324
367
|
}
|
|
325
|
-
const redirectMatch = await this.#redirectsHandler?.match(
|
|
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 {
|
|
372
|
+
return {
|
|
373
|
+
response: await functionMatch2.handle(getHandleRequest()),
|
|
374
|
+
type: "function"
|
|
375
|
+
};
|
|
330
376
|
}
|
|
331
377
|
const response = await this.#redirectsHandler?.handle(
|
|
332
|
-
|
|
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(
|
|
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(
|
|
403
|
+
const staticMatch = await this.#staticHandler?.match(matchRequest);
|
|
358
404
|
if (staticMatch) {
|
|
359
405
|
const response = await staticMatch.handle();
|
|
360
|
-
await this.#headersHandler?.apply(
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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(
|
|
275
|
-
const
|
|
276
|
-
if (
|
|
277
|
-
return {
|
|
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(
|
|
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(
|
|
325
|
+
const staticMatch2 = await this.#staticHandler?.match(matchRequest);
|
|
283
326
|
if (staticMatch2) {
|
|
284
327
|
const response = await staticMatch2.handle();
|
|
285
|
-
await this.#headersHandler?.apply(
|
|
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(
|
|
332
|
+
return { response: await functionMatch.handle(getHandleRequest()), type: "function" };
|
|
290
333
|
}
|
|
291
|
-
const redirectMatch = await this.#redirectsHandler?.match(
|
|
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 {
|
|
338
|
+
return {
|
|
339
|
+
response: await functionMatch2.handle(getHandleRequest()),
|
|
340
|
+
type: "function"
|
|
341
|
+
};
|
|
296
342
|
}
|
|
297
343
|
const response = await this.#redirectsHandler?.handle(
|
|
298
|
-
|
|
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(
|
|
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(
|
|
369
|
+
const staticMatch = await this.#staticHandler?.match(matchRequest);
|
|
324
370
|
if (staticMatch) {
|
|
325
371
|
const response = await staticMatch.handle();
|
|
326
|
-
await this.#headersHandler?.apply(
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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
|
|
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.
|
|
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",
|