netlify-cli 27.13.0 → 27.14.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.
@@ -19,6 +19,7 @@ import { handleOptionError, isOptionError } from '../utils/command-error-handler
19
19
  import { EXIT_CODES } from '../utils/exit-codes.js';
20
20
  import { getFrameworksAPIPaths } from '../utils/frameworks-api.js';
21
21
  import { getSiteByName } from '../utils/get-site.js';
22
+ import { getLocalApiTestOpts } from '../utils/local-api-test-opts.js';
22
23
  import { buildAuthorizeUrl } from '../utils/login-url.js';
23
24
  import openBrowser from '../utils/open-browser.js';
24
25
  import { isInteractive } from '../utils/scripted-commands.js';
@@ -653,6 +654,7 @@ export default class BaseCommand extends Command {
653
654
  host: host,
654
655
  pathPrefix: pathPrefix,
655
656
  scheme: scheme,
657
+ testOpts: getLocalApiTestOpts({ host, scheme }),
656
658
  offline: offline ?? flags.offline,
657
659
  siteFeatureFlagPrefix: 'cli',
658
660
  featureFlags: this.featureFlags,
@@ -172,9 +172,6 @@ const getDeployFilesFilter = ({ deployFolder, site }) => {
172
172
  // when site.root !== deployFolder the behaviour matches our buildbot
173
173
  const skipNodeModules = site.root === deployFolder;
174
174
  return (filename) => {
175
- if (filename == null) {
176
- return false;
177
- }
178
175
  if (filename === deployFolder) {
179
176
  return true;
180
177
  }
@@ -7,7 +7,6 @@ import type { DeployConfig } from './types.js';
7
7
  import type { DeployEnvironmentVariable } from '../env/deploy-env-vars.js';
8
8
  export type { DeployEvent };
9
9
  export interface DeploySiteOptions {
10
- assetType?: 'file' | undefined;
11
10
  branch?: string;
12
11
  concurrentHash?: number;
13
12
  concurrentUpload?: number;
@@ -33,7 +32,7 @@ export interface DeploySiteOptions {
33
32
  tmpDir?: string;
34
33
  workingDir: string;
35
34
  }
36
- export declare const deploySite: (command: BaseCommand, api: NetlifyAPI, siteId: string, dir: string, { assetType, branch, concurrentHash, concurrentUpload, config, deployId, deployTimeout, draft, environment, filter, fnDir, functionsConfig, hashAlgorithm, manifestPath, maxRetry, packagePath, serverEnabled, serverManifestPath, siteRoot, skipFunctionsCache, statusCb, syncFileLimit, tmpDir, workingDir, }: DeploySiteOptions) => Promise<{
35
+ export declare const deploySite: (command: BaseCommand, api: NetlifyAPI, siteId: string, dir: string, { branch, concurrentHash, concurrentUpload, config, deployId, deployTimeout, draft, environment, filter, fnDir, functionsConfig, hashAlgorithm, manifestPath, maxRetry, packagePath, serverEnabled, serverManifestPath, siteRoot, skipFunctionsCache, statusCb, syncFileLimit, tmpDir, workingDir, }: DeploySiteOptions) => Promise<{
37
36
  deployId: string;
38
37
  deploy: import("./util.js").ReadyDeploy;
39
38
  uploadList: (import("./upload-files.js").EdgeFunctionUploadFile | import("./upload-files.js").ServerUploadFile | File | (import("./upload-files.js").FunctionUploadFile & {
@@ -18,7 +18,7 @@ const buildStatsString = (possibleParts) => {
18
18
  }
19
19
  return `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`;
20
20
  };
21
- export const deploySite = async (command, api, siteId, dir, { assetType, branch, concurrentHash = DEFAULT_CONCURRENT_HASH, concurrentUpload = DEFAULT_CONCURRENT_UPLOAD, config, deployId, deployTimeout = DEFAULT_DEPLOY_TIMEOUT, draft = false, environment, filter, fnDir = [], functionsConfig, hashAlgorithm, manifestPath, maxRetry = DEFAULT_MAX_RETRY, packagePath, serverEnabled, serverManifestPath, siteRoot, skipFunctionsCache, statusCb = () => {
21
+ export const deploySite = async (command, api, siteId, dir, { branch, concurrentHash = DEFAULT_CONCURRENT_HASH, concurrentUpload = DEFAULT_CONCURRENT_UPLOAD, config, deployId, deployTimeout = DEFAULT_DEPLOY_TIMEOUT, draft = false, environment, filter, fnDir = [], functionsConfig, hashAlgorithm, manifestPath, maxRetry = DEFAULT_MAX_RETRY, packagePath, serverEnabled, serverManifestPath, siteRoot, skipFunctionsCache, statusCb = () => {
22
22
  /* default to noop */
23
23
  }, syncFileLimit = DEFAULT_SYNC_LIMIT, tmpDir = temporaryDirectory(), workingDir, }) => {
24
24
  statusCb({
@@ -31,7 +31,6 @@ export const deploySite = async (command, api, siteId, dir, { assetType, branch,
31
31
  const dbMigrationsDistPath = await getDbMigrationsDistPathIfExists(workingDir);
32
32
  const [{ files: staticFiles, filesShaMap: staticShaMap }, { fnConfig, fnShaMap, functionSchedules, functions, functionsWithNativeModules, server, serverShaMap }, configFile, { edgeFunctions, edgeFnShaMap },] = await Promise.all([
33
33
  hashFiles({
34
- assetType,
35
34
  concurrentHash,
36
35
  directories: [dir, edgeFunctionsDistPath, deployConfigPath, dbMigrationsDistPath].filter((d) => Boolean(d)),
37
36
  filter,
@@ -47,6 +46,7 @@ export const deploySite = async (command, api, siteId, dir, { assetType, branch,
47
46
  statusCb,
48
47
  manifestPath,
49
48
  packagePath,
49
+ serverConfig: config.server,
50
50
  serverEnabled,
51
51
  serverManifestPath,
52
52
  skipFunctionsCache,
@@ -62,37 +62,29 @@ export const uploadDropFile = async ({ apiBase, userAgent }, deployId, filePath,
62
62
  }
63
63
  };
64
64
  // TODO: Migrate to @netlify/api when Drop endpoints are in the OpenAPI spec.
65
- export const waitForDropDeploy = async ({ apiBase, userAgent }, siteId, deployId, timeout = DEFAULT_DEPLOY_TIMEOUT) => {
66
- let deploy;
67
- const checkDeploy = async () => {
68
- const response = await fetch(`${apiBase}/sites/${siteId}/deploys/${deployId}`, {
69
- headers: makeHeaders(userAgent),
70
- });
71
- if (!response.ok) {
72
- return false;
73
- }
74
- const data = (await response.json());
75
- if (data.state === 'ready') {
76
- deploy = data;
77
- return true;
78
- }
79
- if (data.state === 'error') {
80
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- an empty error message falls back to the generic one
81
- throw new Error(data.error_message || `Deploy ${deployId} had an error`);
82
- }
83
- return false;
84
- };
85
- await pWaitFor(checkDeploy, {
86
- interval: DEPLOY_POLL,
87
- timeout: {
88
- milliseconds: timeout,
89
- message: 'Timeout while waiting for deploy',
90
- },
65
+ export const waitForDropDeploy = async ({ apiBase, userAgent }, siteId, deployId, timeout = DEFAULT_DEPLOY_TIMEOUT) => await pWaitFor(async () => {
66
+ const response = await fetch(`${apiBase}/sites/${siteId}/deploys/${deployId}`, {
67
+ headers: makeHeaders(userAgent),
91
68
  });
92
- // deploy is guaranteed to be set when pWaitFor resolves
93
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
94
- return deploy;
95
- };
69
+ if (!response.ok) {
70
+ return false;
71
+ }
72
+ const data = (await response.json());
73
+ if (data.state === 'ready') {
74
+ return pWaitFor.resolveWith(data);
75
+ }
76
+ if (data.state === 'error') {
77
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- an empty error message falls back to the generic one
78
+ throw new Error(data.error_message || `Deploy ${deployId} had an error`);
79
+ }
80
+ return false;
81
+ }, {
82
+ interval: DEPLOY_POLL,
83
+ timeout: {
84
+ milliseconds: timeout,
85
+ message: 'Timeout while waiting for deploy',
86
+ },
87
+ });
96
88
  // TODO: Migrate to @netlify/api when Drop endpoints are in the OpenAPI spec.
97
89
  export const claimDropSite = async ({ apiBase, userAgent }, siteId, dropToken, authToken) => {
98
90
  const response = await fetch(`${apiBase}/drop/claim`, {
@@ -123,7 +115,6 @@ export const uploadDropFiles = async (apiOptions, deployId, uploadList, token, {
123
115
  msg: `(${String(index + 1)}/${String(uploadList.length)}) Uploading ${fileObj.normalizedPath}...`,
124
116
  phase: 'progress',
125
117
  });
126
- let lastError;
127
118
  for (let attempt = 0; attempt <= maxRetry; attempt++) {
128
119
  try {
129
120
  const body = fs.createReadStream(fileObj.filepath);
@@ -131,18 +122,13 @@ export const uploadDropFiles = async (apiOptions, deployId, uploadList, token, {
131
122
  return;
132
123
  }
133
124
  catch (error) {
134
- lastError = error;
135
- if (lastError.status === 400 || lastError.status === 422) {
125
+ const isPermanentFailure = error instanceof Error && 'status' in error && (error.status === 400 || error.status === 422);
126
+ if (isPermanentFailure || attempt + 1 > maxRetry) {
136
127
  throw error;
137
128
  }
138
- if (attempt < maxRetry) {
139
- await new Promise((resolve) => setTimeout(resolve, 1000 * (attempt + 1)));
140
- }
129
+ await new Promise((resolve) => setTimeout(resolve, 1000 * (attempt + 1)));
141
130
  }
142
131
  }
143
- if (lastError) {
144
- throw lastError;
145
- }
146
132
  };
147
133
  await pMap(uploadList, uploadFile, { concurrency: concurrentUpload });
148
134
  statusCb({
@@ -1,7 +1,7 @@
1
1
  import type { DeployConfig } from './types.js';
2
2
  import type { InlineUploadFile } from './upload-files.js';
3
3
  export declare const hashConfig: ({ config }: {
4
- config: DeployConfig | undefined;
4
+ config: DeployConfig;
5
5
  }) => InlineUploadFile & {
6
6
  hash: string;
7
7
  };
@@ -1,8 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import tomlify from 'tomlify-j0.4';
3
3
  export const hashConfig = ({ config }) => {
4
- if (!config)
5
- throw new Error('Missing config option');
6
4
  const configString = serializeToml(config);
7
5
  const hash = createHash('sha1').update(configString).digest('hex');
8
6
  return {
@@ -1,10 +1,9 @@
1
1
  import type { File } from './file.js';
2
2
  import type { StatusCallback } from './status-cb.js';
3
- declare const hashFiles: ({ assetType, concurrentHash, directories, filter, hashAlgorithm, normalizer, statusCb, }: {
4
- assetType?: 'file' | undefined;
3
+ declare const hashFiles: ({ concurrentHash, directories, filter, hashAlgorithm, normalizer, statusCb, }: {
5
4
  concurrentHash: number;
6
5
  directories: string[];
7
- filter: ((filename: string) => boolean) | undefined;
6
+ filter: (filename: string) => boolean;
8
7
  hashAlgorithm?: string | undefined;
9
8
  normalizer?: (file: File) => File;
10
9
  statusCb: StatusCallback;
@@ -1,13 +1,11 @@
1
1
  import { pipeline } from 'stream/promises';
2
2
  import walker from 'folder-walker';
3
3
  import { fileFilterCtor, fileNormalizerCtor, hasherCtor, manifestCollectorCtor } from './hasher-segments.js';
4
- const hashFiles = async ({ assetType = 'file', concurrentHash, directories, filter, hashAlgorithm = 'sha1', normalizer, statusCb, }) => {
5
- if (!filter)
6
- throw new Error('Missing filter function option');
4
+ const hashFiles = async ({ concurrentHash, directories, filter, hashAlgorithm = 'sha1', normalizer, statusCb, }) => {
7
5
  const fileStream = walker(directories, { filter });
8
6
  const fileFilter = fileFilterCtor();
9
7
  const hasher = hasherCtor({ concurrentHash, hashAlgorithm });
10
- const fileNormalizer = fileNormalizerCtor({ assetType, normalizer });
8
+ const fileNormalizer = fileNormalizerCtor({ normalizer });
11
9
  // Written to by manifestCollector
12
10
  // normalizedPath: hash (wanted by deploy API)
13
11
  const files = {};
@@ -1,53 +1,62 @@
1
- import { type Config as FunctionsConfig, type FunctionResult, type TrafficRules } from '@netlify/zip-it-and-ship-it';
1
+ import { type Config as FunctionsConfig, type FunctionResult, type NetlifyServerConfig, type ServerResult } from '@netlify/zip-it-and-ship-it';
2
2
  import type BaseCommand from '../../commands/base-command.js';
3
3
  import type { StatusCallback } from './status-cb.js';
4
4
  import type { FunctionUploadFile, ServerUploadFile } from './upload-files.js';
5
+ interface ServerPayload {
6
+ sha: string;
7
+ region?: string;
8
+ instance_size?: ServerResult['instanceSize'];
9
+ routes?: ServerResult['routes'];
10
+ excluded_routes?: ServerResult['excludedRoutes'];
11
+ runtime_version?: ServerResult['runtimeVersion'];
12
+ }
5
13
  type HashedFunctionFile = FunctionUploadFile & {
6
14
  hash: string;
7
15
  relname: string;
8
16
  };
9
- declare const trafficRulesConfig: (trafficRules?: TrafficRules) => {
10
- action: {
11
- type: string;
12
- config: {
13
- rate_limit_config: {
14
- algorithm: string;
15
- window_size: number;
16
- window_limit: number;
17
- };
18
- aggregate: {
19
- keys: {
20
- type: string;
21
- }[];
17
+ declare const toFunctionConfigPayload: (func: FunctionResult) => {
18
+ display_name: string | undefined;
19
+ excluded_routes: import("@netlify/zip-it-and-ship-it").Route[] | undefined;
20
+ generator: string | undefined;
21
+ memory: number | undefined;
22
+ region: string | undefined;
23
+ routes: import("@netlify/zip-it-and-ship-it").ExtendedRoute[] | undefined;
24
+ build_data: import("@netlify/zip-it-and-ship-it/dist/utils/format_result.js").BuildData | undefined;
25
+ priority: number | undefined;
26
+ traffic_rules: {
27
+ action: {
28
+ type: string;
29
+ config: {
30
+ rate_limit_config: {
31
+ algorithm: string;
32
+ window_size: number;
33
+ window_limit: number;
34
+ };
35
+ aggregate: {
36
+ keys: {
37
+ type: string;
38
+ }[];
39
+ };
40
+ to: string | undefined;
22
41
  };
23
- to: string | undefined;
24
42
  };
25
- };
26
- } | undefined;
27
- interface FunctionConfigPayload {
28
- display_name: FunctionResult['displayName'];
29
- excluded_routes: FunctionResult['excludedRoutes'];
30
- generator: FunctionResult['generator'];
31
- memory: FunctionResult['memory'];
32
- region: FunctionResult['region'];
33
- routes: FunctionResult['routes'];
34
- build_data: FunctionResult['buildData'];
35
- priority: FunctionResult['priority'];
36
- traffic_rules: ReturnType<typeof trafficRulesConfig>;
37
- vcpu: FunctionResult['vcpu'];
38
- }
39
- declare const hashFns: (command: BaseCommand, directories: string[], { concurrentHash, functionsConfig, hashAlgorithm, manifestPath, packagePath, rootDir, serverEnabled, serverManifestPath, skipFunctionsCache, statusCb, tmpDir, }: {
43
+ } | undefined;
44
+ vcpu: number | undefined;
45
+ };
46
+ type FunctionConfigPayload = ReturnType<typeof toFunctionConfigPayload>;
47
+ declare const hashFns: (command: BaseCommand, directories: string[], { concurrentHash, functionsConfig, hashAlgorithm, manifestPath, packagePath, rootDir, serverConfig, serverEnabled, serverManifestPath, skipFunctionsCache, statusCb, tmpDir, }: {
40
48
  concurrentHash?: number;
41
49
  functionsConfig?: FunctionsConfig | undefined;
42
50
  hashAlgorithm?: string | undefined;
43
51
  manifestPath?: string | undefined;
44
52
  packagePath?: string | undefined;
45
53
  rootDir?: string | undefined;
54
+ serverConfig?: NetlifyServerConfig | undefined;
46
55
  serverEnabled?: boolean | undefined;
47
56
  serverManifestPath?: string | undefined;
48
57
  skipFunctionsCache?: boolean | undefined;
49
58
  statusCb: StatusCallback;
50
- tmpDir?: string | undefined;
59
+ tmpDir: string;
51
60
  }) => Promise<{
52
61
  functionSchedules?: {
53
62
  name: string;
@@ -55,13 +64,9 @@ declare const hashFns: (command: BaseCommand, directories: string[], { concurren
55
64
  }[] | undefined;
56
65
  functions: Record<string, string>;
57
66
  functionsWithNativeModules: FunctionResult[];
58
- shaMap?: Record<string, never> | undefined;
59
67
  fnShaMap?: Record<string, HashedFunctionFile[]> | undefined;
60
68
  fnConfig?: Record<string, FunctionConfigPayload> | undefined;
61
- server?: {
62
- sha: string;
63
- region?: string;
64
- } | undefined;
69
+ server?: ServerPayload | undefined;
65
70
  serverShaMap?: Record<string, ServerUploadFile[]> | undefined;
66
71
  }>;
67
72
  export default hashFns;
@@ -52,7 +52,7 @@ const getFunctionZips = async ({ command, directories, functionsConfig, manifest
52
52
  config: functionsConfig,
53
53
  });
54
54
  };
55
- const getServerBundle = async ({ packagePath, rootDir, serverEnabled, serverManifestPath, tmpDir, }) => {
55
+ const getServerBundle = async ({ packagePath, rootDir, serverConfig, serverEnabled, serverManifestPath, tmpDir, }) => {
56
56
  if (!serverEnabled) {
57
57
  return undefined;
58
58
  }
@@ -72,7 +72,7 @@ const getServerBundle = async ({ packagePath, rootDir, serverEnabled, serverMani
72
72
  if (entryPath === undefined) {
73
73
  return undefined;
74
74
  }
75
- return await zipServer(entryPath, join(tmpDir, 'server'), { basePath: rootDir });
75
+ return await zipServer(entryPath, join(tmpDir, 'server'), { basePath: rootDir, config: serverConfig });
76
76
  };
77
77
  const trafficRulesConfig = (trafficRules) => {
78
78
  if (!trafficRules) {
@@ -93,14 +93,23 @@ const trafficRulesConfig = (trafficRules) => {
93
93
  },
94
94
  };
95
95
  };
96
- const hashFns = async (command, directories, { concurrentHash, functionsConfig, hashAlgorithm = 'sha256', manifestPath, packagePath, rootDir, serverEnabled, serverManifestPath, skipFunctionsCache, statusCb, tmpDir, }) => {
96
+ const toFunctionConfigPayload = (func) => ({
97
+ display_name: func.displayName,
98
+ excluded_routes: func.excludedRoutes,
99
+ generator: func.generator,
100
+ memory: func.memory,
101
+ region: func.region,
102
+ routes: func.routes,
103
+ build_data: func.buildData,
104
+ priority: func.priority,
105
+ traffic_rules: trafficRulesConfig(func.trafficRules),
106
+ vcpu: func.vcpu,
107
+ });
108
+ const hashFns = async (command, directories, { concurrentHash, functionsConfig, hashAlgorithm = 'sha256', manifestPath, packagePath, rootDir, serverConfig, serverEnabled, serverManifestPath, skipFunctionsCache, statusCb, tmpDir, }) => {
97
109
  // Exit early if there is nothing to bundle. A site can have a server without
98
110
  // any functions directory.
99
111
  if (directories.length === 0 && !serverEnabled) {
100
- return { functions: {}, functionsWithNativeModules: [], shaMap: {} };
101
- }
102
- if (!tmpDir) {
103
- throw new Error('Missing tmpDir directory for zipping files');
112
+ return { functions: {}, functionsWithNativeModules: [] };
104
113
  }
105
114
  const [functionZips, serverBundle] = await Promise.all([
106
115
  directories.length === 0
@@ -115,7 +124,7 @@ const hashFns = async (command, directories, { concurrentHash, functionsConfig,
115
124
  statusCb,
116
125
  tmpDir,
117
126
  }),
118
- getServerBundle({ packagePath, rootDir, serverEnabled, serverManifestPath, tmpDir }),
127
+ getServerBundle({ packagePath, rootDir, serverConfig, serverEnabled, serverManifestPath, tmpDir }),
119
128
  ]);
120
129
  // ZISI's in-memory FunctionResult only nests bootstrap/runtime version into
121
130
  // buildData when writing the manifest cache. Reconstruct it for direct-zip paths.
@@ -145,7 +154,7 @@ const hashFns = async (command, directories, { concurrentHash, functionsConfig,
145
154
  priority,
146
155
  trafficRules,
147
156
  }));
148
- const fnConfig = functionZips
157
+ const fnConfig = Object.fromEntries(functionZips
149
158
  .filter((func) => Boolean(func.displayName ||
150
159
  func.generator ||
151
160
  func.routes ||
@@ -155,21 +164,7 @@ const hashFns = async (command, directories, { concurrentHash, functionsConfig,
155
164
  func.region ||
156
165
  func.memory ||
157
166
  func.vcpu))
158
- .reduce((funcs, curr) => ({
159
- ...funcs,
160
- [curr.name]: {
161
- display_name: curr.displayName,
162
- excluded_routes: curr.excludedRoutes,
163
- generator: curr.generator,
164
- memory: curr.memory,
165
- region: curr.region,
166
- routes: curr.routes,
167
- build_data: curr.buildData,
168
- priority: curr.priority,
169
- traffic_rules: trafficRulesConfig(curr.trafficRules),
170
- vcpu: curr.vcpu,
171
- },
172
- }), {});
167
+ .map((func) => [func.name, toFunctionConfigPayload(func)]));
173
168
  const functionSchedules = functionZips
174
169
  .map(({ name, schedule }) => schedule && { name, cron: schedule })
175
170
  .filter((schedule) => schedule !== '' && schedule !== undefined);
@@ -210,7 +205,16 @@ const hashServer = async (serverBundle, { concurrentHash, hashAlgorithm, statusC
210
205
  hasherCtor({ concurrentHash, hashAlgorithm }),
211
206
  manifestCollectorCtor(servers, serverShaMap, { statusCb }),
212
207
  ]);
213
- return { server: { sha: Object.values(servers)[0], region: serverBundle.region }, serverShaMap };
208
+ const { excludedRoutes, instanceSize, region, routes, runtimeVersion } = serverBundle;
209
+ const server = {
210
+ sha: Object.values(servers)[0],
211
+ ...(region === undefined ? {} : { region }),
212
+ ...(instanceSize === undefined ? {} : { instance_size: instanceSize }),
213
+ ...(routes === undefined ? {} : { routes }),
214
+ ...(excludedRoutes === undefined ? {} : { excluded_routes: excludedRoutes }),
215
+ ...(runtimeVersion === undefined ? {} : { runtime_version: runtimeVersion }),
216
+ };
217
+ return { server, serverShaMap };
214
218
  };
215
219
  export default hashFns;
216
220
  //# sourceMappingURL=hash-fns.js.map
@@ -5,8 +5,7 @@ export declare const hasherCtor: ({ concurrentHash, hashAlgorithm }: {
5
5
  concurrentHash?: number;
6
6
  hashAlgorithm: string;
7
7
  }) => Transform;
8
- export declare const fileNormalizerCtor: ({ assetType, normalizer: normalizeFunction, }: {
9
- assetType: 'file';
8
+ export declare const fileNormalizerCtor: ({ normalizer: normalizeFunction }: {
10
9
  normalizer?: (file: File) => File;
11
10
  }) => Transform;
12
11
  export declare const manifestCollectorCtor: <T extends {
@@ -28,11 +28,11 @@ export const hasherCtor = ({ concurrentHash, hashAlgorithm }) => {
28
28
  });
29
29
  };
30
30
  // Inject normalized file names into normalizedPath and assetType
31
- export const fileNormalizerCtor = ({ assetType, normalizer: normalizeFunction, }) => {
31
+ export const fileNormalizerCtor = ({ normalizer: normalizeFunction }) => {
32
32
  return new Transform({
33
33
  objectMode: true,
34
34
  transform(fileObj, _, callback) {
35
- const normalizedFile = { ...fileObj, assetType, normalizedPath: normalizePath(fileObj.relname) };
35
+ const normalizedFile = { ...fileObj, assetType: 'file', normalizedPath: normalizePath(fileObj.relname) };
36
36
  const result = normalizeFunction !== undefined ? normalizeFunction(normalizedFile) : normalizedFile;
37
37
  this.push(result);
38
38
  callback();
@@ -13,5 +13,6 @@ export interface DeployConfig {
13
13
  functionsDirectory?: string;
14
14
  headers?: NetlifyConfig['headers'] | MinimalHeader[];
15
15
  redirects?: unknown[];
16
+ server?: NetlifyConfig['server'];
16
17
  }
17
18
  //# sourceMappingURL=types.d.ts.map
@@ -4,10 +4,10 @@ export type UploadApi = Pick<NetlifyAPI, 'uploadDeployFile' | 'uploadDeployFunct
4
4
  interface UploadFileBase {
5
5
  filepath: string;
6
6
  normalizedPath: string;
7
- body?: undefined;
8
7
  }
9
8
  export interface StaticUploadFile extends UploadFileBase {
10
9
  assetType: 'file';
10
+ body?: undefined;
11
11
  }
12
12
  export interface InlineUploadFile {
13
13
  assetType: 'file';
@@ -22,9 +22,7 @@ const uploadFiles = async (api, deployId, uploadList, { concurrentUpload, maxRet
22
22
  phase: 'start',
23
23
  });
24
24
  const uploadFile = async (fileObj, index) => {
25
- const { body, filepath, normalizedPath } = fileObj;
26
- // FIXME(@netlify/api): octet-stream `body` is typed as `ReadStream` only, but an inline string body works too
27
- const readStreamCtor = () => (body ?? fs.createReadStream(filepath));
25
+ const { normalizedPath } = fileObj;
28
26
  statusCb({
29
27
  type: 'upload',
30
28
  msg: `(${index}/${uploadList.length}) Uploading ${normalizedPath}...`,
@@ -32,8 +30,11 @@ const uploadFiles = async (api, deployId, uploadList, { concurrentUpload, maxRet
32
30
  });
33
31
  switch (fileObj.assetType) {
34
32
  case 'file': {
33
+ const { body, filepath } = fileObj;
35
34
  return await retryUpload(() => api.uploadDeployFile({
36
- body: readStreamCtor,
35
+ // FIXME(@netlify/api): `types.d.ts` hardcodes every `application/octet-stream` body as `ReadStream | (() => ReadStream)`,
36
+ // ignoring the spec's `string` type; a string body works at runtime
37
+ body: () => (body ?? fs.createReadStream(filepath)),
37
38
  deployId,
38
39
  path: encodeURI(normalizedPath),
39
40
  }), maxRetry);
@@ -42,7 +43,7 @@ const uploadFiles = async (api, deployId, uploadList, { concurrentUpload, maxRet
42
43
  const { invocationMode, runtime, timeout } = fileObj;
43
44
  return await retryUpload((retryCount) => {
44
45
  const params = {
45
- body: readStreamCtor,
46
+ body: () => fs.createReadStream(fileObj.filepath),
46
47
  deployId,
47
48
  invocationMode,
48
49
  timeout,
@@ -58,7 +59,7 @@ const uploadFiles = async (api, deployId, uploadList, { concurrentUpload, maxRet
58
59
  case 'edge-function': {
59
60
  return await retryUpload((retryCount) => {
60
61
  const params = {
61
- body: readStreamCtor,
62
+ body: () => fs.createReadStream(fileObj.filepath),
62
63
  deployId,
63
64
  codeSha: normalizedPath,
64
65
  };
@@ -71,7 +72,7 @@ const uploadFiles = async (api, deployId, uploadList, { concurrentUpload, maxRet
71
72
  case 'server': {
72
73
  return await retryUpload((retryCount) => {
73
74
  const params = {
74
- body: readStreamCtor,
75
+ body: () => fs.createReadStream(fileObj.filepath),
75
76
  deployId,
76
77
  codeSha: fileObj.hash,
77
78
  };
@@ -132,8 +133,9 @@ const retryUpload = (uploadFn, maxRetry) => new Promise((resolve, reject) => {
132
133
  // Do something when backoff starts, e.g. show to the
133
134
  // user the delay before next reconnection attempt.
134
135
  });
135
- // eslint-disable-next-line @typescript-eslint/no-misused-promises -- FIXME: the listener's promise is discarded
136
- fibonacciBackoff.on('ready', tryUpload);
136
+ fibonacciBackoff.on('ready', (retryIndex) => {
137
+ void tryUpload(retryIndex);
138
+ });
137
139
  fibonacciBackoff.on('fail', () => {
138
140
  reject(lastError);
139
141
  });
@@ -2,6 +2,10 @@ import type { NetlifyAPI } from '@netlify/api';
2
2
  export declare const pluralize: (amount: number, noun: string) => string;
3
3
  export type Deploy = Awaited<ReturnType<NetlifyAPI['getSiteDeploy']>>;
4
4
  export type ReadyDeploy = Deploy & Required<Pick<Deploy, 'id' | 'site_id' | 'name' | 'admin_url' | 'url' | 'ssl_url' | 'deploy_url' | 'deploy_ssl_url'>>;
5
+ export declare class DeployStateError extends Error {
6
+ readonly deploy: Deploy;
7
+ constructor(message: string, deploy: Deploy);
8
+ }
5
9
  export declare const normalizePath: (relname: string) => string;
6
10
  export declare const waitForDiff: (api: Pick<NetlifyAPI, 'getSiteDeploy'>, deployId: string, siteId: string, timeout: number) => Promise<Deploy>;
7
11
  export declare const waitForDeploy: (api: Pick<NetlifyAPI, 'getSiteDeploy'>, deployId: string, siteId: string, timeout: number) => Promise<ReadyDeploy>;
@@ -2,6 +2,13 @@ import { sep } from 'path';
2
2
  import pWaitFor from 'p-wait-for';
3
3
  import { DEPLOY_POLL } from './constants.js';
4
4
  export const pluralize = (amount, noun) => `${amount} ${noun}${amount === 1 ? '' : 's'}`;
5
+ export class DeployStateError extends Error {
6
+ deploy;
7
+ constructor(message, deploy) {
8
+ super(message);
9
+ this.deploy = deploy;
10
+ }
11
+ }
5
12
  // normalize windows paths to unix paths
6
13
  export const normalizePath = (relname) => {
7
14
  if (relname.includes('#') || relname.includes('?')) {
@@ -9,70 +16,26 @@ export const normalizePath = (relname) => {
9
16
  }
10
17
  return relname.split(sep).join('/');
11
18
  };
19
+ // https://github.com/netlify/bitballoon/blob/master/app/models/deploy.rb#L21-L33
20
+ const DIFFED_STATES = new Set(['prepared', 'uploading', 'uploaded', 'ready']);
21
+ const READY_STATES = new Set(['ready']);
22
+ const pollDeployUntil = async (api, deployId, siteId, timeout, targetStates) => await pWaitFor(async () => {
23
+ const siteDeploy = await api.getSiteDeploy({ siteId, deployId });
24
+ if (siteDeploy.state === 'error') {
25
+ throw new DeployStateError(siteDeploy.error_message || `Deploy ${deployId} had an error`, siteDeploy);
26
+ }
27
+ return siteDeploy.state !== undefined && targetStates.has(siteDeploy.state) && pWaitFor.resolveWith(siteDeploy);
28
+ }, {
29
+ interval: DEPLOY_POLL,
30
+ timeout: {
31
+ milliseconds: timeout,
32
+ message: 'Timeout while waiting for deploy',
33
+ },
34
+ });
12
35
  // poll an async deployId until its done diffing
13
- export const waitForDiff = async (api, deployId, siteId, timeout) => {
14
- const loadDeploy = async () => {
15
- const siteDeploy = await api.getSiteDeploy({ siteId, deployId });
16
- switch (siteDeploy.state) {
17
- // https://github.com/netlify/bitballoon/blob/master/app/models/deploy.rb#L21-L33
18
- case 'error': {
19
- const deployError = new Error(siteDeploy.error_message || `Deploy ${deployId} had an error`);
20
- deployError.deploy = siteDeploy;
21
- throw deployError;
22
- }
23
- case 'prepared':
24
- case 'uploading':
25
- case 'uploaded':
26
- case 'ready': {
27
- return pWaitFor.resolveWith(siteDeploy);
28
- }
29
- case 'preparing':
30
- default: {
31
- return false;
32
- }
33
- }
34
- };
35
- const deploy = await pWaitFor(loadDeploy, {
36
- interval: DEPLOY_POLL,
37
- timeout: {
38
- milliseconds: timeout,
39
- message: 'Timeout while waiting for deploy',
40
- },
41
- });
42
- return deploy;
43
- };
36
+ export const waitForDiff = async (api, deployId, siteId, timeout) => await pollDeployUntil(api, deployId, siteId, timeout, DIFFED_STATES);
44
37
  // Poll a deployId until its ready
45
- export const waitForDeploy = async (api, deployId, siteId, timeout) => {
46
- const loadDeploy = async () => {
47
- const siteDeploy = await api.getSiteDeploy({ siteId, deployId });
48
- switch (siteDeploy.state) {
49
- // https://github.com/netlify/bitballoon/blob/master/app/models/deploy.rb#L21-L33
50
- case 'error': {
51
- const deployError = new Error(siteDeploy.error_message || `Deploy ${deployId} had an error`);
52
- deployError.deploy = siteDeploy;
53
- throw deployError;
54
- }
55
- case 'ready': {
56
- return pWaitFor.resolveWith(siteDeploy);
57
- }
58
- case 'preparing':
59
- case 'prepared':
60
- case 'uploaded':
61
- case 'uploading':
62
- default: {
63
- return false;
64
- }
65
- }
66
- };
67
- const deploy = await pWaitFor(loadDeploy, {
68
- interval: DEPLOY_POLL,
69
- timeout: {
70
- milliseconds: timeout,
71
- message: 'Timeout while waiting for deploy',
72
- },
73
- });
74
- return deploy;
75
- };
38
+ export const waitForDeploy = async (api, deployId, siteId, timeout) => (await pollDeployUntil(api, deployId, siteId, timeout, READY_STATES));
76
39
  // Transform the fileShaMap and fnShaMap into a generic shaMap that file-uploader.js can use
77
40
  export const getUploadList = (required, shaMap) => {
78
41
  if (!required || !shaMap)
@@ -199,8 +199,9 @@ const isOlderThan = async (file, ageMs) => {
199
199
  };
200
200
  const isSameEntry = async (a, b) => {
201
201
  try {
202
- const [statA, statB] = await Promise.all([fs.lstat(a), fs.lstat(b)]);
203
- return statA.ino !== 0 && statA.ino === statB.ino && statA.dev === statB.dev;
202
+ // As numbers, Windows file IDs lose precision and two directories can share one.
203
+ const [statA, statB] = await Promise.all([fs.lstat(a, { bigint: true }), fs.lstat(b, { bigint: true })]);
204
+ return statA.ino !== 0n && statA.ino === statB.ino && statA.dev === statB.dev;
204
205
  }
205
206
  catch {
206
207
  return false;
@@ -0,0 +1,8 @@
1
+ export declare const getLocalApiTestOpts: ({ host, scheme, }: {
2
+ host?: string;
3
+ scheme?: string;
4
+ }) => {
5
+ host: string;
6
+ scheme?: string;
7
+ } | undefined;
8
+ //# sourceMappingURL=local-api-test-opts.d.ts.map
@@ -0,0 +1,14 @@
1
+ const LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '[::1]']);
2
+ export const getLocalApiTestOpts = ({ host, scheme, }) => {
3
+ if (host === undefined) {
4
+ return undefined;
5
+ }
6
+ try {
7
+ const { hostname } = new URL(`http://${host}`);
8
+ return LOOPBACK_HOSTNAMES.has(hostname) ? { host, scheme } : undefined;
9
+ }
10
+ catch {
11
+ return undefined;
12
+ }
13
+ };
14
+ //# sourceMappingURL=local-api-test-opts.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
- "version": "27.13.0",
3
+ "version": "27.14.1",
4
4
  "description": "Netlify command line tool",
5
5
  "keywords": [
6
6
  "api",
@@ -54,31 +54,31 @@
54
54
  "dependencies": {
55
55
  "@fastify/static": "^10.0.0",
56
56
  "@inquirer/prompts": "^8.7.2",
57
- "@netlify/ai": "^1.0.1",
58
- "@netlify/api": "^15.1.2",
59
- "@netlify/blobs": "^11.1.3",
60
- "@netlify/build": "^37.4.0",
57
+ "@netlify/ai": "^1.0.2",
58
+ "@netlify/api": "^15.1.3",
59
+ "@netlify/blobs": "^11.1.6",
60
+ "@netlify/build": "^37.5.1",
61
61
  "@netlify/build-info": "^11.3.0",
62
- "@netlify/config": "^25.2.6",
63
- "@netlify/dev": "^5.1.6",
64
- "@netlify/dev-utils": "^6.0.3",
65
- "@netlify/edge-bundler": "^16.1.2",
66
- "@netlify/edge-functions": "^4.0.2",
62
+ "@netlify/config": "^25.3.0",
63
+ "@netlify/dev": "^5.1.10",
64
+ "@netlify/dev-utils": "^6.0.4",
65
+ "@netlify/edge-bundler": "^16.1.3",
66
+ "@netlify/edge-functions": "^4.0.3",
67
67
  "@netlify/edge-functions-bootstrap": "^3.2.0",
68
68
  "@netlify/headers-parser": "^10.1.1",
69
- "@netlify/images": "^2.0.3",
69
+ "@netlify/images": "^2.0.5",
70
70
  "@netlify/local-functions-proxy": "^2.0.3",
71
71
  "@netlify/redirect-matcher": "^0.4.2",
72
72
  "@netlify/redirect-parser": "^16.1.1",
73
- "@netlify/server-dev": "^0.1.3",
74
- "@netlify/zip-it-and-ship-it": "^16.3.0",
73
+ "@netlify/server-dev": "^0.1.4",
74
+ "@netlify/zip-it-and-ship-it": "^16.4.1",
75
75
  "@opentelemetry/api": "~1.9.0",
76
76
  "@pnpm/tabtab": "^0.5.4",
77
77
  "ansi-escapes": "^7.3.0",
78
78
  "ansi-to-html": "^0.7.2",
79
79
  "ascii-table": "^0.0.9",
80
80
  "backoff": "^2.5.0",
81
- "boxen": "^8.0.1",
81
+ "boxen": "^9.0.0",
82
82
  "chalk": "^6.0.0",
83
83
  "chokidar": "^5.0.0",
84
84
  "ci-info": "^4.4.0",
@@ -152,8 +152,8 @@
152
152
  "@bugsnag/js": "^8.6.0",
153
153
  "@eslint/compat": "^1.4.1",
154
154
  "@eslint/js": "^9.36.0",
155
- "@netlify/functions": "^6.0.2",
156
- "@netlify/types": "^3.2.0",
155
+ "@netlify/functions": "^6.0.3",
156
+ "@netlify/types": "^3.3.0",
157
157
  "@sindresorhus/slugify": "^3.0.0",
158
158
  "@tsconfig/node22": "22.0.5",
159
159
  "@tsconfig/recommended": "^1.0.13",
@@ -196,7 +196,7 @@
196
196
  "memfs": "^4.56.10",
197
197
  "nock": "^14.0.10",
198
198
  "npm-run-all2": "^8.0.4",
199
- "oxfmt": "0.72.0",
199
+ "oxfmt": "0.73.0",
200
200
  "p-timeout": "^7.0.0",
201
201
  "picomatch": "^4.0.4",
202
202
  "pkg-pr-new": "^0.0.86",