@netlify/dev 2.3.1 → 4.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/README.md +1 -2
- package/dist/main.cjs +159 -51
- package/dist/main.d.cts +51 -8
- package/dist/main.d.ts +51 -8
- package/dist/main.js +165 -57
- package/package.json +14 -12
package/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# @netlify/dev
|
|
2
2
|
|
|
3
|
-
> [!WARNING]
|
|
4
|
-
> This module is under active development and does **not** yet support all Netlify platform features.
|
|
3
|
+
> [!WARNING] This module is under active development and does **not** yet support all Netlify platform features.
|
|
5
4
|
|
|
6
5
|
`@netlify/dev` is a local emulator for the Netlify production environment. While it can be used directly by advanced
|
|
7
6
|
users, it is primarily designed as a foundational library for higher-level tools like the
|
package/dist/main.cjs
CHANGED
|
@@ -38,7 +38,9 @@ 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");
|
|
43
|
+
var import_headers = require("@netlify/headers");
|
|
42
44
|
var import_redirects = require("@netlify/redirects");
|
|
43
45
|
var import_static = require("@netlify/static");
|
|
44
46
|
|
|
@@ -53,7 +55,7 @@ var injectEnvVariables = async ({
|
|
|
53
55
|
}) => {
|
|
54
56
|
const results = {};
|
|
55
57
|
let variables = baseVariables;
|
|
56
|
-
if (netlifyAPI && accountSlug) {
|
|
58
|
+
if (netlifyAPI && siteID && accountSlug) {
|
|
57
59
|
variables = await getEnvelopeEnv({
|
|
58
60
|
accountId: accountSlug,
|
|
59
61
|
api: netlifyAPI,
|
|
@@ -69,7 +71,8 @@ var injectEnvVariables = async ({
|
|
|
69
71
|
isInternal,
|
|
70
72
|
originalValue: envAPI.get(key),
|
|
71
73
|
overriddenSources,
|
|
72
|
-
usedSource
|
|
74
|
+
usedSource,
|
|
75
|
+
value: variable.value
|
|
73
76
|
};
|
|
74
77
|
if (!existsInProcess || isInternal) {
|
|
75
78
|
envAPI.set(key, variable.value);
|
|
@@ -194,6 +197,10 @@ var isFile = async (path3) => {
|
|
|
194
197
|
return false;
|
|
195
198
|
};
|
|
196
199
|
|
|
200
|
+
// src/lib/request_id.ts
|
|
201
|
+
var import_ulid = require("ulid");
|
|
202
|
+
var generateRequestID = () => (0, import_ulid.ulid)();
|
|
203
|
+
|
|
197
204
|
// src/lib/runtime.ts
|
|
198
205
|
var import_node_path = __toESM(require("path"), 1);
|
|
199
206
|
var import_node_process = __toESM(require("process"), 1);
|
|
@@ -260,79 +267,98 @@ var NetlifyDev = class {
|
|
|
260
267
|
#apiHost;
|
|
261
268
|
#apiScheme;
|
|
262
269
|
#apiToken;
|
|
270
|
+
#cleanupJobs;
|
|
271
|
+
#edgeFunctionsHandler;
|
|
272
|
+
#functionsHandler;
|
|
273
|
+
#functionsServePath;
|
|
263
274
|
#config;
|
|
264
275
|
#features;
|
|
276
|
+
#headersHandler;
|
|
265
277
|
#logger;
|
|
266
278
|
#projectRoot;
|
|
267
|
-
#
|
|
279
|
+
#redirectsHandler;
|
|
280
|
+
#server;
|
|
268
281
|
#siteID;
|
|
282
|
+
#staticHandler;
|
|
283
|
+
#staticHandlerAdditionalDirectories;
|
|
269
284
|
constructor(options) {
|
|
270
285
|
if (options.apiURL) {
|
|
271
286
|
const apiURL = new URL(options.apiURL);
|
|
272
287
|
this.#apiHost = apiURL.host;
|
|
273
288
|
this.#apiScheme = apiURL.protocol.slice(0, -1);
|
|
274
289
|
}
|
|
290
|
+
const projectRoot = options.projectRoot ?? import_node_process2.default.cwd();
|
|
275
291
|
this.#apiToken = options.apiToken;
|
|
292
|
+
this.#cleanupJobs = [];
|
|
276
293
|
this.#features = {
|
|
277
294
|
blobs: options.blobs?.enabled !== false,
|
|
295
|
+
edgeFunctions: options.edgeFunctions?.enabled !== false,
|
|
278
296
|
environmentVariables: options.environmentVariables?.enabled !== false,
|
|
279
297
|
functions: options.functions?.enabled !== false,
|
|
298
|
+
headers: options.headers?.enabled !== false,
|
|
280
299
|
redirects: options.redirects?.enabled !== false,
|
|
281
300
|
static: options.staticFiles?.enabled !== false
|
|
282
301
|
};
|
|
302
|
+
this.#functionsServePath = import_node_path2.default.join(projectRoot, ".netlify", "functions-serve");
|
|
283
303
|
this.#logger = options.logger ?? globalThis.console;
|
|
284
|
-
this.#
|
|
304
|
+
this.#server = options.serverAddress;
|
|
305
|
+
this.#projectRoot = projectRoot;
|
|
306
|
+
this.#staticHandlerAdditionalDirectories = options.staticFiles?.directories ?? [];
|
|
285
307
|
}
|
|
286
|
-
async handleInEphemeralDirectory(request, destPath) {
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
projectRoot: this.#projectRoot,
|
|
293
|
-
settings: {},
|
|
294
|
-
siteId: this.#siteID,
|
|
295
|
-
timeouts: {},
|
|
296
|
-
userFunctionsPath: userFunctionsPathExists ? userFunctionsPath : void 0
|
|
297
|
-
}) : null;
|
|
298
|
-
const redirects = this.#features.redirects ? new import_redirects.RedirectsHandler({
|
|
299
|
-
configPath: this.#config?.configPath,
|
|
300
|
-
configRedirects: this.#config?.config.redirects,
|
|
301
|
-
jwtRoleClaim: "",
|
|
302
|
-
jwtSecret: "",
|
|
303
|
-
notFoundHandler,
|
|
304
|
-
projectDir: this.#projectRoot
|
|
305
|
-
}) : null;
|
|
306
|
-
const staticFiles = this.#features.static ? new import_static.StaticHandler({
|
|
307
|
-
directory: this.#config?.config.build.publish ?? this.#projectRoot
|
|
308
|
-
}) : null;
|
|
309
|
-
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);
|
|
310
314
|
if (functionMatch) {
|
|
311
315
|
if (functionMatch.preferStatic) {
|
|
312
|
-
const staticMatch2 = await
|
|
316
|
+
const staticMatch2 = await this.#staticHandler?.match(request);
|
|
313
317
|
if (staticMatch2) {
|
|
314
|
-
|
|
318
|
+
const response = await staticMatch2.handle();
|
|
319
|
+
await this.#headersHandler?.apply(request, response, options.headersCollector);
|
|
320
|
+
return { response, type: "static" };
|
|
315
321
|
}
|
|
316
322
|
}
|
|
317
|
-
return functionMatch.handle(request);
|
|
323
|
+
return { response: await functionMatch.handle(request), type: "function" };
|
|
318
324
|
}
|
|
319
|
-
const redirectMatch = await
|
|
325
|
+
const redirectMatch = await this.#redirectsHandler?.match(request);
|
|
320
326
|
if (redirectMatch) {
|
|
321
|
-
const functionMatch2 = await
|
|
327
|
+
const functionMatch2 = await this.#functionsHandler?.match(new Request(redirectMatch.target), destPath);
|
|
322
328
|
if (functionMatch2 && !functionMatch2.preferStatic) {
|
|
323
|
-
return functionMatch2.handle(request);
|
|
329
|
+
return { response: await functionMatch2.handle(request), type: "function" };
|
|
324
330
|
}
|
|
325
|
-
const response = await
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
+
);
|
|
329
346
|
if (response) {
|
|
330
|
-
return response;
|
|
347
|
+
return { response, type: "redirect" };
|
|
331
348
|
}
|
|
332
349
|
}
|
|
333
|
-
const
|
|
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);
|
|
334
358
|
if (staticMatch) {
|
|
335
|
-
|
|
359
|
+
const response = await staticMatch.handle();
|
|
360
|
+
await this.#headersHandler?.apply(request, response, options.headersCollector);
|
|
361
|
+
return { response, type: "static" };
|
|
336
362
|
}
|
|
337
363
|
}
|
|
338
364
|
async getConfig() {
|
|
@@ -352,12 +378,17 @@ var NetlifyDev = class {
|
|
|
352
378
|
});
|
|
353
379
|
return config;
|
|
354
380
|
}
|
|
355
|
-
async handle(request) {
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
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}_`));
|
|
359
390
|
try {
|
|
360
|
-
return await this.handleInEphemeralDirectory(request, destPath);
|
|
391
|
+
return await this.handleInEphemeralDirectory(request, destPath, options);
|
|
361
392
|
} finally {
|
|
362
393
|
try {
|
|
363
394
|
await import_node_fs2.promises.rm(destPath, { force: true, recursive: true });
|
|
@@ -382,9 +413,21 @@ var NetlifyDev = class {
|
|
|
382
413
|
projectRoot: this.#projectRoot,
|
|
383
414
|
siteID: siteID ?? "0"
|
|
384
415
|
});
|
|
385
|
-
this.#
|
|
386
|
-
|
|
387
|
-
|
|
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({
|
|
388
431
|
accountSlug: config?.siteInfo?.account_slug,
|
|
389
432
|
baseVariables: config?.env || {},
|
|
390
433
|
envAPI: runtime.env,
|
|
@@ -392,9 +435,74 @@ var NetlifyDev = class {
|
|
|
392
435
|
siteID
|
|
393
436
|
});
|
|
394
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
|
+
};
|
|
395
503
|
}
|
|
396
504
|
async stop() {
|
|
397
|
-
await this.#
|
|
505
|
+
await Promise.allSettled(this.#cleanupJobs.map((task) => task()));
|
|
398
506
|
}
|
|
399
507
|
};
|
|
400
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
|
|
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
|
|
27
|
+
enabled?: boolean;
|
|
19
28
|
};
|
|
20
29
|
/**
|
|
21
30
|
* Configuration options for Netlify Functions.
|
|
@@ -23,7 +32,15 @@ interface Features {
|
|
|
23
32
|
* {@link} https://docs.netlify.com/functions/overview/
|
|
24
33
|
*/
|
|
25
34
|
functions?: {
|
|
26
|
-
enabled
|
|
35
|
+
enabled?: boolean;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Configuration options for Netlify response headers.
|
|
39
|
+
*
|
|
40
|
+
* {@link} https://docs.netlify.com/routing/headers/
|
|
41
|
+
*/
|
|
42
|
+
headers?: {
|
|
43
|
+
enabled?: boolean;
|
|
27
44
|
};
|
|
28
45
|
/**
|
|
29
46
|
* Configuration options for Netlify redirects and rewrites.
|
|
@@ -31,13 +48,23 @@ interface Features {
|
|
|
31
48
|
* {@link} https://docs.netlify.com/routing/redirects/
|
|
32
49
|
*/
|
|
33
50
|
redirects?: {
|
|
34
|
-
enabled
|
|
51
|
+
enabled?: boolean;
|
|
35
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;
|
|
36
58
|
/**
|
|
37
59
|
* Configuration options for serving static files.
|
|
38
60
|
*/
|
|
39
61
|
staticFiles?: {
|
|
40
|
-
enabled
|
|
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[];
|
|
41
68
|
};
|
|
42
69
|
}
|
|
43
70
|
interface NetlifyDevOptions extends Features {
|
|
@@ -46,15 +73,31 @@ interface NetlifyDevOptions extends Features {
|
|
|
46
73
|
logger?: Logger;
|
|
47
74
|
projectRoot?: string;
|
|
48
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';
|
|
49
86
|
declare class NetlifyDev {
|
|
50
87
|
#private;
|
|
51
88
|
constructor(options: NetlifyDevOptions);
|
|
52
89
|
private handleInEphemeralDirectory;
|
|
53
90
|
private getConfig;
|
|
54
|
-
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>;
|
|
55
96
|
get siteIsLinked(): boolean;
|
|
56
|
-
start(): Promise<
|
|
97
|
+
start(): Promise<{
|
|
98
|
+
serverAddress: string | undefined;
|
|
99
|
+
}>;
|
|
57
100
|
stop(): Promise<void>;
|
|
58
101
|
}
|
|
59
102
|
|
|
60
|
-
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
|
|
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
|
|
27
|
+
enabled?: boolean;
|
|
19
28
|
};
|
|
20
29
|
/**
|
|
21
30
|
* Configuration options for Netlify Functions.
|
|
@@ -23,7 +32,15 @@ interface Features {
|
|
|
23
32
|
* {@link} https://docs.netlify.com/functions/overview/
|
|
24
33
|
*/
|
|
25
34
|
functions?: {
|
|
26
|
-
enabled
|
|
35
|
+
enabled?: boolean;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Configuration options for Netlify response headers.
|
|
39
|
+
*
|
|
40
|
+
* {@link} https://docs.netlify.com/routing/headers/
|
|
41
|
+
*/
|
|
42
|
+
headers?: {
|
|
43
|
+
enabled?: boolean;
|
|
27
44
|
};
|
|
28
45
|
/**
|
|
29
46
|
* Configuration options for Netlify redirects and rewrites.
|
|
@@ -31,13 +48,23 @@ interface Features {
|
|
|
31
48
|
* {@link} https://docs.netlify.com/routing/redirects/
|
|
32
49
|
*/
|
|
33
50
|
redirects?: {
|
|
34
|
-
enabled
|
|
51
|
+
enabled?: boolean;
|
|
35
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;
|
|
36
58
|
/**
|
|
37
59
|
* Configuration options for serving static files.
|
|
38
60
|
*/
|
|
39
61
|
staticFiles?: {
|
|
40
|
-
enabled
|
|
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[];
|
|
41
68
|
};
|
|
42
69
|
}
|
|
43
70
|
interface NetlifyDevOptions extends Features {
|
|
@@ -46,15 +73,31 @@ interface NetlifyDevOptions extends Features {
|
|
|
46
73
|
logger?: Logger;
|
|
47
74
|
projectRoot?: string;
|
|
48
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';
|
|
49
86
|
declare class NetlifyDev {
|
|
50
87
|
#private;
|
|
51
88
|
constructor(options: NetlifyDevOptions);
|
|
52
89
|
private handleInEphemeralDirectory;
|
|
53
90
|
private getConfig;
|
|
54
|
-
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>;
|
|
55
96
|
get siteIsLinked(): boolean;
|
|
56
|
-
start(): Promise<
|
|
97
|
+
start(): Promise<{
|
|
98
|
+
serverAddress: string | undefined;
|
|
99
|
+
}>;
|
|
57
100
|
stop(): Promise<void>;
|
|
58
101
|
}
|
|
59
102
|
|
|
60
|
-
export { type Features, NetlifyDev };
|
|
103
|
+
export { type Features, NetlifyDev, type ResponseType };
|
package/dist/main.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// src/main.ts
|
|
2
|
-
import { promises as fs2 } from "
|
|
3
|
-
import path2 from "
|
|
4
|
-
import process2 from "
|
|
2
|
+
import { promises as fs2 } from "fs";
|
|
3
|
+
import path2 from "path";
|
|
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";
|
|
9
|
+
import { HeadersHandler } from "@netlify/headers";
|
|
8
10
|
import { RedirectsHandler } from "@netlify/redirects";
|
|
9
11
|
import { StaticHandler } from "@netlify/static";
|
|
10
12
|
|
|
@@ -19,7 +21,7 @@ var injectEnvVariables = async ({
|
|
|
19
21
|
}) => {
|
|
20
22
|
const results = {};
|
|
21
23
|
let variables = baseVariables;
|
|
22
|
-
if (netlifyAPI && accountSlug) {
|
|
24
|
+
if (netlifyAPI && siteID && accountSlug) {
|
|
23
25
|
variables = await getEnvelopeEnv({
|
|
24
26
|
accountId: accountSlug,
|
|
25
27
|
api: netlifyAPI,
|
|
@@ -35,7 +37,8 @@ var injectEnvVariables = async ({
|
|
|
35
37
|
isInternal,
|
|
36
38
|
originalValue: envAPI.get(key),
|
|
37
39
|
overriddenSources,
|
|
38
|
-
usedSource
|
|
40
|
+
usedSource,
|
|
41
|
+
value: variable.value
|
|
39
42
|
};
|
|
40
43
|
if (!existsInProcess || isInternal) {
|
|
41
44
|
envAPI.set(key, variable.value);
|
|
@@ -142,7 +145,7 @@ var getEnvelopeEnv = async ({
|
|
|
142
145
|
};
|
|
143
146
|
|
|
144
147
|
// src/lib/fs.ts
|
|
145
|
-
import { promises as fs } from "
|
|
148
|
+
import { promises as fs } from "fs";
|
|
146
149
|
var isDirectory = async (path3) => {
|
|
147
150
|
try {
|
|
148
151
|
const stat = await fs.stat(path3);
|
|
@@ -160,9 +163,13 @@ var isFile = async (path3) => {
|
|
|
160
163
|
return false;
|
|
161
164
|
};
|
|
162
165
|
|
|
166
|
+
// src/lib/request_id.ts
|
|
167
|
+
import { ulid } from "ulid";
|
|
168
|
+
var generateRequestID = () => ulid();
|
|
169
|
+
|
|
163
170
|
// src/lib/runtime.ts
|
|
164
|
-
import path from "
|
|
165
|
-
import process from "
|
|
171
|
+
import path from "path";
|
|
172
|
+
import process from "process";
|
|
166
173
|
import { BlobsServer } from "@netlify/blobs/server";
|
|
167
174
|
import { startRuntime } from "@netlify/runtime";
|
|
168
175
|
var restoreEnvironment = (snapshot) => {
|
|
@@ -226,79 +233,98 @@ var NetlifyDev = class {
|
|
|
226
233
|
#apiHost;
|
|
227
234
|
#apiScheme;
|
|
228
235
|
#apiToken;
|
|
236
|
+
#cleanupJobs;
|
|
237
|
+
#edgeFunctionsHandler;
|
|
238
|
+
#functionsHandler;
|
|
239
|
+
#functionsServePath;
|
|
229
240
|
#config;
|
|
230
241
|
#features;
|
|
242
|
+
#headersHandler;
|
|
231
243
|
#logger;
|
|
232
244
|
#projectRoot;
|
|
233
|
-
#
|
|
245
|
+
#redirectsHandler;
|
|
246
|
+
#server;
|
|
234
247
|
#siteID;
|
|
248
|
+
#staticHandler;
|
|
249
|
+
#staticHandlerAdditionalDirectories;
|
|
235
250
|
constructor(options) {
|
|
236
251
|
if (options.apiURL) {
|
|
237
252
|
const apiURL = new URL(options.apiURL);
|
|
238
253
|
this.#apiHost = apiURL.host;
|
|
239
254
|
this.#apiScheme = apiURL.protocol.slice(0, -1);
|
|
240
255
|
}
|
|
256
|
+
const projectRoot = options.projectRoot ?? process2.cwd();
|
|
241
257
|
this.#apiToken = options.apiToken;
|
|
258
|
+
this.#cleanupJobs = [];
|
|
242
259
|
this.#features = {
|
|
243
260
|
blobs: options.blobs?.enabled !== false,
|
|
261
|
+
edgeFunctions: options.edgeFunctions?.enabled !== false,
|
|
244
262
|
environmentVariables: options.environmentVariables?.enabled !== false,
|
|
245
263
|
functions: options.functions?.enabled !== false,
|
|
264
|
+
headers: options.headers?.enabled !== false,
|
|
246
265
|
redirects: options.redirects?.enabled !== false,
|
|
247
266
|
static: options.staticFiles?.enabled !== false
|
|
248
267
|
};
|
|
268
|
+
this.#functionsServePath = path2.join(projectRoot, ".netlify", "functions-serve");
|
|
249
269
|
this.#logger = options.logger ?? globalThis.console;
|
|
250
|
-
this.#
|
|
270
|
+
this.#server = options.serverAddress;
|
|
271
|
+
this.#projectRoot = projectRoot;
|
|
272
|
+
this.#staticHandlerAdditionalDirectories = options.staticFiles?.directories ?? [];
|
|
251
273
|
}
|
|
252
|
-
async handleInEphemeralDirectory(request, destPath) {
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
projectRoot: this.#projectRoot,
|
|
259
|
-
settings: {},
|
|
260
|
-
siteId: this.#siteID,
|
|
261
|
-
timeouts: {},
|
|
262
|
-
userFunctionsPath: userFunctionsPathExists ? userFunctionsPath : void 0
|
|
263
|
-
}) : null;
|
|
264
|
-
const redirects = this.#features.redirects ? new RedirectsHandler({
|
|
265
|
-
configPath: this.#config?.configPath,
|
|
266
|
-
configRedirects: this.#config?.config.redirects,
|
|
267
|
-
jwtRoleClaim: "",
|
|
268
|
-
jwtSecret: "",
|
|
269
|
-
notFoundHandler,
|
|
270
|
-
projectDir: this.#projectRoot
|
|
271
|
-
}) : null;
|
|
272
|
-
const staticFiles = this.#features.static ? new StaticHandler({
|
|
273
|
-
directory: this.#config?.config.build.publish ?? this.#projectRoot
|
|
274
|
-
}) : null;
|
|
275
|
-
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);
|
|
276
280
|
if (functionMatch) {
|
|
277
281
|
if (functionMatch.preferStatic) {
|
|
278
|
-
const staticMatch2 = await
|
|
282
|
+
const staticMatch2 = await this.#staticHandler?.match(request);
|
|
279
283
|
if (staticMatch2) {
|
|
280
|
-
|
|
284
|
+
const response = await staticMatch2.handle();
|
|
285
|
+
await this.#headersHandler?.apply(request, response, options.headersCollector);
|
|
286
|
+
return { response, type: "static" };
|
|
281
287
|
}
|
|
282
288
|
}
|
|
283
|
-
return functionMatch.handle(request);
|
|
289
|
+
return { response: await functionMatch.handle(request), type: "function" };
|
|
284
290
|
}
|
|
285
|
-
const redirectMatch = await
|
|
291
|
+
const redirectMatch = await this.#redirectsHandler?.match(request);
|
|
286
292
|
if (redirectMatch) {
|
|
287
|
-
const functionMatch2 = await
|
|
293
|
+
const functionMatch2 = await this.#functionsHandler?.match(new Request(redirectMatch.target), destPath);
|
|
288
294
|
if (functionMatch2 && !functionMatch2.preferStatic) {
|
|
289
|
-
return functionMatch2.handle(request);
|
|
295
|
+
return { response: await functionMatch2.handle(request), type: "function" };
|
|
290
296
|
}
|
|
291
|
-
const response = await
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
+
);
|
|
295
312
|
if (response) {
|
|
296
|
-
return response;
|
|
313
|
+
return { response, type: "redirect" };
|
|
297
314
|
}
|
|
298
315
|
}
|
|
299
|
-
const
|
|
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);
|
|
300
324
|
if (staticMatch) {
|
|
301
|
-
|
|
325
|
+
const response = await staticMatch.handle();
|
|
326
|
+
await this.#headersHandler?.apply(request, response, options.headersCollector);
|
|
327
|
+
return { response, type: "static" };
|
|
302
328
|
}
|
|
303
329
|
}
|
|
304
330
|
async getConfig() {
|
|
@@ -318,12 +344,17 @@ var NetlifyDev = class {
|
|
|
318
344
|
});
|
|
319
345
|
return config;
|
|
320
346
|
}
|
|
321
|
-
async handle(request) {
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
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}_`));
|
|
325
356
|
try {
|
|
326
|
-
return await this.handleInEphemeralDirectory(request, destPath);
|
|
357
|
+
return await this.handleInEphemeralDirectory(request, destPath, options);
|
|
327
358
|
} finally {
|
|
328
359
|
try {
|
|
329
360
|
await fs2.rm(destPath, { force: true, recursive: true });
|
|
@@ -348,9 +379,21 @@ var NetlifyDev = class {
|
|
|
348
379
|
projectRoot: this.#projectRoot,
|
|
349
380
|
siteID: siteID ?? "0"
|
|
350
381
|
});
|
|
351
|
-
this.#
|
|
352
|
-
|
|
353
|
-
|
|
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({
|
|
354
397
|
accountSlug: config?.siteInfo?.account_slug,
|
|
355
398
|
baseVariables: config?.env || {},
|
|
356
399
|
envAPI: runtime.env,
|
|
@@ -358,9 +401,74 @@ var NetlifyDev = class {
|
|
|
358
401
|
siteID
|
|
359
402
|
});
|
|
360
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
|
+
};
|
|
361
469
|
}
|
|
362
470
|
async stop() {
|
|
363
|
-
await this.#
|
|
471
|
+
await Promise.allSettled(this.#cleanupJobs.map((task) => task()));
|
|
364
472
|
}
|
|
365
473
|
};
|
|
366
474
|
export {
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/dev",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Emulation of the Netlify environment for local development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
|
-
"node": "
|
|
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.
|
|
50
|
-
"@netlify/types": "
|
|
51
|
-
"tmp-promise": "^3.0.3",
|
|
49
|
+
"@netlify/api": "^14.0.3",
|
|
50
|
+
"@netlify/types": "2.0.1",
|
|
52
51
|
"tsup": "^8.0.0",
|
|
53
52
|
"vitest": "^3.0.0"
|
|
54
53
|
},
|
|
55
54
|
"dependencies": {
|
|
56
|
-
"@netlify/blobs": "9.1.
|
|
57
|
-
"@netlify/config": "^23.0.
|
|
58
|
-
"@netlify/dev-utils": "
|
|
59
|
-
"@netlify/functions": "
|
|
60
|
-
"@netlify/
|
|
61
|
-
"@netlify/
|
|
62
|
-
"@netlify/
|
|
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.0",
|
|
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
|
}
|