@netlify/dev 2.2.2 → 2.3.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/README.md CHANGED
@@ -21,7 +21,7 @@ Functions, Blobs, Static files, and Redirects.
21
21
  | Cache API | ✅ Yes |
22
22
  | Redirects and Rewrites | ✅ Yes |
23
23
  | Headers | ❌ No |
24
- | Environment Variables | No |
24
+ | Environment Variables | Yes |
25
25
 
26
26
  > Note: Missing features will be added incrementally. This module is **not** intended to be a full replacement for the
27
27
  > Netlify CLI.
package/dist/main.cjs CHANGED
@@ -33,6 +33,7 @@ __export(main_exports, {
33
33
  NetlifyDev: () => NetlifyDev
34
34
  });
35
35
  module.exports = __toCommonJS(main_exports);
36
+ var import_node_fs2 = require("fs");
36
37
  var import_node_path2 = __toESM(require("path"), 1);
37
38
  var import_node_process2 = __toESM(require("process"), 1);
38
39
  var import_config = require("@netlify/config");
@@ -41,6 +42,139 @@ var import_dev = require("@netlify/functions/dev");
41
42
  var import_redirects = require("@netlify/redirects");
42
43
  var import_static = require("@netlify/static");
43
44
 
45
+ // src/lib/env.ts
46
+ var SUPPORTED_CONTEXTS = ["all", "production", "deploy-preview", "branch-deploy", "dev"];
47
+ var injectEnvVariables = async ({
48
+ accountSlug,
49
+ baseVariables = {},
50
+ envAPI,
51
+ netlifyAPI,
52
+ siteID
53
+ }) => {
54
+ const results = {};
55
+ let variables = baseVariables;
56
+ if (netlifyAPI && accountSlug) {
57
+ variables = await getEnvelopeEnv({
58
+ accountId: accountSlug,
59
+ api: netlifyAPI,
60
+ env: baseVariables,
61
+ siteId: siteID
62
+ });
63
+ }
64
+ for (const [key, variable] of Object.entries(variables)) {
65
+ const existsInProcess = envAPI.has(key);
66
+ const [usedSource, ...overriddenSources] = existsInProcess ? ["process", ...variable.sources] : variable.sources;
67
+ const isInternal = variable.sources.includes("internal");
68
+ const result = {
69
+ isInternal,
70
+ originalValue: envAPI.get(key),
71
+ overriddenSources,
72
+ usedSource
73
+ };
74
+ if (!existsInProcess || isInternal) {
75
+ envAPI.set(key, variable.value);
76
+ }
77
+ results[key] = result;
78
+ }
79
+ return results;
80
+ };
81
+ var getValueForContext = (values, contextOrBranch) => {
82
+ const isSupportedContext = SUPPORTED_CONTEXTS.includes(contextOrBranch);
83
+ if (!isSupportedContext) {
84
+ const valueMatchingAsBranch = values.find((val) => val.context_parameter === contextOrBranch);
85
+ if (valueMatchingAsBranch != null) {
86
+ return valueMatchingAsBranch;
87
+ }
88
+ const valueMatchingContext = values.find((val) => val.context === "all" || val.context === "branch-deploy");
89
+ return valueMatchingContext ?? void 0;
90
+ }
91
+ const valueMatchingAsContext = values.find((val) => val.context === "all" || val.context === contextOrBranch);
92
+ return valueMatchingAsContext ?? void 0;
93
+ };
94
+ var filterEnvBySource = (env, source) => Object.fromEntries(Object.entries(env).filter(([, variable]) => variable.sources[0] === source));
95
+ var fetchEnvelopeItems = async function({
96
+ accountId,
97
+ api,
98
+ key,
99
+ siteId
100
+ }) {
101
+ if (accountId === void 0) {
102
+ return [];
103
+ }
104
+ try {
105
+ if (key) {
106
+ const envelopeItem = await api.getEnvVar({ accountId, key, siteId });
107
+ return [envelopeItem];
108
+ }
109
+ const envelopeItems = await api.getEnvVars({ accountId, siteId });
110
+ return envelopeItems;
111
+ } catch {
112
+ return [];
113
+ }
114
+ };
115
+ var formatEnvelopeData = ({
116
+ context = "dev",
117
+ envelopeItems = [],
118
+ scope = "any",
119
+ source
120
+ }) => envelopeItems.filter(({ values }) => Boolean(getValueForContext(values, context))).filter(({ scopes }) => scope === "any" ? true : scopes.includes(scope)).sort((left, right) => left.key.toLowerCase() < right.key.toLowerCase() ? -1 : 1).reduce((acc, cur) => {
121
+ const val = getValueForContext(cur.values, context);
122
+ if (val === void 0) {
123
+ throw new TypeError(`failed to locate environment variable value for ${context} context`);
124
+ }
125
+ const { context: itemContext, context_parameter: branch, value } = val;
126
+ return {
127
+ ...acc,
128
+ [cur.key]: {
129
+ context: itemContext,
130
+ branch,
131
+ scopes: cur.scopes,
132
+ sources: [source],
133
+ value
134
+ }
135
+ };
136
+ }, {});
137
+ var getEnvelopeEnv = async ({
138
+ accountId,
139
+ api,
140
+ context = "dev",
141
+ env,
142
+ key = "",
143
+ raw = false,
144
+ scope = "any",
145
+ siteId
146
+ }) => {
147
+ const [accountEnvelopeItems, siteEnvelopeItems] = await Promise.all([
148
+ fetchEnvelopeItems({ api, accountId, key }),
149
+ fetchEnvelopeItems({ api, accountId, key, siteId })
150
+ ]);
151
+ const accountEnv = formatEnvelopeData({ context, envelopeItems: accountEnvelopeItems, scope, source: "account" });
152
+ const siteEnv = formatEnvelopeData({ context, envelopeItems: siteEnvelopeItems, scope, source: "ui" });
153
+ if (raw) {
154
+ const entries = Object.entries({ ...accountEnv, ...siteEnv });
155
+ return entries.reduce(
156
+ (obj, [envVarKey, metadata]) => ({
157
+ ...obj,
158
+ [envVarKey]: metadata.value
159
+ }),
160
+ {}
161
+ );
162
+ }
163
+ const generalEnv = filterEnvBySource(env, "general");
164
+ const internalEnv = filterEnvBySource(env, "internal");
165
+ const addonsEnv = filterEnvBySource(env, "addons");
166
+ const configFileEnv = filterEnvBySource(env, "configFile");
167
+ const includeConfigEnvVars = /any|builds|post[-_]processing/.test(scope);
168
+ return {
169
+ ...generalEnv,
170
+ ...accountEnv,
171
+ ...includeConfigEnvVars ? addonsEnv : {},
172
+ ...siteEnv,
173
+ ...includeConfigEnvVars ? configFileEnv : {},
174
+ ...internalEnv
175
+ };
176
+ };
177
+
44
178
  // src/lib/fs.ts
45
179
  var import_node_fs = require("fs");
46
180
  var isDirectory = async (path3) => {
@@ -112,6 +246,7 @@ var getRuntime = async ({ blobs, deployID, projectRoot, siteID }) => {
112
246
  siteID
113
247
  });
114
248
  return {
249
+ env,
115
250
  stop: async () => {
116
251
  restoreEnvironment(envSnapshot);
117
252
  await blobsServer?.stop();
@@ -122,6 +257,8 @@ var getRuntime = async ({ blobs, deployID, projectRoot, siteID }) => {
122
257
  // src/main.ts
123
258
  var notFoundHandler = async () => new Response("Not found", { status: 404 });
124
259
  var NetlifyDev = class {
260
+ #apiHost;
261
+ #apiScheme;
125
262
  #apiToken;
126
263
  #config;
127
264
  #features;
@@ -130,8 +267,15 @@ var NetlifyDev = class {
130
267
  #runtime;
131
268
  #siteID;
132
269
  constructor(options) {
270
+ if (options.apiURL) {
271
+ const apiURL = new URL(options.apiURL);
272
+ this.#apiHost = apiURL.host;
273
+ this.#apiScheme = apiURL.protocol.slice(0, -1);
274
+ }
275
+ this.#apiToken = options.apiToken;
133
276
  this.#features = {
134
277
  blobs: options.blobs?.enabled !== false,
278
+ environmentVariables: options.environmentVariables?.enabled !== false,
135
279
  functions: options.functions?.enabled !== false,
136
280
  redirects: options.redirects?.enabled !== false,
137
281
  static: options.staticFiles?.enabled !== false
@@ -139,27 +283,12 @@ var NetlifyDev = class {
139
283
  this.#logger = options.logger ?? globalThis.console;
140
284
  this.#projectRoot = options.projectRoot ?? import_node_process2.default.cwd();
141
285
  }
142
- async getConfig() {
143
- const configFilePath = import_node_path2.default.resolve(this.#projectRoot, "netlify.toml");
144
- const configFileExists = await isFile(configFilePath);
145
- const config = await (0, import_config.resolveConfig)({
146
- config: configFileExists ? configFilePath : void 0,
147
- repositoryRoot: this.#projectRoot,
148
- cwd: import_node_process2.default.cwd(),
149
- context: "dev",
150
- siteId: this.#siteID,
151
- token: this.#apiToken,
152
- mode: "cli",
153
- offline: !this.#siteID
154
- });
155
- return config;
156
- }
157
- async handle(request) {
286
+ async handleInEphemeralDirectory(request, destPath) {
158
287
  const userFunctionsPath = this.#config?.config.functionsDirectory ?? import_node_path2.default.join(this.#projectRoot, "netlify/functions");
159
288
  const userFunctionsPathExists = await isDirectory(userFunctionsPath);
160
289
  const functions = this.#features.functions ? new import_dev.FunctionsHandler({
161
290
  config: this.#config,
162
- destPath: import_node_path2.default.join(this.#projectRoot, ".netlify", "functions-serve"),
291
+ destPath,
163
292
  projectRoot: this.#projectRoot,
164
293
  settings: {},
165
294
  siteId: this.#siteID,
@@ -206,23 +335,63 @@ var NetlifyDev = class {
206
335
  return staticMatch.handle();
207
336
  }
208
337
  }
338
+ async getConfig() {
339
+ const configFilePath = import_node_path2.default.resolve(this.#projectRoot, "netlify.toml");
340
+ const configFileExists = await isFile(configFilePath);
341
+ const config = await (0, import_config.resolveConfig)({
342
+ config: configFileExists ? configFilePath : void 0,
343
+ context: "dev",
344
+ cwd: import_node_process2.default.cwd(),
345
+ host: this.#apiHost,
346
+ offline: !this.#siteID,
347
+ mode: "cli",
348
+ repositoryRoot: this.#projectRoot,
349
+ scheme: this.#apiScheme,
350
+ siteId: this.#siteID,
351
+ token: this.#apiToken
352
+ });
353
+ return config;
354
+ }
355
+ async handle(request) {
356
+ const servePath = import_node_path2.default.join(this.#projectRoot, ".netlify", "functions-serve");
357
+ await import_node_fs2.promises.mkdir(servePath, { recursive: true });
358
+ const destPath = await import_node_fs2.promises.mkdtemp(import_node_path2.default.join(servePath, "_"));
359
+ try {
360
+ return await this.handleInEphemeralDirectory(request, destPath);
361
+ } finally {
362
+ try {
363
+ await import_node_fs2.promises.rm(destPath, { force: true, recursive: true });
364
+ } catch {
365
+ }
366
+ }
367
+ }
209
368
  get siteIsLinked() {
210
369
  return Boolean(this.#siteID);
211
370
  }
212
371
  async start() {
213
372
  await (0, import_dev_utils.ensureNetlifyIgnore)(this.#projectRoot, this.#logger);
214
- const apiToken = await (0, import_dev_utils.getAPIToken)();
215
- this.#apiToken = apiToken;
373
+ this.#apiToken = this.#apiToken ?? await (0, import_dev_utils.getAPIToken)();
216
374
  const state = new import_dev_utils.LocalState(this.#projectRoot);
217
375
  const siteID = state.get("siteId");
218
376
  this.#siteID = siteID;
219
- this.#config = await this.getConfig();
220
- this.#runtime = await getRuntime({
377
+ const config = await this.getConfig();
378
+ this.#config = config;
379
+ const runtime = await getRuntime({
221
380
  blobs: Boolean(this.#features.blobs),
222
381
  deployID: "0",
223
382
  projectRoot: this.#projectRoot,
224
383
  siteID: siteID ?? "0"
225
384
  });
385
+ this.#runtime = runtime;
386
+ if (this.#features.environmentVariables && siteID) {
387
+ await injectEnvVariables({
388
+ accountSlug: config?.siteInfo?.account_slug,
389
+ baseVariables: config?.env || {},
390
+ envAPI: runtime.env,
391
+ netlifyAPI: config?.api,
392
+ siteID
393
+ });
394
+ }
226
395
  }
227
396
  async stop() {
228
397
  await this.#runtime?.stop();
package/dist/main.d.cts CHANGED
@@ -9,6 +9,14 @@ interface Features {
9
9
  blobs?: {
10
10
  enabled: boolean;
11
11
  };
12
+ /**
13
+ * Configuration options for environment variables.
14
+ *
15
+ * {@link} https://docs.netlify.com/environment-variables/overview/
16
+ */
17
+ environmentVariables?: {
18
+ enabled: boolean;
19
+ };
12
20
  /**
13
21
  * Configuration options for Netlify Functions.
14
22
  *
@@ -33,12 +41,15 @@ interface Features {
33
41
  };
34
42
  }
35
43
  interface NetlifyDevOptions extends Features {
44
+ apiURL?: string;
45
+ apiToken?: string;
36
46
  logger?: Logger;
37
47
  projectRoot?: string;
38
48
  }
39
49
  declare class NetlifyDev {
40
50
  #private;
41
51
  constructor(options: NetlifyDevOptions);
52
+ private handleInEphemeralDirectory;
42
53
  private getConfig;
43
54
  handle(request: Request): Promise<Response | undefined>;
44
55
  get siteIsLinked(): boolean;
package/dist/main.d.ts CHANGED
@@ -9,6 +9,14 @@ interface Features {
9
9
  blobs?: {
10
10
  enabled: boolean;
11
11
  };
12
+ /**
13
+ * Configuration options for environment variables.
14
+ *
15
+ * {@link} https://docs.netlify.com/environment-variables/overview/
16
+ */
17
+ environmentVariables?: {
18
+ enabled: boolean;
19
+ };
12
20
  /**
13
21
  * Configuration options for Netlify Functions.
14
22
  *
@@ -33,12 +41,15 @@ interface Features {
33
41
  };
34
42
  }
35
43
  interface NetlifyDevOptions extends Features {
44
+ apiURL?: string;
45
+ apiToken?: string;
36
46
  logger?: Logger;
37
47
  projectRoot?: string;
38
48
  }
39
49
  declare class NetlifyDev {
40
50
  #private;
41
51
  constructor(options: NetlifyDevOptions);
52
+ private handleInEphemeralDirectory;
42
53
  private getConfig;
43
54
  handle(request: Request): Promise<Response | undefined>;
44
55
  get siteIsLinked(): boolean;
package/dist/main.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // src/main.ts
2
+ import { promises as fs2 } from "node:fs";
2
3
  import path2 from "node:path";
3
4
  import process2 from "node:process";
4
5
  import { resolveConfig } from "@netlify/config";
@@ -7,6 +8,139 @@ import { FunctionsHandler } from "@netlify/functions/dev";
7
8
  import { RedirectsHandler } from "@netlify/redirects";
8
9
  import { StaticHandler } from "@netlify/static";
9
10
 
11
+ // src/lib/env.ts
12
+ var SUPPORTED_CONTEXTS = ["all", "production", "deploy-preview", "branch-deploy", "dev"];
13
+ var injectEnvVariables = async ({
14
+ accountSlug,
15
+ baseVariables = {},
16
+ envAPI,
17
+ netlifyAPI,
18
+ siteID
19
+ }) => {
20
+ const results = {};
21
+ let variables = baseVariables;
22
+ if (netlifyAPI && accountSlug) {
23
+ variables = await getEnvelopeEnv({
24
+ accountId: accountSlug,
25
+ api: netlifyAPI,
26
+ env: baseVariables,
27
+ siteId: siteID
28
+ });
29
+ }
30
+ for (const [key, variable] of Object.entries(variables)) {
31
+ const existsInProcess = envAPI.has(key);
32
+ const [usedSource, ...overriddenSources] = existsInProcess ? ["process", ...variable.sources] : variable.sources;
33
+ const isInternal = variable.sources.includes("internal");
34
+ const result = {
35
+ isInternal,
36
+ originalValue: envAPI.get(key),
37
+ overriddenSources,
38
+ usedSource
39
+ };
40
+ if (!existsInProcess || isInternal) {
41
+ envAPI.set(key, variable.value);
42
+ }
43
+ results[key] = result;
44
+ }
45
+ return results;
46
+ };
47
+ var getValueForContext = (values, contextOrBranch) => {
48
+ const isSupportedContext = SUPPORTED_CONTEXTS.includes(contextOrBranch);
49
+ if (!isSupportedContext) {
50
+ const valueMatchingAsBranch = values.find((val) => val.context_parameter === contextOrBranch);
51
+ if (valueMatchingAsBranch != null) {
52
+ return valueMatchingAsBranch;
53
+ }
54
+ const valueMatchingContext = values.find((val) => val.context === "all" || val.context === "branch-deploy");
55
+ return valueMatchingContext ?? void 0;
56
+ }
57
+ const valueMatchingAsContext = values.find((val) => val.context === "all" || val.context === contextOrBranch);
58
+ return valueMatchingAsContext ?? void 0;
59
+ };
60
+ var filterEnvBySource = (env, source) => Object.fromEntries(Object.entries(env).filter(([, variable]) => variable.sources[0] === source));
61
+ var fetchEnvelopeItems = async function({
62
+ accountId,
63
+ api,
64
+ key,
65
+ siteId
66
+ }) {
67
+ if (accountId === void 0) {
68
+ return [];
69
+ }
70
+ try {
71
+ if (key) {
72
+ const envelopeItem = await api.getEnvVar({ accountId, key, siteId });
73
+ return [envelopeItem];
74
+ }
75
+ const envelopeItems = await api.getEnvVars({ accountId, siteId });
76
+ return envelopeItems;
77
+ } catch {
78
+ return [];
79
+ }
80
+ };
81
+ var formatEnvelopeData = ({
82
+ context = "dev",
83
+ envelopeItems = [],
84
+ scope = "any",
85
+ source
86
+ }) => envelopeItems.filter(({ values }) => Boolean(getValueForContext(values, context))).filter(({ scopes }) => scope === "any" ? true : scopes.includes(scope)).sort((left, right) => left.key.toLowerCase() < right.key.toLowerCase() ? -1 : 1).reduce((acc, cur) => {
87
+ const val = getValueForContext(cur.values, context);
88
+ if (val === void 0) {
89
+ throw new TypeError(`failed to locate environment variable value for ${context} context`);
90
+ }
91
+ const { context: itemContext, context_parameter: branch, value } = val;
92
+ return {
93
+ ...acc,
94
+ [cur.key]: {
95
+ context: itemContext,
96
+ branch,
97
+ scopes: cur.scopes,
98
+ sources: [source],
99
+ value
100
+ }
101
+ };
102
+ }, {});
103
+ var getEnvelopeEnv = async ({
104
+ accountId,
105
+ api,
106
+ context = "dev",
107
+ env,
108
+ key = "",
109
+ raw = false,
110
+ scope = "any",
111
+ siteId
112
+ }) => {
113
+ const [accountEnvelopeItems, siteEnvelopeItems] = await Promise.all([
114
+ fetchEnvelopeItems({ api, accountId, key }),
115
+ fetchEnvelopeItems({ api, accountId, key, siteId })
116
+ ]);
117
+ const accountEnv = formatEnvelopeData({ context, envelopeItems: accountEnvelopeItems, scope, source: "account" });
118
+ const siteEnv = formatEnvelopeData({ context, envelopeItems: siteEnvelopeItems, scope, source: "ui" });
119
+ if (raw) {
120
+ const entries = Object.entries({ ...accountEnv, ...siteEnv });
121
+ return entries.reduce(
122
+ (obj, [envVarKey, metadata]) => ({
123
+ ...obj,
124
+ [envVarKey]: metadata.value
125
+ }),
126
+ {}
127
+ );
128
+ }
129
+ const generalEnv = filterEnvBySource(env, "general");
130
+ const internalEnv = filterEnvBySource(env, "internal");
131
+ const addonsEnv = filterEnvBySource(env, "addons");
132
+ const configFileEnv = filterEnvBySource(env, "configFile");
133
+ const includeConfigEnvVars = /any|builds|post[-_]processing/.test(scope);
134
+ return {
135
+ ...generalEnv,
136
+ ...accountEnv,
137
+ ...includeConfigEnvVars ? addonsEnv : {},
138
+ ...siteEnv,
139
+ ...includeConfigEnvVars ? configFileEnv : {},
140
+ ...internalEnv
141
+ };
142
+ };
143
+
10
144
  // src/lib/fs.ts
11
145
  import { promises as fs } from "node:fs";
12
146
  var isDirectory = async (path3) => {
@@ -78,6 +212,7 @@ var getRuntime = async ({ blobs, deployID, projectRoot, siteID }) => {
78
212
  siteID
79
213
  });
80
214
  return {
215
+ env,
81
216
  stop: async () => {
82
217
  restoreEnvironment(envSnapshot);
83
218
  await blobsServer?.stop();
@@ -88,6 +223,8 @@ var getRuntime = async ({ blobs, deployID, projectRoot, siteID }) => {
88
223
  // src/main.ts
89
224
  var notFoundHandler = async () => new Response("Not found", { status: 404 });
90
225
  var NetlifyDev = class {
226
+ #apiHost;
227
+ #apiScheme;
91
228
  #apiToken;
92
229
  #config;
93
230
  #features;
@@ -96,8 +233,15 @@ var NetlifyDev = class {
96
233
  #runtime;
97
234
  #siteID;
98
235
  constructor(options) {
236
+ if (options.apiURL) {
237
+ const apiURL = new URL(options.apiURL);
238
+ this.#apiHost = apiURL.host;
239
+ this.#apiScheme = apiURL.protocol.slice(0, -1);
240
+ }
241
+ this.#apiToken = options.apiToken;
99
242
  this.#features = {
100
243
  blobs: options.blobs?.enabled !== false,
244
+ environmentVariables: options.environmentVariables?.enabled !== false,
101
245
  functions: options.functions?.enabled !== false,
102
246
  redirects: options.redirects?.enabled !== false,
103
247
  static: options.staticFiles?.enabled !== false
@@ -105,27 +249,12 @@ var NetlifyDev = class {
105
249
  this.#logger = options.logger ?? globalThis.console;
106
250
  this.#projectRoot = options.projectRoot ?? process2.cwd();
107
251
  }
108
- async getConfig() {
109
- const configFilePath = path2.resolve(this.#projectRoot, "netlify.toml");
110
- const configFileExists = await isFile(configFilePath);
111
- const config = await resolveConfig({
112
- config: configFileExists ? configFilePath : void 0,
113
- repositoryRoot: this.#projectRoot,
114
- cwd: process2.cwd(),
115
- context: "dev",
116
- siteId: this.#siteID,
117
- token: this.#apiToken,
118
- mode: "cli",
119
- offline: !this.#siteID
120
- });
121
- return config;
122
- }
123
- async handle(request) {
252
+ async handleInEphemeralDirectory(request, destPath) {
124
253
  const userFunctionsPath = this.#config?.config.functionsDirectory ?? path2.join(this.#projectRoot, "netlify/functions");
125
254
  const userFunctionsPathExists = await isDirectory(userFunctionsPath);
126
255
  const functions = this.#features.functions ? new FunctionsHandler({
127
256
  config: this.#config,
128
- destPath: path2.join(this.#projectRoot, ".netlify", "functions-serve"),
257
+ destPath,
129
258
  projectRoot: this.#projectRoot,
130
259
  settings: {},
131
260
  siteId: this.#siteID,
@@ -172,23 +301,63 @@ var NetlifyDev = class {
172
301
  return staticMatch.handle();
173
302
  }
174
303
  }
304
+ async getConfig() {
305
+ const configFilePath = path2.resolve(this.#projectRoot, "netlify.toml");
306
+ const configFileExists = await isFile(configFilePath);
307
+ const config = await resolveConfig({
308
+ config: configFileExists ? configFilePath : void 0,
309
+ context: "dev",
310
+ cwd: process2.cwd(),
311
+ host: this.#apiHost,
312
+ offline: !this.#siteID,
313
+ mode: "cli",
314
+ repositoryRoot: this.#projectRoot,
315
+ scheme: this.#apiScheme,
316
+ siteId: this.#siteID,
317
+ token: this.#apiToken
318
+ });
319
+ return config;
320
+ }
321
+ async handle(request) {
322
+ const servePath = path2.join(this.#projectRoot, ".netlify", "functions-serve");
323
+ await fs2.mkdir(servePath, { recursive: true });
324
+ const destPath = await fs2.mkdtemp(path2.join(servePath, "_"));
325
+ try {
326
+ return await this.handleInEphemeralDirectory(request, destPath);
327
+ } finally {
328
+ try {
329
+ await fs2.rm(destPath, { force: true, recursive: true });
330
+ } catch {
331
+ }
332
+ }
333
+ }
175
334
  get siteIsLinked() {
176
335
  return Boolean(this.#siteID);
177
336
  }
178
337
  async start() {
179
338
  await ensureNetlifyIgnore(this.#projectRoot, this.#logger);
180
- const apiToken = await getAPIToken();
181
- this.#apiToken = apiToken;
339
+ this.#apiToken = this.#apiToken ?? await getAPIToken();
182
340
  const state = new LocalState(this.#projectRoot);
183
341
  const siteID = state.get("siteId");
184
342
  this.#siteID = siteID;
185
- this.#config = await this.getConfig();
186
- this.#runtime = await getRuntime({
343
+ const config = await this.getConfig();
344
+ this.#config = config;
345
+ const runtime = await getRuntime({
187
346
  blobs: Boolean(this.#features.blobs),
188
347
  deployID: "0",
189
348
  projectRoot: this.#projectRoot,
190
349
  siteID: siteID ?? "0"
191
350
  });
351
+ this.#runtime = runtime;
352
+ if (this.#features.environmentVariables && siteID) {
353
+ await injectEnvVariables({
354
+ accountSlug: config?.siteInfo?.account_slug,
355
+ baseVariables: config?.env || {},
356
+ envAPI: runtime.env,
357
+ netlifyAPI: config?.api,
358
+ siteID
359
+ });
360
+ }
192
361
  }
193
362
  async stop() {
194
363
  await this.#runtime?.stop();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/dev",
3
- "version": "2.2.2",
3
+ "version": "2.3.1",
4
4
  "description": "Emulation of the Netlify environment for local development",
5
5
  "type": "module",
6
6
  "engines": {
@@ -46,18 +46,19 @@
46
46
  },
47
47
  "author": "Netlify Inc.",
48
48
  "devDependencies": {
49
- "@netlify/types": "1.1.1",
49
+ "@netlify/api": "^14.0.2",
50
+ "@netlify/types": "1.2.0",
50
51
  "tmp-promise": "^3.0.3",
51
52
  "tsup": "^8.0.0",
52
53
  "vitest": "^3.0.0"
53
54
  },
54
55
  "dependencies": {
55
- "@netlify/blobs": "9.1.1",
56
- "@netlify/config": "^22.0.1",
57
- "@netlify/dev-utils": "2.1.1",
58
- "@netlify/functions": "3.1.8",
59
- "@netlify/redirects": "1.1.3",
60
- "@netlify/runtime": "2.2.1",
61
- "@netlify/static": "1.1.3"
56
+ "@netlify/blobs": "9.1.2",
57
+ "@netlify/config": "^23.0.7",
58
+ "@netlify/dev-utils": "2.2.0",
59
+ "@netlify/functions": "3.1.10",
60
+ "@netlify/redirects": "1.1.4",
61
+ "@netlify/runtime": "2.2.2",
62
+ "@netlify/static": "1.1.4"
62
63
  }
63
64
  }