netlify 27.10.1 → 27.11.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.
Files changed (45) hide show
  1. package/dist/commands/agents/agents-create.js +19 -33
  2. package/dist/commands/base-command.d.ts +1 -1
  3. package/dist/commands/base-command.js +8 -11
  4. package/dist/commands/clone/clone.js +5 -10
  5. package/dist/commands/completion/completion.js +5 -9
  6. package/dist/commands/create/create-action.js +16 -28
  7. package/dist/commands/database/db-init.js +17 -31
  8. package/dist/commands/database/db-migration-new.js +13 -23
  9. package/dist/commands/database/db-migration-pull.js +5 -9
  10. package/dist/commands/database/db-reset.js +5 -9
  11. package/dist/commands/deploy/deploy.js +23 -36
  12. package/dist/commands/env/env-list.js +5 -9
  13. package/dist/commands/functions/functions-create.js +35 -71
  14. package/dist/commands/functions/functions-invoke.js +5 -10
  15. package/dist/commands/init/init.js +12 -25
  16. package/dist/commands/link/link.js +26 -52
  17. package/dist/commands/logs/build.js +2 -4
  18. package/dist/commands/main.js +10 -17
  19. package/dist/commands/recipes/recipes.js +6 -16
  20. package/dist/commands/sites/sites-create.js +12 -22
  21. package/dist/commands/sites/sites-delete.js +3 -7
  22. package/dist/commands/switch/switch.js +5 -9
  23. package/dist/lib/edge-functions/editor-helper.js +3 -5
  24. package/dist/recipes/ai-context/index.js +9 -17
  25. package/dist/recipes/blobs-migrate/index.js +3 -7
  26. package/dist/recipes/vscode/index.js +6 -10
  27. package/dist/utils/build-info.js +3 -6
  28. package/dist/utils/deploy/deploy-site.js +21 -12
  29. package/dist/utils/deploy/upload-files.js +6 -2
  30. package/dist/utils/deploy/util.d.ts +1 -0
  31. package/dist/utils/deploy/util.js +1 -0
  32. package/dist/utils/exit-codes.d.ts +2 -0
  33. package/dist/utils/exit-codes.js +2 -0
  34. package/dist/utils/gh-auth.js +10 -22
  35. package/dist/utils/github-api.d.ts +22 -0
  36. package/dist/utils/github-api.js +47 -0
  37. package/dist/utils/init/config-github.js +29 -45
  38. package/dist/utils/init/config-manual.js +8 -28
  39. package/dist/utils/init/utils.js +14 -38
  40. package/dist/utils/prompts/confirm-prompt.js +4 -15
  41. package/dist/utils/proxy.js +7 -48
  42. package/dist/utils/redirects.js +3 -2
  43. package/dist/utils/rules-proxy.js +31 -29
  44. package/dist/utils/types.d.ts +2 -2
  45. package/package.json +10 -14
@@ -1,7 +1,7 @@
1
1
  import { writeFile } from 'fs/promises';
2
2
  import path from 'path';
3
3
  import cleanDeep from 'clean-deep';
4
- import inquirer from 'inquirer';
4
+ import { confirm, input } from '@inquirer/prompts';
5
5
  import { fileExistsAsync } from '../../lib/fs.js';
6
6
  import { normalizeBackslash } from '../../lib/path.js';
7
7
  import { detectBuildSettings } from '../build-info.js';
@@ -41,30 +41,6 @@ const normalizeSettings = (settings, config, command) => {
41
41
  recommendedPlugins,
42
42
  };
43
43
  };
44
- const getPromptInputs = ({ defaultBaseDir, defaultBuildCmd, defaultBuildDir, }) => {
45
- const inputs = [
46
- defaultBaseDir !== '' && {
47
- type: 'input',
48
- name: 'baseDir',
49
- message: 'Base directory `(blank for current dir):',
50
- default: defaultBaseDir,
51
- },
52
- {
53
- type: 'input',
54
- name: 'buildCmd',
55
- message: 'Your build command (hugo build/yarn run build/etc):',
56
- filter: (val) => (val === '' ? '# no build command' : val),
57
- default: defaultBuildCmd,
58
- },
59
- {
60
- type: 'input',
61
- name: 'buildDir',
62
- message: 'Directory to deploy (blank for current dir):',
63
- default: defaultBuildDir,
64
- },
65
- ].filter(Boolean);
66
- return inputs.filter(Boolean);
67
- };
68
44
  export const getBuildSettings = async ({ command, config, }) => {
69
45
  const settings = await detectBuildSettings(command);
70
46
  const setting = settings.length > 0 ? settings[0] : {};
@@ -79,11 +55,15 @@ export const getBuildSettings = async ({ command, config, }) => {
79
55
  log('For each setting, press Enter to accept the default or provide your own value.');
80
56
  log();
81
57
  }
82
- const { baseDir, buildCmd, buildDir } = await inquirer.prompt(getPromptInputs({
83
- defaultBaseDir,
84
- defaultBuildCmd,
85
- defaultBuildDir,
86
- }));
58
+ const baseDir = defaultBaseDir === ''
59
+ ? undefined
60
+ : await input({ message: 'Base directory `(blank for current dir):', default: defaultBaseDir });
61
+ const buildCmdInput = await input({
62
+ message: 'Your build command (hugo build/yarn run build/etc):',
63
+ default: defaultBuildCmd,
64
+ });
65
+ const buildCmd = buildCmdInput === '' ? '# no build command' : buildCmdInput;
66
+ const buildDir = await input({ message: 'Directory to deploy (blank for current dir):', default: defaultBuildDir });
87
67
  const pluginsToInstall = recommendedPlugins.map((plugin) => ({ package: plugin }));
88
68
  const normalizedBaseDir = baseDir ? normalizeBackslash(baseDir) : undefined;
89
69
  return { baseDir: normalizedBaseDir, buildCmd, buildDir, functionsDir: defaultFunctionsDir, pluginsToInstall };
@@ -123,14 +103,10 @@ export const saveNetlifyToml = async ({ baseDir, buildCmd, buildDir, config, con
123
103
  if (configPath === undefined && Object.keys(cleanDeep(config)).length !== 0) {
124
104
  return;
125
105
  }
126
- const { makeNetlifyTOML } = await inquirer.prompt([
127
- {
128
- type: 'confirm',
129
- name: 'makeNetlifyTOML',
130
- message: 'No netlify.toml detected. Would you like to create one with these build settings?',
131
- default: true,
132
- },
133
- ]);
106
+ const makeNetlifyTOML = await confirm({
107
+ message: 'No netlify.toml detected. Would you like to create one with these build settings?',
108
+ default: true,
109
+ });
134
110
  if (makeNetlifyTOML) {
135
111
  try {
136
112
  await writeFile(tomlPath, getNetlifyToml({ command: buildCmd, publish: buildDir, functions: functionsDir }), 'utf-8');
@@ -1,20 +1,9 @@
1
- import inquirer from 'inquirer';
1
+ import { confirm } from '@inquirer/prompts';
2
2
  import { log, exit } from '../command-helpers.js';
3
3
  export const confirmPrompt = async (message) => {
4
- try {
5
- const { confirm } = await inquirer.prompt({
6
- type: 'confirm',
7
- name: 'confirm',
8
- message,
9
- default: false,
10
- });
11
- log();
12
- if (!confirm) {
13
- exit();
14
- }
15
- }
16
- catch (error) {
17
- console.error(error);
4
+ const confirmed = await confirm({ message, default: false });
5
+ log();
6
+ if (!confirmed) {
18
7
  exit();
19
8
  }
20
9
  };
@@ -16,13 +16,10 @@ import { ImageHandler } from '@netlify/images';
16
16
  import { ServerHandler } from '@netlify/server-dev';
17
17
  import { runBeforeProcessExit } from './shell.js';
18
18
  import contentType from 'content-type';
19
- import { parseCookie } from 'cookie';
20
- import { getProperty } from 'dot-prop';
21
19
  import generateETag from 'etag';
22
20
  import getAvailablePort from 'get-port';
23
21
  import httpProxy from 'http-proxy';
24
22
  import { createProxyMiddleware } from 'http-proxy-middleware';
25
- import { jwtDecode } from 'jwt-decode';
26
23
  import { locatePath } from 'locate-path';
27
24
  import { throttle } from './object-utilities.js';
28
25
  import pFilter from 'p-filter';
@@ -126,7 +123,7 @@ const isEndpointExists = async function (endpoint, origin) {
126
123
  }
127
124
  };
128
125
  const isExternal = function (match) {
129
- return 'to' in match && /^https?:\/\//.exec(match.to) != null;
126
+ return match.type === 'match' && /^https?:\/\//.exec(match.to) != null;
130
127
  };
131
128
  const stripOrigin = function ({ hash, pathname, search }) {
132
129
  return `${pathname}${search}${hash}`;
@@ -193,7 +190,7 @@ const serveRedirect = async function ({ env, functionsRegistry, imageProxy, matc
193
190
  return proxy.web(req, res, options);
194
191
  options = options || req.proxyOptions || {};
195
192
  options.match = null;
196
- if (match.force404) {
193
+ if (match.type === 'forcedNotFound') {
197
194
  res.writeHead(404);
198
195
  res.end(await render404(options.publicFolder));
199
196
  return;
@@ -203,8 +200,9 @@ const serveRedirect = async function ({ env, functionsRegistry, imageProxy, matc
203
200
  req.headers[key] = value;
204
201
  });
205
202
  }
206
- if (match.signingSecret) {
207
- const signingSecretVar = env[match.signingSecret];
203
+ const signingSecretName = match.signer?.jwtSecret;
204
+ if (signingSecretName) {
205
+ const signingSecretVar = env[signingSecretName];
208
206
  if (signingSecretVar) {
209
207
  req.headers['x-nf-sign'] = signRedirect({
210
208
  deployContext: 'dev',
@@ -214,7 +212,7 @@ const serveRedirect = async function ({ env, functionsRegistry, imageProxy, matc
214
212
  });
215
213
  }
216
214
  else {
217
- log(NETLIFYDEVWARN, `Could not sign redirect because environment variable ${chalk.yellow(match.signingSecret)} is not set`);
215
+ log(NETLIFYDEVWARN, `Could not sign redirect because environment variable ${chalk.yellow(signingSecretName)} is not set`);
218
216
  }
219
217
  }
220
218
  if (isFunction(options.functionsPort, req.url)) {
@@ -226,44 +224,6 @@ const serveRedirect = async function ({ env, functionsRegistry, imageProxy, matc
226
224
  return;
227
225
  }
228
226
  const originalURL = req.url;
229
- if (match.exceptions && match.exceptions.JWT) {
230
- // Some values of JWT can start with :, so, make sure to normalize them
231
- const expectedRoles = new Set(match.exceptions.JWT.split(',').map((value) => (value.startsWith(':') ? value.slice(1) : value)));
232
- const cookieValues = parseCookie(req.headers.cookie || '');
233
- const token = cookieValues.nf_jwt;
234
- // Serve not found by default
235
- req.url = '/.netlify/non-existent-path';
236
- if (token) {
237
- let jwtValue = {};
238
- try {
239
- jwtValue = jwtDecode(token) || {};
240
- }
241
- catch (error) {
242
- // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'.
243
- console.warn(NETLIFYDEVWARN, 'Error while decoding JWT provided in request', error.message);
244
- res.writeHead(400);
245
- res.end('Invalid JWT provided. Please see logs for more info.');
246
- return;
247
- }
248
- // @ts-expect-error TS(2339) FIXME: Property 'exp' does not exist on type '{}'.
249
- if ((jwtValue.exp || 0) < Math.round(Date.now() / MILLISEC_TO_SEC)) {
250
- console.warn(NETLIFYDEVWARN, 'Expired JWT provided in request', req.url);
251
- }
252
- else {
253
- const presentedRoles = getProperty(jwtValue, options.jwtRolePath) || [];
254
- if (!Array.isArray(presentedRoles)) {
255
- console.warn(NETLIFYDEVWARN, `Invalid roles value provided in JWT ${options.jwtRolePath}`, presentedRoles);
256
- res.writeHead(400);
257
- res.end('Invalid JWT provided. Please see logs for more info.');
258
- return;
259
- }
260
- // Restore the URL if everything is correct
261
- if (presentedRoles.some((pr) => expectedRoles.has(pr))) {
262
- req.url = originalURL;
263
- }
264
- }
265
- }
266
- }
267
227
  const reqUrl = reqToURL(req, req.url);
268
228
  const isHiddenProxy = match.proxyHeaders &&
269
229
  Object.entries(match.proxyHeaders).some(([key, val]) => key.toLowerCase() === 'x-nf-hidden-proxy' && val === 'true');
@@ -363,7 +323,6 @@ const serveRedirect = async function ({ env, functionsRegistry, imageProxy, matc
363
323
  const reqToURL = function (req, pathname) {
364
324
  return new URL(pathname, `${req.protocol || (req.headers.scheme && `${req.headers.scheme}:`) || 'http:'}//${req.headers.host || req.hostname}`);
365
325
  };
366
- const MILLISEC_TO_SEC = 1e3;
367
326
  const initializeProxy = async function ({ config, configPath, distDir, env, host, imageProxy, port, projectDir, siteInfo, }) {
368
327
  const proxy = httpProxy.createProxyServer({
369
328
  selfHandleResponse: true,
@@ -836,7 +795,7 @@ export const startProxy = async function ({ accountId, addonsUrls, aiGatewayCont
836
795
  }
837
796
  }
838
797
  const match = await rewriter(req);
839
- if (match && !match.force404 && isExternal(match)) {
798
+ if (match && isExternal(match)) {
840
799
  const reqUrl = reqToURL(req, req.url);
841
800
  const dest = new URL(match.to, `${reqUrl.protocol}//${reqUrl.host}`);
842
801
  const destURL = stripOrigin(dest);
@@ -26,10 +26,11 @@ const handleRedirectParsingErrors = function (errors) {
26
26
  const getErrorMessage = function ({ message }) {
27
27
  return message;
28
28
  };
29
- // `netlify-redirector` does not handle the same shape as the backend:
30
- // - `from` is called `origin`
29
+ // `@netlify/redirect-matcher` reads a different rule shape from the backend's:
31
30
  // - `query` is called `params`
32
31
  // - `conditions.role|country|language` are capitalized
32
+ // - `signed` becomes `sign.jwt_secret`
33
+ // The matcher reads the source path from `path`; `origin` is ignored.
33
34
  const normalizeRedirect = function ({
34
35
  // @ts-expect-error TS(7031) FIXME: Binding element 'country' implicitly has an 'any' ... Remove this comment to see the full error message
35
36
  conditions: { country, language, role, ...conditions },
@@ -1,10 +1,10 @@
1
1
  import path from 'path';
2
+ import { createMatcher } from '@netlify/redirect-matcher';
2
3
  import chokidar from 'chokidar';
3
4
  import { parseCookie } from 'cookie';
4
- import redirector from 'netlify-redirector';
5
5
  import pFilter from 'p-filter';
6
6
  import { fileExistsAsync } from '../lib/fs.js';
7
- import { NETLIFYDEVLOG } from './command-helpers.js';
7
+ import { NETLIFYDEVERR, NETLIFYDEVLOG, log } from './command-helpers.js';
8
8
  import { parseRedirects } from './redirects.js';
9
9
  const watchers = [];
10
10
  export const onChanges = function (files, listener) {
@@ -37,22 +37,35 @@ export const createRewriter = async function ({ config, configPath, distDir, geo
37
37
  const existingRedirectsFiles = await pFilter(watchedRedirectFiles, fileExistsAsync);
38
38
  console.log(`${NETLIFYDEVLOG} Reloading redirect rules from`, existingRedirectsFiles.map((redirectFile) => path.relative(projectDir, redirectFile)));
39
39
  redirects = await parseRedirects({ config, redirectsFiles, configPath });
40
+ // Not closed: a request may still hold the previous matcher. The package
41
+ // frees it once it is garbage-collected.
40
42
  matcher = null;
41
43
  });
42
- const getMatcher = async () => {
43
- if (matcher)
44
- return matcher;
45
- if (redirects.length !== 0) {
46
- return (matcher = await redirector.parseJSON(JSON.stringify(redirects), {
47
- jwtSecret,
48
- jwtRoleClaim,
49
- }));
44
+ const buildMatcher = async () => {
45
+ // Without rules, skip compiling the matcher's WebAssembly module.
46
+ if (redirects.length === 0) {
47
+ return { match: () => null };
50
48
  }
51
- return {
52
- match() {
53
- return null;
54
- },
55
- };
49
+ const built = await createMatcher(redirects, { jwtSecret, jwtRoleClaim });
50
+ if (built.parseErrors.length !== 0) {
51
+ log(NETLIFYDEVERR, `Redirects matcher errors:\n${built.parseErrors.map(({ message }) => message).join('\n\n')}`);
52
+ }
53
+ return built;
54
+ };
55
+ // The promise is cached, not the matcher, so concurrent requests share one
56
+ // build and a reload mid-build cannot cache a matcher of the old rules.
57
+ const getMatcher = () => {
58
+ if (!matcher) {
59
+ const build = buildMatcher();
60
+ matcher = build;
61
+ // A failed build is retried by the next request, unless a reload has
62
+ // already replaced it with a newer one.
63
+ build.catch(() => {
64
+ if (matcher === build)
65
+ matcher = null;
66
+ });
67
+ }
68
+ return matcher;
56
69
  };
57
70
  return async function rewriter(req) {
58
71
  const matcherFunc = await getMatcher();
@@ -63,25 +76,14 @@ export const createRewriter = async function ({ config, configPath, distDir, geo
63
76
  'x-country': cookieValues.nf_country || geoCountry || 'us',
64
77
  ...req.headers,
65
78
  };
66
- // Definition: https://github.com/netlify/libredirect/blob/e81bbeeff9f7c260a5fb74cad296ccc67a92325b/node/src/redirects.cpp#L28-L60
67
- const matchReq = {
79
+ return matcherFunc.match({
68
80
  scheme: reqUrl.protocol.replace(/:.*$/, ''),
69
81
  host: reqUrl.hostname,
70
82
  path: decodeURIComponent(reqUrl.pathname),
71
83
  query: reqUrl.search.slice(1),
72
84
  headers,
73
- cookieValues,
74
- getHeader: (name) => {
75
- const val = headers[name.toLowerCase()];
76
- if (Array.isArray(val)) {
77
- return val[0];
78
- }
79
- return val || '';
80
- },
81
- getCookie: (key) => cookieValues[key] || '',
82
- };
83
- const match = matcherFunc.match(matchReq);
84
- return match;
85
+ cookies: cookieValues,
86
+ });
85
87
  };
86
88
  };
87
89
  //# sourceMappingURL=rules-proxy.js.map
@@ -1,7 +1,7 @@
1
1
  import type { Buffer } from 'buffer';
2
2
  import type { IncomingMessage } from 'http';
3
3
  import type { PollingStrategy } from '@netlify/build-info';
4
- import type { Match } from 'netlify-redirector';
4
+ import type { MatchResult } from '@netlify/redirect-matcher';
5
5
  export type { GlobalConfigStore } from '@netlify/dev-utils';
6
6
  export type { LocalState } from '@netlify/dev-utils';
7
7
  export type FrameworkNames = '#static' | '#auto' | '#custom' | string;
@@ -63,7 +63,7 @@ export interface Request extends IncomingMessage {
63
63
  protocol?: string;
64
64
  hostname?: string;
65
65
  }
66
- export type Rewriter = (req: Request) => Promise<Match | null>;
66
+ export type Rewriter = (req: Request) => Promise<MatchResult | null>;
67
67
  export interface SiteInfo {
68
68
  account_id?: string | undefined;
69
69
  account_name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "netlify",
3
- "version": "27.10.1",
3
+ "version": "27.11.0",
4
4
  "description": "Netlify command line tool",
5
5
  "keywords": [
6
6
  "api",
@@ -52,24 +52,25 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@fastify/static": "^10.0.0",
55
+ "@inquirer/prompts": "^8.7.2",
55
56
  "@netlify/ai": "^1.0.1",
56
57
  "@netlify/api": "^15.1.2",
57
- "@netlify/blobs": "^11.1.0",
58
- "@netlify/build": "^37.3.1",
58
+ "@netlify/blobs": "^11.1.3",
59
+ "@netlify/build": "^37.3.3",
59
60
  "@netlify/build-info": "^11.3.0",
60
- "@netlify/config": "^25.2.5",
61
- "@netlify/dev": "^5.1.2",
62
- "@netlify/dev-utils": "^6.0.1",
61
+ "@netlify/config": "^25.2.6",
62
+ "@netlify/dev": "^5.1.6",
63
+ "@netlify/dev-utils": "^6.0.3",
63
64
  "@netlify/edge-bundler": "^16.1.1",
64
65
  "@netlify/edge-functions": "^4.0.0",
65
66
  "@netlify/edge-functions-bootstrap": "^3.2.0",
66
67
  "@netlify/headers-parser": "^10.1.1",
67
68
  "@netlify/images": "^2.0.1",
68
69
  "@netlify/local-functions-proxy": "^2.0.3",
70
+ "@netlify/redirect-matcher": "^0.4.2",
69
71
  "@netlify/redirect-parser": "^16.1.1",
70
72
  "@netlify/server-dev": "^0.1.1",
71
- "@netlify/zip-it-and-ship-it": "^16.2.1",
72
- "@octokit/rest": "^22.0.0",
73
+ "@netlify/zip-it-and-ship-it": "^16.2.3",
73
74
  "@opentelemetry/api": "~1.9.0",
74
75
  "@pnpm/tabtab": "^0.5.4",
75
76
  "ansi-escapes": "^7.3.0",
@@ -88,7 +89,6 @@
88
89
  "cron-parser": "^5.0.0",
89
90
  "debug": "^4.4.3",
90
91
  "decache": "^4.6.2",
91
- "dot-prop": "^10.1.0",
92
92
  "dotenv": "^17.3.1",
93
93
  "env-paths": "^4.0.0",
94
94
  "envinfo": "^7.21.0",
@@ -107,8 +107,6 @@
107
107
  "http-proxy": "^1.18.1",
108
108
  "http-proxy-middleware": "^3.0.5",
109
109
  "https-proxy-agent": "^8.0.0",
110
- "inquirer": "^8.2.7",
111
- "inquirer-autocomplete-prompt": "^1.4.0",
112
110
  "is-docker": "^4.0.0",
113
111
  "is-stream": "^4.0.1",
114
112
  "is-wsl": "^3.1.0",
@@ -123,7 +121,6 @@
123
121
  "modern-tar": "^0.8.0",
124
122
  "multiparty": "^4.2.3",
125
123
  "nanospinner": "^1.2.2",
126
- "netlify-redirector": "^0.5.0",
127
124
  "node-fetch": "^3.3.2",
128
125
  "normalize-package-data": "^7.0.1",
129
126
  "open": "^11.0.0",
@@ -168,8 +165,6 @@
168
165
  "@types/express": "^5.0.6",
169
166
  "@types/folder-walker": "^3.2.5",
170
167
  "@types/gitconfiglocal": "^2.0.3",
171
- "@types/inquirer": "^9.0.9",
172
- "@types/inquirer-autocomplete-prompt": "^3.0.3",
173
168
  "@types/jsonwebtoken": "^9.0.10",
174
169
  "@types/lodash.shuffle": "^4.2.9",
175
170
  "@types/multiparty": "^4.2.1",
@@ -192,6 +187,7 @@
192
187
  "c8": "^10.1.3",
193
188
  "cheerio": "^1.1.2",
194
189
  "dedent": "^1.7.2",
190
+ "dot-prop": "^10.1.0",
195
191
  "eslint": "^9.36.0",
196
192
  "eslint-config-prettier": "^10.1.8",
197
193
  "eslint-plugin-n": "^17.23.1",