netlify-cli 27.6.0 → 27.8.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.
@@ -2,7 +2,12 @@ import { getDrivingAgent } from './agent-detection.js';
2
2
  // By contract these two hold only a non-sensitive agent name[@version]. Every other marker's value is a flag,
3
3
  // a session or run id, or a path, none of which may reach a URL.
4
4
  const SOURCES_WITH_ANNOUNCED_VALUE = new Set(['NETLIFY_AGENT', 'AI_AGENT']);
5
+ const LOGIN_SOURCES = new Set(['mcp']);
5
6
  const sanitizeUtmTerm = (raw) => raw.replace(/[^A-Za-z0-9_.:@-]/g, '').slice(0, 64);
7
+ const getUtmSource = (env) => {
8
+ const source = env.NETLIFY_LOGIN_SOURCE;
9
+ return source !== undefined && LOGIN_SOURCES.has(source) ? source : 'cli';
10
+ };
6
11
  const getUtmTerm = (source, env) => {
7
12
  const value = SOURCES_WITH_ANNOUNCED_VALUE.has(source) ? env[source] : undefined;
8
13
  return sanitizeUtmTerm(value ? `${source}:${value}` : source);
@@ -12,7 +17,7 @@ export const buildAuthorizeUrl = (ticketId, env = process.env) => {
12
17
  const params = new URLSearchParams({
13
18
  response_type: 'ticket',
14
19
  ticket: ticketId,
15
- utm_source: 'cli',
20
+ utm_source: getUtmSource(env),
16
21
  utm_campaign: 'integrations',
17
22
  });
18
23
  const agent = getDrivingAgent(env);
@@ -4,7 +4,7 @@ import type { $TSFixMe, NetlifyOptions } from '../commands/types.js';
4
4
  import type { BlobsContextWithEdgeAccess } from '../lib/blobs/blobs.js';
5
5
  import type { FunctionsRegistry } from '../lib/functions/registry.js';
6
6
  import { type NormalizedCachedConfigConfig } from './command-helpers.js';
7
- import type { LocalState } from './types.js';
7
+ import type { LocalState, SiteInfo } from './types.js';
8
8
  import type { ServerSettings } from './types.js';
9
9
  interface InspectSettings {
10
10
  enabled: boolean;
@@ -31,7 +31,7 @@ export declare const startProxyServer: ({ accountId, addonsUrls, aiGatewayContex
31
31
  settings: ServerSettings;
32
32
  offline: boolean;
33
33
  site: $TSFixMe;
34
- siteInfo: $TSFixMe;
34
+ siteInfo: SiteInfo;
35
35
  projectDir: string;
36
36
  repositoryRoot?: string;
37
37
  state: LocalState;
@@ -2,7 +2,7 @@ import type { AIGatewayContext } from '@netlify/ai/bootstrap';
2
2
  import type { BaseCommand } from '../commands/index.js';
3
3
  import type { $TSFixMe } from '../commands/types.js';
4
4
  import { type NormalizedCachedConfigConfig } from './command-helpers.js';
5
- import type { ServerSettings } from './types.js';
5
+ import type { ServerSettings, SiteInfo } from './types.js';
6
6
  export declare const getProxyUrl: (settings: Pick<ServerSettings, 'https' | 'port'>) => string;
7
7
  export declare const startProxy: ({ accountId, addonsUrls, aiGatewayContext, api, blobsContext, command, config, configPath, debug, disableEdgeFunctions, env, functionsRegistry, geoCountry, geolocationMode, getUpdatedConfig, inspectSettings, offline, projectDir, repositoryRoot, settings, siteInfo, state, watchIgnore, deployEnvironment, }: {
8
8
  command: BaseCommand;
@@ -11,6 +11,7 @@ export declare const startProxy: ({ accountId, addonsUrls, aiGatewayContext, api
11
11
  disableEdgeFunctions: boolean;
12
12
  getUpdatedConfig: () => Promise<NormalizedCachedConfigConfig>;
13
13
  aiGatewayContext?: AIGatewayContext | null;
14
+ siteInfo?: SiteInfo;
14
15
  watchIgnore: string[];
15
16
  deployEnvironment: {
16
17
  key: string;
@@ -11,8 +11,10 @@ import process from 'process';
11
11
  import url from 'url';
12
12
  import util from 'util';
13
13
  import zlib from 'zlib';
14
- import { renderFunctionErrorPage } from '@netlify/dev-utils';
14
+ import { FileWatcher, fromWebResponse, mockLocation, renderFunctionErrorPage } from '@netlify/dev-utils';
15
15
  import { ImageHandler } from '@netlify/images';
16
+ import { ServerHandler } from '@netlify/server-dev';
17
+ import { runBeforeProcessExit } from './shell.js';
16
18
  import contentType from 'content-type';
17
19
  import { parseCookie } from 'cookie';
18
20
  import { getProperty } from 'dot-prop';
@@ -607,7 +609,7 @@ const initializeProxy = async function ({ config, configPath, distDir, env, host
607
609
  };
608
610
  return handlers;
609
611
  };
610
- const onRequest = async ({ addonsUrls, api, edgeFunctionsProxy, env, functionsRegistry, functionsServer, imageProxy, proxy, rewriter, settings, siteInfo, }, req, res) => {
612
+ const onRequest = async ({ addonsUrls, api, edgeFunctionsProxy, env, functionsRegistry, functionsServer, imageProxy, proxy, rewriter, serverHandler, settings, siteInfo, }, req, res) => {
611
613
  req.originalBody =
612
614
  req.method && ['GET', 'OPTIONS', 'HEAD'].includes(req.method) ? null : await createStreamPromise(req, BYTES_LIMIT);
613
615
  if (isImageRequest(req)) {
@@ -636,6 +638,40 @@ const onRequest = async ({ addonsUrls, api, edgeFunctionsProxy, env, functionsRe
636
638
  handleAddonUrl({ req, res, addonUrl });
637
639
  return;
638
640
  }
641
+ if (serverHandler) {
642
+ try {
643
+ const requestURL = reqToURL(req, req.url);
644
+ const serverMatch = await serverHandler.match(new Request(requestURL));
645
+ if (serverMatch) {
646
+ const staticFile = await getStatic(decodeURIComponent(requestURL.pathname), settings.dist ?? '');
647
+ if (!staticFile) {
648
+ const headers = new Headers();
649
+ for (let index = 0; index < req.rawHeaders.length; index += 2) {
650
+ headers.append(req.rawHeaders[index], req.rawHeaders[index + 1]);
651
+ }
652
+ const response = await serverMatch.handle(new Request(requestURL, {
653
+ body: req.originalBody,
654
+ headers,
655
+ method: req.method,
656
+ }));
657
+ await fromWebResponse(response, res);
658
+ return;
659
+ }
660
+ }
661
+ }
662
+ catch (error) {
663
+ // The response may have failed mid-stream, in which case the head is
664
+ // out and the only remaining option is dropping the connection.
665
+ if (res.headersSent) {
666
+ res.destroy();
667
+ }
668
+ else {
669
+ res.writeHead(500);
670
+ res.end(error instanceof Error ? error.message : 'Failed to serve request from Netlify Server');
671
+ }
672
+ return;
673
+ }
674
+ }
639
675
  const match = await rewriter(req);
640
676
  const options = {
641
677
  match,
@@ -725,6 +761,24 @@ export const startProxy = async function ({ accountId, addonsUrls, aiGatewayCont
725
761
  logger: { log, warn, error: logError },
726
762
  imagesConfig: config.images,
727
763
  });
764
+ const serverEntryEnabled = process.env.EXPERIMENTAL_NETLIFY_SERVER === 'true' || Boolean(siteInfo?.feature_flags?.netlify_build_server_entry);
765
+ let serverHandler;
766
+ if (serverEntryEnabled) {
767
+ const serverFileWatcher = new FileWatcher();
768
+ serverHandler = new ServerHandler({
769
+ accountID: siteInfo?.account_id,
770
+ fileWatcher: serverFileWatcher,
771
+ geolocation: mockLocation,
772
+ logger: { log, warn, error: logError },
773
+ projectRoot: projectDir,
774
+ siteID: siteInfo?.id,
775
+ });
776
+ const handlerToStop = serverHandler;
777
+ runBeforeProcessExit(async () => {
778
+ await handlerToStop.stop();
779
+ await serverFileWatcher.close();
780
+ });
781
+ }
728
782
  const imageProxy = initializeImageProxy({
729
783
  settings,
730
784
  imageHandler,
@@ -752,6 +806,7 @@ export const startProxy = async function ({ accountId, addonsUrls, aiGatewayCont
752
806
  const onRequestWithOptions = onRequest.bind(undefined, {
753
807
  proxy,
754
808
  rewriter,
809
+ serverHandler,
755
810
  settings,
756
811
  addonsUrls,
757
812
  functionsRegistry,
@@ -766,6 +821,20 @@ export const startProxy = async function ({ accountId, addonsUrls, aiGatewayCont
766
821
  ? https.createServer({ cert: settings.https.cert, key: settings.https.key }, onRequestWithOptions)
767
822
  : http.createServer(onRequestWithOptions);
768
823
  const onUpgrade = async function onUpgrade(req, socket, head) {
824
+ if (serverHandler) {
825
+ let handled = false;
826
+ try {
827
+ handled = await serverHandler.handleUpgrade(req, socket, head);
828
+ }
829
+ catch (error) {
830
+ logError(`Failed to hand over upgrade request to server: ${error instanceof Error ? error.message : String(error)}`);
831
+ socket.destroy();
832
+ return;
833
+ }
834
+ if (handled) {
835
+ return;
836
+ }
837
+ }
769
838
  const match = await rewriter(req);
770
839
  if (match && !match.force404 && isExternal(match)) {
771
840
  const reqUrl = reqToURL(req, req.url);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
- "version": "27.6.0",
3
+ "version": "27.8.0",
4
4
  "description": "Netlify command line tool",
5
5
  "keywords": [
6
6
  "api",
@@ -55,11 +55,11 @@
55
55
  "@fastify/static": "^10.0.0",
56
56
  "@netlify/ai": "^1.0.1",
57
57
  "@netlify/api": "^15.1.2",
58
- "@netlify/blobs": "^11.0.3",
59
- "@netlify/build": "^36.4.8",
58
+ "@netlify/blobs": "^11.1.0",
59
+ "@netlify/build": "^37.0.0",
60
60
  "@netlify/build-info": "^11.3.0",
61
61
  "@netlify/config": "^25.2.5",
62
- "@netlify/dev": "^5.0.5",
62
+ "@netlify/dev": "^5.1.2",
63
63
  "@netlify/dev-utils": "^6.0.1",
64
64
  "@netlify/edge-bundler": "^16.0.4",
65
65
  "@netlify/edge-functions": "^4.0.0",
@@ -68,9 +68,10 @@
68
68
  "@netlify/images": "^2.0.1",
69
69
  "@netlify/local-functions-proxy": "^2.0.3",
70
70
  "@netlify/redirect-parser": "^16.1.1",
71
- "@netlify/zip-it-and-ship-it": "^15.5.1",
71
+ "@netlify/server-dev": "^0.1.1",
72
+ "@netlify/zip-it-and-ship-it": "^16.0.0",
72
73
  "@octokit/rest": "^22.0.0",
73
- "@opentelemetry/api": "^1.8.0",
74
+ "@opentelemetry/api": "~1.9.0",
74
75
  "@pnpm/tabtab": "^0.5.4",
75
76
  "ansi-escapes": "^7.3.0",
76
77
  "ansi-to-html": "^0.7.2",