@netlify/build 29.48.2 → 29.49.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.
Files changed (34) hide show
  1. package/lib/core/build.d.ts +4 -4
  2. package/lib/core/build.js +5 -4
  3. package/lib/core/dry.d.ts +2 -1
  4. package/lib/core/dry.js +8 -8
  5. package/lib/core/feature_flags.js +1 -4
  6. package/lib/log/description.d.ts +1 -1
  7. package/lib/log/messages/core_steps.d.ts +2 -1
  8. package/lib/log/messages/core_steps.js +9 -2
  9. package/lib/log/output_flusher.d.ts +0 -1
  10. package/lib/log/stream.d.ts +0 -2
  11. package/lib/plugins/child/utils.d.ts +1 -1
  12. package/lib/plugins/options.d.ts +3 -3
  13. package/lib/plugins_core/blobs_upload/index.js +10 -8
  14. package/lib/plugins_core/deploy/buildbot_client.d.ts +0 -1
  15. package/lib/plugins_core/deploy_config/index.js +38 -17
  16. package/lib/plugins_core/deploy_config/util.d.ts +2 -0
  17. package/lib/plugins_core/deploy_config/util.js +30 -0
  18. package/lib/plugins_core/dev_blobs_upload/index.js +10 -8
  19. package/lib/plugins_core/edge_functions/index.d.ts +4 -2
  20. package/lib/plugins_core/edge_functions/index.js +37 -15
  21. package/lib/plugins_core/functions/index.d.ts +5 -2
  22. package/lib/plugins_core/functions/index.js +21 -7
  23. package/lib/plugins_core/functions/utils.d.ts +3 -1
  24. package/lib/plugins_core/functions/utils.js +4 -1
  25. package/lib/plugins_core/functions/zisi.d.ts +1 -1
  26. package/lib/plugins_core/functions/zisi.js +4 -3
  27. package/lib/report/statsd.d.ts +1 -1
  28. package/lib/time/measure.d.ts +0 -1
  29. package/lib/utils/blobs.d.ts +14 -8
  30. package/lib/utils/blobs.js +46 -18
  31. package/lib/utils/frameworks_api.d.ts +45 -0
  32. package/lib/utils/frameworks_api.js +75 -0
  33. package/lib/utils/package.d.ts +1 -1
  34. package/package.json +4 -4
@@ -16,18 +16,18 @@ export declare const startBuild: (flags: Partial<BuildFlags>) => {
16
16
  cacheDir: string;
17
17
  sendStatus: boolean;
18
18
  saveConfig: boolean;
19
- apiHost?: string | undefined;
19
+ apiHost?: string;
20
20
  testOpts: import("./types.js").TestOptions;
21
21
  statsd: {
22
22
  port: number;
23
23
  };
24
- timeline: string;
24
+ timeline: "build" | string;
25
25
  cachedConfig: Record<string, unknown>;
26
26
  siteId: string;
27
27
  dry: false;
28
- context: string;
28
+ context: "production" | string;
29
29
  statsdOpts: {
30
- host?: number | undefined;
30
+ host?: number;
31
31
  port: number;
32
32
  };
33
33
  };
package/lib/core/build.js CHANGED
@@ -280,9 +280,10 @@ const initAndRunBuild = async function ({ pluginsOptions, netlifyConfig, configO
280
280
  context,
281
281
  systemLog,
282
282
  });
283
- const pluginsEnv = featureFlags.build_inject_blobs_context
284
- ? { ...childEnv, ...getBlobsEnvironmentContext({ api, deployId: deployId, siteId: siteInfo?.id, token }) }
285
- : childEnv;
283
+ const pluginsEnv = {
284
+ ...childEnv,
285
+ ...getBlobsEnvironmentContext({ api, deployId: deployId, siteId: siteInfo?.id, token }),
286
+ };
286
287
  if (pluginsOptionsA?.length) {
287
288
  const buildPlugins = {};
288
289
  for (const plugin of pluginsOptionsA) {
@@ -394,7 +395,7 @@ const runBuild = async function ({ childProcesses, pluginsOptions, netlifyConfig
394
395
  });
395
396
  const { steps, events } = timeline === 'dev' ? getDevSteps(devCommand, pluginsSteps, eventHandlers) : getSteps(pluginsSteps, eventHandlers);
396
397
  if (dry) {
397
- await doDryRun({ buildDir, steps, netlifyConfig, constants, buildbotServerSocket, logs });
398
+ await doDryRun({ buildDir, steps, netlifyConfig, constants, buildbotServerSocket, logs, featureFlags });
398
399
  return { netlifyConfig };
399
400
  }
400
401
  const { stepsCount, netlifyConfig: netlifyConfigA, statuses, failedPlugins, timers: timersB, configMutations, metrics, } = await runSteps({
package/lib/core/dry.d.ts CHANGED
@@ -1,8 +1,9 @@
1
- export function doDryRun({ buildDir, steps, netlifyConfig, constants, buildbotServerSocket, logs }: {
1
+ export function doDryRun({ buildDir, steps, netlifyConfig, constants, buildbotServerSocket, logs, featureFlags, }: {
2
2
  buildDir: any;
3
3
  steps: any;
4
4
  netlifyConfig: any;
5
5
  constants: any;
6
6
  buildbotServerSocket: any;
7
7
  logs: any;
8
+ featureFlags: any;
8
9
  }): Promise<void>;
package/lib/core/dry.js CHANGED
@@ -2,22 +2,22 @@ import pFilter from 'p-filter';
2
2
  import { logDryRunStart, logDryRunStep, logDryRunEnd } from '../log/messages/dry.js';
3
3
  import { runsOnlyOnBuildFailure } from '../plugins/events.js';
4
4
  // If the `dry` flag is specified, do a dry run
5
- export const doDryRun = async function ({ buildDir, steps, netlifyConfig, constants, buildbotServerSocket, logs }) {
6
- const successSteps = await pFilter(steps, ({ event, condition }) => shouldIncludeStep({ buildDir, event, condition, netlifyConfig, constants, buildbotServerSocket }));
5
+ export const doDryRun = async function ({ buildDir, steps, netlifyConfig, constants, buildbotServerSocket, logs, featureFlags, }) {
6
+ const successSteps = await pFilter(steps, ({ event, condition }) => shouldIncludeStep({ buildDir, event, condition, netlifyConfig, constants, buildbotServerSocket, featureFlags }));
7
7
  const eventWidth = Math.max(...successSteps.map(getEventLength));
8
8
  const stepsCount = successSteps.length;
9
9
  logDryRunStart({ logs, eventWidth, stepsCount });
10
- successSteps.forEach((step, index) => {
11
- if (step.quiet) {
12
- return;
13
- }
10
+ successSteps
11
+ .filter((step) => !step.quiet)
12
+ .forEach((step, index) => {
14
13
  logDryRunStep({ logs, step, index, netlifyConfig, eventWidth, stepsCount });
15
14
  });
16
15
  logDryRunEnd(logs);
17
16
  };
18
- const shouldIncludeStep = async function ({ buildDir, event, condition, netlifyConfig, constants, buildbotServerSocket, }) {
17
+ const shouldIncludeStep = async function ({ buildDir, event, condition, netlifyConfig, constants, buildbotServerSocket, featureFlags, }) {
19
18
  return (!runsOnlyOnBuildFailure(event) &&
20
- (condition === undefined || (await condition({ buildDir, constants, netlifyConfig, buildbotServerSocket }))));
19
+ (condition === undefined ||
20
+ (await condition({ buildDir, constants, netlifyConfig, buildbotServerSocket, featureFlags }))));
21
21
  };
22
22
  const getEventLength = function ({ event }) {
23
23
  return event.length;
@@ -10,13 +10,10 @@ const getFeatureFlag = function (name) {
10
10
  };
11
11
  // Default values for feature flags
12
12
  export const DEFAULT_FEATURE_FLAGS = {
13
- build_inject_blobs_context: false,
14
13
  buildbot_zisi_trace_nft: false,
15
14
  buildbot_zisi_esbuild_parser: false,
16
- buildbot_zisi_system_log: false,
17
- edge_functions_cache_cli: false,
18
- edge_functions_system_logger: false,
19
15
  netlify_build_reduced_output: false,
20
16
  netlify_build_updated_plugin_compatibility: false,
17
+ netlify_build_frameworks_api: false,
21
18
  netlify_build_plugin_system_log: false,
22
19
  };
@@ -1,3 +1,3 @@
1
1
  export declare const getBuildCommandDescription: (buildCommandOrigin: any) => any;
2
2
  /** Retrieve human-friendly plugin origin */
3
- export declare const getPluginOrigin: (loadedFrom: 'package.json' | string, origin: string) => string;
3
+ export declare const getPluginOrigin: (loadedFrom: "package.json" | string, origin: string) => string;
@@ -3,13 +3,14 @@ export function logBundleResults({ logs, results }: {
3
3
  results?: any[] | undefined;
4
4
  }): void;
5
5
  export function logFunctionsNonExistingDir(logs: any, relativeFunctionsSrc: any): void;
6
- export function logFunctionsToBundle({ logs, userFunctions, userFunctionsSrc, userFunctionsSrcExists, internalFunctions, internalFunctionsSrc, type, }: {
6
+ export function logFunctionsToBundle({ logs, userFunctions, userFunctionsSrc, userFunctionsSrcExists, internalFunctions, internalFunctionsSrc, frameworkFunctions, type, }: {
7
7
  logs: any;
8
8
  userFunctions: any;
9
9
  userFunctionsSrc: any;
10
10
  userFunctionsSrcExists: any;
11
11
  internalFunctions: any;
12
12
  internalFunctionsSrc: any;
13
+ frameworkFunctions: any;
13
14
  type?: string | undefined;
14
15
  }): void;
15
16
  export function logSecretsScanSkipMessage(logs: any, msg: any): void;
@@ -41,11 +41,18 @@ export const logFunctionsNonExistingDir = function (logs, relativeFunctionsSrc)
41
41
  log(logs, `The Netlify Functions setting targets a non-existing directory: ${relativeFunctionsSrc}`);
42
42
  };
43
43
  // Print the list of Netlify Functions about to be bundled
44
- export const logFunctionsToBundle = function ({ logs, userFunctions, userFunctionsSrc, userFunctionsSrcExists, internalFunctions, internalFunctionsSrc, type = 'Functions', }) {
44
+ export const logFunctionsToBundle = function ({ logs, userFunctions, userFunctionsSrc, userFunctionsSrcExists, internalFunctions, internalFunctionsSrc, frameworkFunctions, type = 'Functions', }) {
45
45
  if (internalFunctions.length !== 0) {
46
46
  log(logs, `Packaging ${type} from ${THEME.highlightWords(internalFunctionsSrc)} directory:`);
47
47
  logArray(logs, internalFunctions, { indent: false });
48
48
  }
49
+ if (frameworkFunctions.length !== 0) {
50
+ if (internalFunctions.length !== 0) {
51
+ log(logs, '');
52
+ }
53
+ log(logs, `Packaging ${type} generated by your framework:`);
54
+ logArray(logs, frameworkFunctions, { indent: false });
55
+ }
49
56
  if (!userFunctionsSrcExists) {
50
57
  return;
51
58
  }
@@ -53,7 +60,7 @@ export const logFunctionsToBundle = function ({ logs, userFunctions, userFunctio
53
60
  log(logs, `No ${type} were found in ${THEME.highlightWords(userFunctionsSrc)} directory`);
54
61
  return;
55
62
  }
56
- if (internalFunctions.length !== 0) {
63
+ if (internalFunctions.length !== 0 || frameworkFunctions.length !== 0) {
57
64
  log(logs, '');
58
65
  }
59
66
  log(logs, `Packaging ${type} from ${THEME.highlightWords(userFunctionsSrc)} directory:`);
@@ -1,4 +1,3 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
1
  import { Transform } from 'stream';
3
2
  import type { StandardStreams } from './stream.js';
4
3
  declare const flusherSymbol: unique symbol;
@@ -1,5 +1,3 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
- /// <reference types="node" resolution-mode="require"/>
3
1
  import type { ChildProcess } from '../plugins/spawn.js';
4
2
  import { Logs } from './logger.js';
5
3
  import type { OutputFlusher } from './output_flusher.js';
@@ -20,7 +20,7 @@ export function getUtils({ event, constants: { FUNCTIONS_SRC, INTERNAL_FUNCTIONS
20
20
  list: (optsA: any) => Promise<string[]>;
21
21
  getCacheDir: (optsA: any) => string;
22
22
  };
23
- run: (file: string, args?: object | string[] | undefined, options?: Record<string, unknown> | undefined) => import("execa").ExecaChildProcess<string>;
23
+ run: (file: string, args?: string[] | object, options?: Record<string, unknown>) => import("execa").ExecaChildProcess<string>;
24
24
  functions: {
25
25
  add: (src: any) => Promise<void>;
26
26
  list: any;
@@ -10,9 +10,9 @@ export declare const getSpawnInfo: () => {
10
10
  pluginPackageJson: PackageJson;
11
11
  };
12
12
  location: {
13
- event: 'load';
13
+ event: "load";
14
14
  packageName: string;
15
- loadedFrom: 'core';
16
- origin: 'core';
15
+ loadedFrom: "core";
16
+ origin: "core";
17
17
  };
18
18
  };
@@ -5,6 +5,7 @@ import semver from 'semver';
5
5
  import { DEFAULT_API_HOST } from '../../core/normalize_flags.js';
6
6
  import { logError } from '../../log/logger.js';
7
7
  import { getFileWithMetadata, getKeysToUpload, scanForBlobs } from '../../utils/blobs.js';
8
+ import { getBlobs } from '../../utils/frameworks_api.js';
8
9
  const coreStep = async function ({ logs, deployId, buildDir, packagePath, constants: { SITE_ID, NETLIFY_API_TOKEN, NETLIFY_API_HOST }, systemLog, }) {
9
10
  // This should never happen due to the condition check
10
11
  if (!deployId || !NETLIFY_API_TOKEN) {
@@ -29,22 +30,23 @@ const coreStep = async function ({ logs, deployId, buildDir, packagePath, consta
29
30
  systemLog('No blobs to upload to deploy store.');
30
31
  return {};
31
32
  }
32
- // If using the deploy config API, configure the store to use the region that
33
- // was configured for the deploy.
34
- if (!blobs.isLegacyDirectory) {
33
+ // If using the deploy config API or the Frameworks API, configure the store
34
+ // to use the region that was configured for the deploy. We don't do it for
35
+ // the legacy file-based upload API since that would be a breaking change.
36
+ if (blobs.apiVersion > 1) {
35
37
  storeOpts.experimentalRegion = 'auto';
36
38
  }
37
39
  const blobStore = getDeployStore(storeOpts);
38
- const keys = await getKeysToUpload(blobs.directory);
39
- if (keys.length === 0) {
40
+ const blobsToUpload = blobs.apiVersion >= 3 ? await getBlobs(blobs.directory) : await getKeysToUpload(blobs.directory);
41
+ if (blobsToUpload.length === 0) {
40
42
  systemLog('No blobs to upload to deploy store.');
41
43
  return {};
42
44
  }
43
- systemLog(`Uploading ${keys.length} blobs to deploy store`);
45
+ systemLog(`Uploading ${blobsToUpload.length} blobs to deploy store...`);
44
46
  try {
45
- await pMap(keys, async (key) => {
47
+ await pMap(blobsToUpload, async ({ key, contentPath, metadataPath }) => {
46
48
  systemLog(`Uploading blob ${key}`);
47
- const { data, metadata } = await getFileWithMetadata(blobs.directory, key);
49
+ const { data, metadata } = await getFileWithMetadata(key, contentPath, metadataPath);
48
50
  await blobStore.set(key, data, { metadata });
49
51
  }, { concurrency: 10 });
50
52
  }
@@ -1,4 +1,3 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
1
  import net from 'net';
3
2
  /**
4
3
  * Creates the Buildbot IPC client we use to initiate the deploy
@@ -1,33 +1,55 @@
1
- import { promises as fs } from 'fs';
2
- import { resolve } from 'path';
3
1
  import { mergeConfigs } from '@netlify/config';
4
2
  import { getConfigMutations } from '../../plugins/child/diff.js';
5
- import { filterConfig } from './util.js';
3
+ import { filterConfig, loadConfigFile } from './util.js';
6
4
  // The properties that can be set using this API. Each element represents a
7
5
  // path using dot-notation — e.g. `["build", "functions"]` represents the
8
6
  // `build.functions` property.
9
- const ALLOWED_PROPERTIES = [['images', 'remote_images']];
7
+ const ALLOWED_PROPERTIES = [
8
+ ['build', 'functions'],
9
+ ['build', 'publish'],
10
+ ['functions', '*'],
11
+ ['functions', '*', '*'],
12
+ ['headers'],
13
+ ['images', 'remote_images'],
14
+ ['redirects'],
15
+ ];
16
+ // For array properties, any values set in this API will be merged with the
17
+ // main configuration file in such a way that user-defined values always take
18
+ // precedence. The exception are these properties that let frameworks set
19
+ // values that should be evaluated before any user-defined values. They use
20
+ // a special notation where `redirects!` represents "forced redirects", etc.
21
+ const OVERRIDE_PROPERTIES = new Set(['redirects!']);
10
22
  const coreStep = async function ({ buildDir, netlifyConfig, packagePath, systemLog = () => {
11
23
  // no-op
12
24
  }, }) {
13
- const configPath = resolve(buildDir, packagePath ?? '', '.netlify/deploy/v1/config.json');
14
- let config = {};
25
+ let config;
15
26
  try {
16
- const data = await fs.readFile(configPath, 'utf8');
17
- config = JSON.parse(data);
27
+ config = await loadConfigFile(buildDir, packagePath);
18
28
  }
19
29
  catch (err) {
20
- // If the file doesn't exist, this is a non-error.
21
- if (err.code === 'ENOENT') {
22
- return {};
23
- }
24
- systemLog(`Failed to read Deploy Configuration API: ${err.message}`);
30
+ systemLog(`Failed to read Frameworks API: ${err.message}`);
25
31
  throw new Error('An error occured while processing the platform configurarion defined by your framework');
26
32
  }
33
+ if (!config) {
34
+ return {};
35
+ }
36
+ const configOverrides = {};
37
+ for (const key in config) {
38
+ // If the key uses the special notation for defining mutations that should
39
+ // take precedence over user-defined properties, extract the canonical
40
+ // property, set it on a different object, and delete it from the main one.
41
+ if (OVERRIDE_PROPERTIES.has(key)) {
42
+ const canonicalKey = key.slice(0, -1);
43
+ configOverrides[canonicalKey] = config[key];
44
+ delete config[key];
45
+ }
46
+ }
27
47
  // Filtering out any properties that can't be mutated using this API.
28
48
  const filteredConfig = filterConfig(config, [], ALLOWED_PROPERTIES, systemLog);
29
49
  // Merging the config extracted from the API with the initial config.
30
- const newConfig = mergeConfigs([filteredConfig, netlifyConfig], { concatenateArrays: true });
50
+ const newConfig = mergeConfigs([filteredConfig, netlifyConfig, configOverrides], {
51
+ concatenateArrays: true,
52
+ });
31
53
  // Diffing the initial and the new configs to compute the mutations (what
32
54
  // changed between them).
33
55
  const configMutations = getConfigMutations(netlifyConfig, newConfig, applyDeployConfig.event);
@@ -38,9 +60,8 @@ const coreStep = async function ({ buildDir, netlifyConfig, packagePath, systemL
38
60
  export const applyDeployConfig = {
39
61
  event: 'onBuild',
40
62
  coreStep,
41
- coreStepId: 'deploy_config',
42
- coreStepName: 'Applying Deploy Configuration',
63
+ coreStepId: 'frameworks_api_config',
64
+ coreStepName: 'Applying configuration from Frameworks API',
43
65
  coreStepDescription: () => '',
44
- condition: ({ featureFlags }) => featureFlags?.netlify_build_deploy_configuration_api,
45
66
  quiet: true,
46
67
  };
@@ -1,4 +1,6 @@
1
+ import type { NetlifyConfig } from '../../index.js';
1
2
  import { SystemLogger } from '../types.js';
3
+ export declare const loadConfigFile: (buildDir: string, packagePath?: string) => Promise<Partial<NetlifyConfig> | undefined>;
2
4
  /**
3
5
  * Checks whether a property can be defined using the Deploy Configuration API.
4
6
  */
@@ -1,5 +1,35 @@
1
+ import { promises as fs } from 'fs';
2
+ import { resolve } from 'path';
1
3
  import isPlainObject from 'is-plain-obj';
2
4
  import mapObject, { mapObjectSkip } from 'map-obj';
5
+ import { FRAMEWORKS_API_CONFIG_ENDPOINT } from '../../utils/frameworks_api.js';
6
+ export const loadConfigFile = async (buildDir, packagePath) => {
7
+ const configPath = resolve(buildDir, packagePath ?? '', FRAMEWORKS_API_CONFIG_ENDPOINT);
8
+ try {
9
+ const data = await fs.readFile(configPath, 'utf8');
10
+ return JSON.parse(data);
11
+ }
12
+ catch (err) {
13
+ // If the file doesn't exist, this is a non-error.
14
+ if (err.code !== 'ENOENT') {
15
+ throw err;
16
+ }
17
+ }
18
+ // The first version of this API was called "Deploy Configuration API" and it
19
+ // had `.netlify/deploy` as its base directory. For backwards-compatibility,
20
+ // we need to support that path for the config file.
21
+ const legacyConfigPath = resolve(buildDir, packagePath ?? '', '.netlify/deploy/v1/config.json');
22
+ try {
23
+ const data = await fs.readFile(legacyConfigPath, 'utf8');
24
+ return JSON.parse(data);
25
+ }
26
+ catch (err) {
27
+ // If the file doesn't exist, this is a non-error.
28
+ if (err.code !== 'ENOENT') {
29
+ throw err;
30
+ }
31
+ }
32
+ };
3
33
  /**
4
34
  * Checks whether a property matches a template that may contain wildcards.
5
35
  * Both the property and the template use a dot-notation represented as an
@@ -4,6 +4,7 @@ import pMap from 'p-map';
4
4
  import semver from 'semver';
5
5
  import { log, logError } from '../../log/logger.js';
6
6
  import { getFileWithMetadata, getKeysToUpload, scanForBlobs } from '../../utils/blobs.js';
7
+ import { getBlobs } from '../../utils/frameworks_api.js';
7
8
  const coreStep = async function ({ debug, logs, deployId, buildDir, quiet, packagePath, constants: { SITE_ID, NETLIFY_API_TOKEN, NETLIFY_API_HOST }, }) {
8
9
  // This should never happen due to the condition check
9
10
  if (!deployId || !NETLIFY_API_TOKEN) {
@@ -30,28 +31,29 @@ const coreStep = async function ({ debug, logs, deployId, buildDir, quiet, packa
30
31
  }
31
32
  return {};
32
33
  }
33
- // If using the deploy config API, configure the store to use the region that
34
- // was configured for the deploy.
35
- if (!blobs.isLegacyDirectory) {
34
+ // If using the deploy config API or the Frameworks API, configure the store
35
+ // to use the region that was configured for the deploy. We don't do it for
36
+ // the legacy file-based upload API since that would be a breaking change.
37
+ if (blobs.apiVersion > 1) {
36
38
  storeOpts.experimentalRegion = 'auto';
37
39
  }
38
40
  const blobStore = getDeployStore(storeOpts);
39
- const keys = await getKeysToUpload(blobs.directory);
40
- if (keys.length === 0) {
41
+ const blobsToUpload = blobs.apiVersion >= 3 ? await getBlobs(blobs.directory) : await getKeysToUpload(blobs.directory);
42
+ if (blobsToUpload.length === 0) {
41
43
  if (!quiet) {
42
44
  log(logs, 'No blobs to upload to deploy store.');
43
45
  }
44
46
  return {};
45
47
  }
46
48
  if (!quiet) {
47
- log(logs, `Uploading ${keys.length} blobs to deploy store...`);
49
+ log(logs, `Uploading ${blobsToUpload.length} blobs to deploy store...`);
48
50
  }
49
51
  try {
50
- await pMap(keys, async (key) => {
52
+ await pMap(blobsToUpload, async ({ key, contentPath, metadataPath }) => {
51
53
  if (debug && !quiet) {
52
54
  log(logs, `- Uploading blob ${key}`, { indent: true });
53
55
  }
54
- const { data, metadata } = await getFileWithMetadata(blobs.directory, key);
56
+ const { data, metadata } = await getFileWithMetadata(key, contentPath, metadataPath);
55
57
  await blobStore.set(key, data, { metadata });
56
58
  }, { concurrency: 10 });
57
59
  }
@@ -1,7 +1,7 @@
1
1
  import { Metric } from '../../core/report_metrics.js';
2
2
  export declare const bundleEdgeFunctions: {
3
3
  event: string;
4
- coreStep: ({ buildDir, constants: { EDGE_FUNCTIONS_DIST: distDirectory, EDGE_FUNCTIONS_SRC: srcDirectory, INTERNAL_EDGE_FUNCTIONS_SRC: internalSrcDirectory, IS_LOCAL: isRunningLocally, }, debug, systemLog, featureFlags, logs, netlifyConfig, edgeFunctionsBootstrapURL, }: {
4
+ coreStep: ({ buildDir, packagePath, constants: { EDGE_FUNCTIONS_DIST: distDirectory, EDGE_FUNCTIONS_SRC: srcDirectory, INTERNAL_EDGE_FUNCTIONS_SRC: internalSrcDirectory, IS_LOCAL: isRunningLocally, }, debug, systemLog, featureFlags, logs, netlifyConfig, edgeFunctionsBootstrapURL, }: {
5
5
  buildDir: string;
6
6
  packagePath: string;
7
7
  constants: Record<string, string>;
@@ -17,11 +17,13 @@ export declare const bundleEdgeFunctions: {
17
17
  coreStepId: string;
18
18
  coreStepName: string;
19
19
  coreStepDescription: () => string;
20
- condition: ({ buildDir, constants: { INTERNAL_EDGE_FUNCTIONS_SRC, EDGE_FUNCTIONS_SRC }, }: {
20
+ condition: ({ buildDir, constants: { INTERNAL_EDGE_FUNCTIONS_SRC, EDGE_FUNCTIONS_SRC }, featureFlags, packagePath, }: {
21
21
  buildDir: any;
22
22
  constants: {
23
23
  INTERNAL_EDGE_FUNCTIONS_SRC: any;
24
24
  EDGE_FUNCTIONS_SRC: any;
25
25
  };
26
+ featureFlags: any;
27
+ packagePath: any;
26
28
  }) => Promise<boolean>;
27
29
  };
@@ -4,23 +4,36 @@ import { bundle, find } from '@netlify/edge-bundler';
4
4
  import { pathExists } from 'path-exists';
5
5
  import { log, reduceLogLines } from '../../log/logger.js';
6
6
  import { logFunctionsToBundle } from '../../log/messages/core_steps.js';
7
+ import { FRAMEWORKS_API_EDGE_FUNCTIONS_ENDPOINT, FRAMEWORKS_API_EDGE_FUNCTIONS_IMPORT_MAP, } from '../../utils/frameworks_api.js';
7
8
  import { tagBundlingError } from './lib/error.js';
8
9
  import { validateEdgeFunctionsManifest } from './validate_manifest/validate_edge_functions_manifest.js';
9
10
  // TODO: Replace this with a custom cache directory.
10
11
  const DENO_CLI_CACHE_DIRECTORY = '.netlify/plugins/deno-cli';
11
12
  const IMPORT_MAP_FILENAME = 'edge-functions-import-map.json';
12
- const coreStep = async function ({ buildDir, constants: { EDGE_FUNCTIONS_DIST: distDirectory, EDGE_FUNCTIONS_SRC: srcDirectory, INTERNAL_EDGE_FUNCTIONS_SRC: internalSrcDirectory, IS_LOCAL: isRunningLocally, }, debug, systemLog, featureFlags, logs, netlifyConfig, edgeFunctionsBootstrapURL, }) {
13
- const { edge_functions: declarations = [] } = netlifyConfig;
14
- const { deno_import_map: userDefinedImportMap } = netlifyConfig.functions['*'];
13
+ const coreStep = async function ({ buildDir, packagePath, constants: { EDGE_FUNCTIONS_DIST: distDirectory, EDGE_FUNCTIONS_SRC: srcDirectory, INTERNAL_EDGE_FUNCTIONS_SRC: internalSrcDirectory, IS_LOCAL: isRunningLocally, }, debug, systemLog, featureFlags, logs, netlifyConfig, edgeFunctionsBootstrapURL, }) {
14
+ const { edge_functions: declarations = [], functions } = netlifyConfig;
15
+ const { deno_import_map: userDefinedImportMap } = functions['*'];
16
+ const importMapPaths = [userDefinedImportMap];
15
17
  const distPath = resolve(buildDir, distDirectory);
16
18
  const internalSrcPath = resolve(buildDir, internalSrcDirectory);
17
19
  const distImportMapPath = join(dirname(internalSrcPath), IMPORT_MAP_FILENAME);
18
20
  const srcPath = srcDirectory ? resolve(buildDir, srcDirectory) : undefined;
19
- const sourcePaths = [internalSrcPath, srcPath].filter(Boolean);
20
- logFunctions({ internalSrcDirectory, internalSrcPath, logs, srcDirectory, srcPath });
21
- // If we're running in buildbot and the feature flag is enabled, we set the
22
- // Deno cache dir to a directory that is persisted between builds.
23
- const cacheDirectory = !isRunningLocally && featureFlags.edge_functions_cache_cli ? resolve(buildDir, DENO_CLI_CACHE_DIRECTORY) : undefined;
21
+ const frameworksAPISrcPath = resolve(buildDir, packagePath || '', FRAMEWORKS_API_EDGE_FUNCTIONS_ENDPOINT);
22
+ const generatedFunctionPaths = [internalSrcPath];
23
+ if (featureFlags.netlify_build_frameworks_api) {
24
+ if (await pathExists(frameworksAPISrcPath)) {
25
+ generatedFunctionPaths.unshift(frameworksAPISrcPath);
26
+ }
27
+ const frameworkImportMap = resolve(buildDir, packagePath || '', FRAMEWORKS_API_EDGE_FUNCTIONS_ENDPOINT, FRAMEWORKS_API_EDGE_FUNCTIONS_IMPORT_MAP);
28
+ if (await pathExists(frameworkImportMap)) {
29
+ importMapPaths.push(frameworkImportMap);
30
+ }
31
+ }
32
+ const sourcePaths = [...generatedFunctionPaths, srcPath].filter(Boolean);
33
+ logFunctions({ frameworksAPISrcPath, internalSrcDirectory, internalSrcPath, logs, srcDirectory, srcPath });
34
+ // If we're running in buildbot, we set the Deno cache dir to a directory
35
+ // that is persisted between builds.
36
+ const cacheDirectory = !isRunningLocally ? resolve(buildDir, DENO_CLI_CACHE_DIRECTORY) : undefined;
24
37
  // Cleaning up the dist directory, in case it has any artifacts from previous
25
38
  // builds.
26
39
  try {
@@ -48,10 +61,10 @@ const coreStep = async function ({ buildDir, constants: { EDGE_FUNCTIONS_DIST: d
48
61
  debug,
49
62
  distImportMapPath,
50
63
  featureFlags,
51
- importMapPaths: [userDefinedImportMap],
64
+ importMapPaths,
52
65
  userLogger: (...args) => log(logs, reduceLogLines(args)),
53
- systemLogger: featureFlags.edge_functions_system_logger ? systemLog : undefined,
54
- internalSrcFolder: internalSrcPath,
66
+ systemLogger: systemLog,
67
+ internalSrcFolder: generatedFunctionPaths,
55
68
  bootstrapURL: edgeFunctionsBootstrapURL,
56
69
  vendorDirectory,
57
70
  });
@@ -84,19 +97,27 @@ const getMetrics = (manifest) => {
84
97
  // one configured by the user or the internal one) exists. We use a dynamic
85
98
  // `condition` because the directories might be created by the build command
86
99
  // or plugins.
87
- const hasEdgeFunctionsDirectories = async function ({ buildDir, constants: { INTERNAL_EDGE_FUNCTIONS_SRC, EDGE_FUNCTIONS_SRC }, }) {
100
+ const hasEdgeFunctionsDirectories = async function ({ buildDir, constants: { INTERNAL_EDGE_FUNCTIONS_SRC, EDGE_FUNCTIONS_SRC }, featureFlags, packagePath, }) {
88
101
  const hasFunctionsSrc = EDGE_FUNCTIONS_SRC !== undefined && EDGE_FUNCTIONS_SRC !== '';
89
102
  if (hasFunctionsSrc) {
90
103
  return true;
91
104
  }
92
105
  const internalFunctionsSrc = resolve(buildDir, INTERNAL_EDGE_FUNCTIONS_SRC);
93
- return await pathExists(internalFunctionsSrc);
106
+ if (await pathExists(internalFunctionsSrc)) {
107
+ return true;
108
+ }
109
+ if (featureFlags.netlify_build_frameworks_api) {
110
+ const frameworkFunctionsSrc = resolve(buildDir, packagePath || '', FRAMEWORKS_API_EDGE_FUNCTIONS_ENDPOINT);
111
+ return await pathExists(frameworkFunctionsSrc);
112
+ }
113
+ return false;
94
114
  };
95
- const logFunctions = async ({ internalSrcDirectory, internalSrcPath, logs, srcDirectory: userFunctionsSrc, srcPath, }) => {
96
- const [userFunctionsSrcExists, userFunctions, internalFunctions] = await Promise.all([
115
+ const logFunctions = async ({ frameworksAPISrcPath, internalSrcDirectory, internalSrcPath, logs, srcDirectory: userFunctionsSrc, srcPath, }) => {
116
+ const [userFunctionsSrcExists, userFunctions, internalFunctions, frameworkFunctions] = await Promise.all([
97
117
  srcPath ? pathExists(srcPath) : Promise.resolve(false),
98
118
  srcPath ? find([srcPath]) : Promise.resolve([]),
99
119
  find([internalSrcPath]),
120
+ frameworksAPISrcPath ? find([frameworksAPISrcPath]) : Promise.resolve([]),
100
121
  ]);
101
122
  logFunctionsToBundle({
102
123
  logs,
@@ -105,6 +126,7 @@ const logFunctions = async ({ internalSrcDirectory, internalSrcPath, logs, srcDi
105
126
  userFunctionsSrcExists,
106
127
  internalFunctions: internalFunctions.map(({ name }) => name),
107
128
  internalFunctionsSrc: internalSrcDirectory,
129
+ frameworkFunctions: frameworkFunctions.map(({ name }) => name),
108
130
  type: 'Edge Functions',
109
131
  });
110
132
  };
@@ -1,6 +1,6 @@
1
1
  export declare const bundleFunctions: {
2
2
  event: string;
3
- coreStep: ({ childEnv, constants: { INTERNAL_FUNCTIONS_SRC: relativeInternalFunctionsSrc, IS_LOCAL: isRunningLocally, FUNCTIONS_SRC: relativeFunctionsSrc, FUNCTIONS_DIST: relativeFunctionsDist, }, buildDir, logs, netlifyConfig, featureFlags, repositoryRoot, userNodeVersion, systemLog, }: {
3
+ coreStep: ({ childEnv, constants: { INTERNAL_FUNCTIONS_SRC: relativeInternalFunctionsSrc, IS_LOCAL: isRunningLocally, FUNCTIONS_SRC: relativeFunctionsSrc, FUNCTIONS_DIST: relativeFunctionsDist, }, buildDir, packagePath, logs, netlifyConfig, featureFlags, repositoryRoot, userNodeVersion, systemLog, }: {
4
4
  childEnv: any;
5
5
  constants: {
6
6
  INTERNAL_FUNCTIONS_SRC: any;
@@ -9,6 +9,7 @@ export declare const bundleFunctions: {
9
9
  FUNCTIONS_DIST: any;
10
10
  };
11
11
  buildDir: any;
12
+ packagePath: any;
12
13
  logs: any;
13
14
  netlifyConfig: any;
14
15
  featureFlags: any;
@@ -34,12 +35,14 @@ export declare const bundleFunctions: {
34
35
  coreStepId: string;
35
36
  coreStepName: string;
36
37
  coreStepDescription: () => string;
37
- condition: ({ buildDir, constants: { INTERNAL_FUNCTIONS_SRC, FUNCTIONS_SRC } }: {
38
+ condition: ({ buildDir, constants: { INTERNAL_FUNCTIONS_SRC, FUNCTIONS_SRC }, featureFlags, packagePath, }: {
38
39
  buildDir: any;
39
40
  constants: {
40
41
  INTERNAL_FUNCTIONS_SRC: any;
41
42
  FUNCTIONS_SRC: any;
42
43
  };
44
+ featureFlags: any;
45
+ packagePath: any;
43
46
  }) => Promise<boolean>;
44
47
  };
45
48
  export declare const zipItAndShipIt: {
@@ -4,6 +4,7 @@ import { pathExists } from 'path-exists';
4
4
  import { addErrorInfo } from '../../error/info.js';
5
5
  import { log } from '../../log/logger.js';
6
6
  import { logBundleResults, logFunctionsNonExistingDir, logFunctionsToBundle } from '../../log/messages/core_steps.js';
7
+ import { FRAMEWORKS_API_FUNCTIONS_ENDPOINT } from '../../utils/frameworks_api.js';
7
8
  import { getZipError } from './error.js';
8
9
  import { getUserAndInternalFunctions, validateFunctionsSrc } from './utils.js';
9
10
  import { getZisiParameters } from './zisi.js';
@@ -45,7 +46,7 @@ const validateCustomRoutes = function (functions) {
45
46
  }
46
47
  }
47
48
  };
48
- const zipFunctionsAndLogResults = async ({ buildDir, childEnv, featureFlags, functionsConfig, functionsDist, functionsSrc, internalFunctionsSrc, isRunningLocally, logs, repositoryRoot, userNodeVersion, systemLog, }) => {
49
+ const zipFunctionsAndLogResults = async ({ buildDir, childEnv, featureFlags, functionsConfig, functionsDist, functionsSrc, frameworkFunctionsSrc, internalFunctionsSrc, isRunningLocally, logs, repositoryRoot, userNodeVersion, systemLog, }) => {
49
50
  const zisiParameters = getZisiParameters({
50
51
  buildDir,
51
52
  childEnv,
@@ -61,7 +62,7 @@ const zipFunctionsAndLogResults = async ({ buildDir, childEnv, featureFlags, fun
61
62
  try {
62
63
  // Printing an empty line before bundling output.
63
64
  log(logs, '');
64
- const sourceDirectories = [internalFunctionsSrc, functionsSrc].filter(Boolean);
65
+ const sourceDirectories = [frameworkFunctionsSrc, internalFunctionsSrc, functionsSrc].filter(Boolean);
65
66
  const results = await zipItAndShipIt.zipFunctions(sourceDirectories, functionsDist, zisiParameters);
66
67
  validateCustomRoutes(results);
67
68
  const bundlers = Array.from(getBundlers(results));
@@ -73,18 +74,22 @@ const zipFunctionsAndLogResults = async ({ buildDir, childEnv, featureFlags, fun
73
74
  }
74
75
  };
75
76
  // Plugin to package Netlify functions with @netlify/zip-it-and-ship-it
76
- const coreStep = async function ({ childEnv, constants: { INTERNAL_FUNCTIONS_SRC: relativeInternalFunctionsSrc, IS_LOCAL: isRunningLocally, FUNCTIONS_SRC: relativeFunctionsSrc, FUNCTIONS_DIST: relativeFunctionsDist, }, buildDir, logs, netlifyConfig, featureFlags, repositoryRoot, userNodeVersion, systemLog, }) {
77
+ const coreStep = async function ({ childEnv, constants: { INTERNAL_FUNCTIONS_SRC: relativeInternalFunctionsSrc, IS_LOCAL: isRunningLocally, FUNCTIONS_SRC: relativeFunctionsSrc, FUNCTIONS_DIST: relativeFunctionsDist, }, buildDir, packagePath, logs, netlifyConfig, featureFlags, repositoryRoot, userNodeVersion, systemLog, }) {
77
78
  const functionsSrc = relativeFunctionsSrc === undefined ? undefined : resolve(buildDir, relativeFunctionsSrc);
78
79
  const functionsDist = resolve(buildDir, relativeFunctionsDist);
79
80
  const internalFunctionsSrc = resolve(buildDir, relativeInternalFunctionsSrc);
80
81
  const internalFunctionsSrcExists = await pathExists(internalFunctionsSrc);
82
+ const frameworkFunctionsSrc = resolve(buildDir, packagePath || '', FRAMEWORKS_API_FUNCTIONS_ENDPOINT);
83
+ const frameworkFunctionsSrcExists = await pathExists(frameworkFunctionsSrc);
81
84
  const functionsSrcExists = await validateFunctionsSrc({ functionsSrc, relativeFunctionsSrc });
82
- const [userFunctions = [], internalFunctions = []] = await getUserAndInternalFunctions({
85
+ const [userFunctions = [], internalFunctions = [], frameworkFunctions = []] = await getUserAndInternalFunctions({
83
86
  featureFlags,
84
87
  functionsSrc,
85
88
  functionsSrcExists,
86
89
  internalFunctionsSrc,
87
90
  internalFunctionsSrcExists,
91
+ frameworkFunctionsSrc,
92
+ frameworkFunctionsSrcExists,
88
93
  });
89
94
  if (functionsSrc && !functionsSrcExists) {
90
95
  logFunctionsNonExistingDir(logs, relativeFunctionsSrc);
@@ -99,8 +104,9 @@ const coreStep = async function ({ childEnv, constants: { INTERNAL_FUNCTIONS_SRC
99
104
  userFunctionsSrcExists: functionsSrcExists,
100
105
  internalFunctions,
101
106
  internalFunctionsSrc: relativeInternalFunctionsSrc,
107
+ frameworkFunctions,
102
108
  });
103
- if (userFunctions.length === 0 && internalFunctions.length === 0) {
109
+ if (userFunctions.length === 0 && internalFunctions.length === 0 && frameworkFunctions.length === 0) {
104
110
  return {};
105
111
  }
106
112
  const { bundlers } = await zipFunctionsAndLogResults({
@@ -110,6 +116,7 @@ const coreStep = async function ({ childEnv, constants: { INTERNAL_FUNCTIONS_SRC
110
116
  functionsConfig: netlifyConfig.functions,
111
117
  functionsDist,
112
118
  functionsSrc,
119
+ frameworkFunctionsSrc,
113
120
  internalFunctionsSrc,
114
121
  isRunningLocally,
115
122
  logs,
@@ -129,13 +136,20 @@ const coreStep = async function ({ childEnv, constants: { INTERNAL_FUNCTIONS_SRC
129
136
  // one configured by the user or the internal one) exists. We use a dynamic
130
137
  // `condition` because the directories might be created by the build command
131
138
  // or plugins.
132
- const hasFunctionsDirectories = async function ({ buildDir, constants: { INTERNAL_FUNCTIONS_SRC, FUNCTIONS_SRC } }) {
139
+ const hasFunctionsDirectories = async function ({ buildDir, constants: { INTERNAL_FUNCTIONS_SRC, FUNCTIONS_SRC }, featureFlags, packagePath, }) {
133
140
  const hasFunctionsSrc = FUNCTIONS_SRC !== undefined && FUNCTIONS_SRC !== '';
134
141
  if (hasFunctionsSrc) {
135
142
  return true;
136
143
  }
137
144
  const internalFunctionsSrc = resolve(buildDir, INTERNAL_FUNCTIONS_SRC);
138
- return await pathExists(internalFunctionsSrc);
145
+ if (await pathExists(internalFunctionsSrc)) {
146
+ return true;
147
+ }
148
+ if (featureFlags.netlify_build_frameworks_api) {
149
+ const frameworkFunctionsSrc = resolve(buildDir, packagePath || '', FRAMEWORKS_API_FUNCTIONS_ENDPOINT);
150
+ return await pathExists(frameworkFunctionsSrc);
151
+ }
152
+ return false;
139
153
  };
140
154
  export const bundleFunctions = {
141
155
  event: 'onBuild',
@@ -1,9 +1,11 @@
1
- export function getUserAndInternalFunctions({ featureFlags, functionsSrc, functionsSrcExists, internalFunctionsSrc, internalFunctionsSrcExists, }: {
1
+ export function getUserAndInternalFunctions({ featureFlags, functionsSrc, functionsSrcExists, internalFunctionsSrc, internalFunctionsSrcExists, frameworkFunctionsSrc, frameworkFunctionsSrcExists, }: {
2
2
  featureFlags: any;
3
3
  functionsSrc: any;
4
4
  functionsSrcExists: any;
5
5
  internalFunctionsSrc: any;
6
6
  internalFunctionsSrcExists: any;
7
+ frameworkFunctionsSrc: any;
8
+ frameworkFunctionsSrcExists: any;
7
9
  }): Promise<any[]>;
8
10
  export function validateFunctionsSrc({ functionsSrc, relativeFunctionsSrc }: {
9
11
  functionsSrc: any;
@@ -13,11 +13,14 @@ const getRelativeFunctionMainFiles = async function ({ featureFlags, functionsSr
13
13
  const functions = await listFunctions(functionsSrc, { featureFlags: zisiFeatureFlags });
14
14
  return functions.map(({ mainFile }) => relative(functionsSrc, mainFile));
15
15
  };
16
- export const getUserAndInternalFunctions = ({ featureFlags, functionsSrc, functionsSrcExists, internalFunctionsSrc, internalFunctionsSrcExists, }) => {
16
+ export const getUserAndInternalFunctions = ({ featureFlags, functionsSrc, functionsSrcExists, internalFunctionsSrc, internalFunctionsSrcExists, frameworkFunctionsSrc, frameworkFunctionsSrcExists, }) => {
17
17
  const paths = [
18
18
  functionsSrcExists ? functionsSrc : undefined,
19
19
  internalFunctionsSrcExists ? internalFunctionsSrc : undefined,
20
20
  ];
21
+ if (featureFlags.netlify_build_frameworks_api && frameworkFunctionsSrcExists) {
22
+ paths.push(frameworkFunctionsSrc);
23
+ }
21
24
  return Promise.all(paths.map((path) => path && getRelativeFunctionMainFiles({ featureFlags, functionsSrc: path })));
22
25
  };
23
26
  // Returns `true` if the functions directory exists and is valid. Returns
@@ -6,7 +6,7 @@ type GetZisiParametersType = {
6
6
  featureFlags: FeatureFlags;
7
7
  functionsConfig: Record<string, any>;
8
8
  functionsDist: string;
9
- internalFunctionsSrc: string;
9
+ internalFunctionsSrc: string | undefined;
10
10
  isRunningLocally: boolean;
11
11
  repositoryRoot: string;
12
12
  userNodeVersion: string;
@@ -25,8 +25,9 @@ export const getZisiParameters = ({ buildDir, childEnv, featureFlags, functionsC
25
25
  normalizeFunctionConfig({ buildDir, functionConfig: object, isRunningLocally, nodeVersion }),
26
26
  ]);
27
27
  const zisiFeatureFlags = getZisiFeatureFlags(featureFlags);
28
- // Only internal functions are allowed to have a json config file
29
- const configFileDirectories = [resolve(internalFunctionsSrc)];
28
+ // Only the legacy internal functions directory is allowed to have a JSON
29
+ // config file.
30
+ const configFileDirectories = internalFunctionsSrc ? [resolve(internalFunctionsSrc)] : undefined;
30
31
  return {
31
32
  basePath: buildDir,
32
33
  config,
@@ -35,7 +36,7 @@ export const getZisiParameters = ({ buildDir, childEnv, featureFlags, functionsC
35
36
  repositoryRoot,
36
37
  configFileDirectories,
37
38
  internalSrcFolder: internalFunctionsSrc,
38
- systemLog: featureFlags.buildbot_zisi_system_log ? systemLog : undefined,
39
+ systemLog,
39
40
  };
40
41
  };
41
42
  // The function configuration keys returned by @netlify/config are not an exact
@@ -4,7 +4,7 @@ export interface InputStatsDOptions {
4
4
  port?: number;
5
5
  }
6
6
  export type StatsDOptions = Required<InputStatsDOptions>;
7
- export declare const validateStatsDOptions: (statsdOpts: InputStatsDOptions) => statsdOpts is Required<InputStatsDOptions>;
7
+ export declare const validateStatsDOptions: (statsdOpts: InputStatsDOptions) => statsdOpts is StatsDOptions;
8
8
  /**
9
9
  * Start a new StatsD Client and a new UDP socket
10
10
  */
@@ -1,4 +1,3 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
1
  /** Starts a timer */
3
2
  export declare const startTimer: NodeJS.HRTime;
4
3
  /** Stops a timer */
@@ -1,4 +1,3 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
1
  /** Retrieve the absolute path of the deploy scoped internal blob directories */
3
2
  export declare const getBlobsDirs: (buildDir: string, packagePath?: string) => string[];
4
3
  interface EnvironmentContext {
@@ -17,21 +16,28 @@ export declare const getBlobsEnvironmentContext: ({ api, deployId, siteId, token
17
16
  };
18
17
  /**
19
18
  * Detect if there are any blobs to upload, and if so, what directory they're
20
- * in and whether that directory is the legacy `.netlify/blobs` path or the
21
- * newer deploy config API endpoint.
19
+ * in and what version of the file-based API is being used.
22
20
  *
23
21
  * @param buildDir The build directory. (current working directory where the build is executed)
24
- * @param packagePath An optional package path for mono repositories
22
+ * @param packagePath An optional package path for monorepos
25
23
  * @returns
26
24
  */
27
25
  export declare const scanForBlobs: (buildDir: string, packagePath?: string) => Promise<{
26
+ apiVersion: number;
28
27
  directory: string;
29
- isLegacyDirectory: boolean;
30
28
  } | null>;
31
- /** Given output directory, find all file paths to upload excluding metadata files */
32
- export declare const getKeysToUpload: (blobsDir: string) => Promise<string[]>;
29
+ /**
30
+ * Returns the blobs that should be uploaded for a given directory tree. The
31
+ * result is an array with the blob key, the path to its data file, and the
32
+ * path to its metadata file.
33
+ */
34
+ export declare const getKeysToUpload: (blobsDir: string) => Promise<{
35
+ key: string;
36
+ contentPath: string;
37
+ metadataPath: string;
38
+ }[]>;
33
39
  /** Read a file and its metadata file from the blobs directory */
34
- export declare const getFileWithMetadata: (blobsDir: string, key: string) => Promise<{
40
+ export declare const getFileWithMetadata: (key: string, contentPath: string, metadataPath?: string) => Promise<{
35
41
  data: Buffer;
36
42
  metadata: Record<string, string>;
37
43
  }>;
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { fdir } from 'fdir';
4
4
  import { DEFAULT_API_HOST } from '../core/normalize_flags.js';
5
+ import { FRAMEWORKS_API_BLOBS_ENDPOINT } from './frameworks_api.js';
5
6
  const LEGACY_BLOBS_PATH = '.netlify/blobs/deploy';
6
7
  const DEPLOY_CONFIG_BLOBS_PATH = '.netlify/deploy/v1/blobs/deploy';
7
8
  /** Retrieve the absolute path of the deploy scoped internal blob directories */
@@ -27,51 +28,78 @@ export const getBlobsEnvironmentContext = ({ api = { host: DEFAULT_API_HOST, sch
27
28
  };
28
29
  /**
29
30
  * Detect if there are any blobs to upload, and if so, what directory they're
30
- * in and whether that directory is the legacy `.netlify/blobs` path or the
31
- * newer deploy config API endpoint.
31
+ * in and what version of the file-based API is being used.
32
32
  *
33
33
  * @param buildDir The build directory. (current working directory where the build is executed)
34
- * @param packagePath An optional package path for mono repositories
34
+ * @param packagePath An optional package path for monorepos
35
35
  * @returns
36
36
  */
37
37
  export const scanForBlobs = async function (buildDir, packagePath) {
38
- const blobsDir = path.resolve(buildDir, packagePath || '', DEPLOY_CONFIG_BLOBS_PATH);
39
- const blobsDirScan = await new fdir().onlyCounts().crawl(blobsDir).withPromise();
40
- if (blobsDirScan.files > 0) {
38
+ // We start by looking for files using the Frameworks API.
39
+ const frameworkBlobsDir = path.resolve(buildDir, packagePath || '', FRAMEWORKS_API_BLOBS_ENDPOINT, 'deploy');
40
+ const frameworkBlobsDirScan = await new fdir().onlyCounts().crawl(frameworkBlobsDir).withPromise();
41
+ if (frameworkBlobsDirScan.files > 0) {
41
42
  return {
42
- directory: blobsDir,
43
- isLegacyDirectory: false,
43
+ apiVersion: 3,
44
+ directory: frameworkBlobsDir,
44
45
  };
45
46
  }
47
+ // Next, we look for files using the legacy Deploy Configuration API. It was
48
+ // short-lived and not really documented, but we do have sites relying on
49
+ // it, so we must support it for backwards-compatibility.
50
+ const deployConfigBlobsDir = path.resolve(buildDir, packagePath || '', DEPLOY_CONFIG_BLOBS_PATH);
51
+ const deployConfigBlobsDirScan = await new fdir().onlyCounts().crawl(deployConfigBlobsDir).withPromise();
52
+ if (deployConfigBlobsDirScan.files > 0) {
53
+ return {
54
+ apiVersion: 2,
55
+ directory: deployConfigBlobsDir,
56
+ };
57
+ }
58
+ // Finally, we look for files using the initial spec for file-based Blobs
59
+ // uploads.
46
60
  const legacyBlobsDir = path.resolve(buildDir, packagePath || '', LEGACY_BLOBS_PATH);
47
61
  const legacyBlobsDirScan = await new fdir().onlyCounts().crawl(legacyBlobsDir).withPromise();
48
62
  if (legacyBlobsDirScan.files > 0) {
49
63
  return {
64
+ apiVersion: 1,
50
65
  directory: legacyBlobsDir,
51
- isLegacyDirectory: true,
52
66
  };
53
67
  }
54
68
  return null;
55
69
  };
56
70
  const METADATA_PREFIX = '$';
57
71
  const METADATA_SUFFIX = '.json';
58
- /** Given output directory, find all file paths to upload excluding metadata files */
72
+ /**
73
+ * Returns the blobs that should be uploaded for a given directory tree. The
74
+ * result is an array with the blob key, the path to its data file, and the
75
+ * path to its metadata file.
76
+ */
59
77
  export const getKeysToUpload = async (blobsDir) => {
78
+ const blobsToUpload = [];
60
79
  const files = await new fdir()
61
80
  .withRelativePaths() // we want the relative path from the blobsDir
62
81
  .filter((fpath) => !path.basename(fpath).startsWith(METADATA_PREFIX))
63
82
  .crawl(blobsDir)
64
83
  .withPromise();
65
- // normalize the path separators to all use the forward slash
66
- return files.map((f) => f.split(path.sep).join('/'));
84
+ files.forEach((filePath) => {
85
+ const key = filePath.split(path.sep).join('/');
86
+ const contentPath = path.resolve(blobsDir, filePath);
87
+ const basename = path.basename(filePath);
88
+ const metadataPath = path.resolve(blobsDir, path.dirname(filePath), `${METADATA_PREFIX}${basename}${METADATA_SUFFIX}`);
89
+ blobsToUpload.push({
90
+ key,
91
+ contentPath,
92
+ metadataPath,
93
+ });
94
+ });
95
+ return blobsToUpload;
67
96
  };
68
97
  /** Read a file and its metadata file from the blobs directory */
69
- export const getFileWithMetadata = async (blobsDir, key) => {
70
- const contentPath = path.join(blobsDir, key);
71
- const dirname = path.dirname(key);
72
- const basename = path.basename(key);
73
- const metadataPath = path.join(blobsDir, dirname, `${METADATA_PREFIX}${basename}${METADATA_SUFFIX}`);
74
- const [data, metadata] = await Promise.all([readFile(contentPath), readMetadata(metadataPath)]).catch((err) => {
98
+ export const getFileWithMetadata = async (key, contentPath, metadataPath) => {
99
+ const [data, metadata] = await Promise.all([
100
+ readFile(contentPath),
101
+ metadataPath ? readMetadata(metadataPath) : {},
102
+ ]).catch((err) => {
75
103
  throw new Error(`Failed while reading '${key}' and its metadata: ${err.message}`);
76
104
  });
77
105
  return { data, metadata };
@@ -0,0 +1,45 @@
1
+ export declare const FRAMEWORKS_API_BLOBS_ENDPOINT = ".netlify/v1/blobs";
2
+ export declare const FRAMEWORKS_API_CONFIG_ENDPOINT = ".netlify/v1/config.json";
3
+ export declare const FRAMEWORKS_API_EDGE_FUNCTIONS_ENDPOINT = ".netlify/v1/edge-functions";
4
+ export declare const FRAMEWORKS_API_EDGE_FUNCTIONS_IMPORT_MAP = "import_map.json";
5
+ export declare const FRAMEWORKS_API_FUNCTIONS_ENDPOINT = ".netlify/v1/functions";
6
+ type DirectoryTreeFiles = Map<string, string[]>;
7
+ /**
8
+ * Traverses a directory tree in search of leaf files. The key of each leaf
9
+ * file is determined by its path relative to the base directory.
10
+ *
11
+ * For example, given the following directory tree:
12
+ *
13
+ * .netlify/
14
+ * └── v1/
15
+ * └── blobs/
16
+ * └── deploy/
17
+ * ├── example.com/
18
+ * │ └── blob
19
+ * └── netlify.com/
20
+ * ├── blob
21
+ * └── blob.meta.json
22
+ *
23
+ * If this method is called on `.netlify/v1/blobs/deploy` with `blob` and
24
+ * `blob.meta.json` as leaf names, it will return the following Map:
25
+ *
26
+ * {
27
+ * "example.com" => [
28
+ * "/full/path/to/.netlify/v1/blobs/deploy/example.com/blob"
29
+ * ],
30
+ * "netlify.com" => [
31
+ * "/full/path/to/.netlify/v1/blobs/deploy/netlify.com/blob",
32
+ * "/full/path/to/.netlify/v1/blobs/deploy/netlify.com/blob.meta.json"
33
+ * ]
34
+ * }
35
+ */
36
+ export declare const findFiles: (directory: string, leafNames: Set<string>) => Promise<DirectoryTreeFiles>;
37
+ /**
38
+ * Finds blobs and their corresponding metadata files in a given directory.
39
+ */
40
+ export declare const getBlobs: (blobsDirectory: string) => Promise<{
41
+ key: string;
42
+ contentPath: string;
43
+ metadataPath?: string;
44
+ }[]>;
45
+ export {};
@@ -0,0 +1,75 @@
1
+ import { basename, dirname, resolve, sep } from 'node:path';
2
+ import { fdir } from 'fdir';
3
+ export const FRAMEWORKS_API_BLOBS_ENDPOINT = '.netlify/v1/blobs';
4
+ export const FRAMEWORKS_API_CONFIG_ENDPOINT = '.netlify/v1/config.json';
5
+ export const FRAMEWORKS_API_EDGE_FUNCTIONS_ENDPOINT = '.netlify/v1/edge-functions';
6
+ export const FRAMEWORKS_API_EDGE_FUNCTIONS_IMPORT_MAP = 'import_map.json';
7
+ export const FRAMEWORKS_API_FUNCTIONS_ENDPOINT = '.netlify/v1/functions';
8
+ /**
9
+ * Traverses a directory tree in search of leaf files. The key of each leaf
10
+ * file is determined by its path relative to the base directory.
11
+ *
12
+ * For example, given the following directory tree:
13
+ *
14
+ * .netlify/
15
+ * └── v1/
16
+ * └── blobs/
17
+ * └── deploy/
18
+ * ├── example.com/
19
+ * │ └── blob
20
+ * └── netlify.com/
21
+ * ├── blob
22
+ * └── blob.meta.json
23
+ *
24
+ * If this method is called on `.netlify/v1/blobs/deploy` with `blob` and
25
+ * `blob.meta.json` as leaf names, it will return the following Map:
26
+ *
27
+ * {
28
+ * "example.com" => [
29
+ * "/full/path/to/.netlify/v1/blobs/deploy/example.com/blob"
30
+ * ],
31
+ * "netlify.com" => [
32
+ * "/full/path/to/.netlify/v1/blobs/deploy/netlify.com/blob",
33
+ * "/full/path/to/.netlify/v1/blobs/deploy/netlify.com/blob.meta.json"
34
+ * ]
35
+ * }
36
+ */
37
+ export const findFiles = async (directory, leafNames) => {
38
+ const results = new Map();
39
+ const groups = await new fdir()
40
+ .withRelativePaths()
41
+ .filter((path) => leafNames.has(basename(path)))
42
+ .group()
43
+ .crawl(directory)
44
+ .withPromise();
45
+ groups.forEach(({ files }) => {
46
+ if (files.length === 0) {
47
+ return;
48
+ }
49
+ const key = dirname(files[0]).split(sep).join('/');
50
+ results.set(key, files.map((relativePath) => resolve(directory, relativePath)));
51
+ });
52
+ return results;
53
+ };
54
+ const BLOBS_CONTENT_FILE = 'blob';
55
+ const BLOBS_META_FILE = 'blob.meta.json';
56
+ /**
57
+ * Finds blobs and their corresponding metadata files in a given directory.
58
+ */
59
+ export const getBlobs = async (blobsDirectory) => {
60
+ const files = await findFiles(blobsDirectory, new Set([BLOBS_CONTENT_FILE, BLOBS_META_FILE]));
61
+ const blobs = [];
62
+ files.forEach((filePaths, key) => {
63
+ const contentPath = filePaths.find((path) => basename(path) === 'blob');
64
+ if (!contentPath) {
65
+ return;
66
+ }
67
+ const metadataPath = filePaths.find((path) => basename(path) === BLOBS_META_FILE);
68
+ blobs.push({
69
+ key,
70
+ contentPath,
71
+ metadataPath,
72
+ });
73
+ });
74
+ return blobs;
75
+ };
@@ -6,5 +6,5 @@ type PackageResult = {
6
6
  /**
7
7
  * Retrieve `package.json` from a specific directory
8
8
  */
9
- export declare const getPackageJson: (cwd: string, options?: Omit<Options, 'cwd'>) => Promise<PackageResult>;
9
+ export declare const getPackageJson: (cwd: string, options?: Omit<Options, "cwd">) => Promise<PackageResult>;
10
10
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/build",
3
- "version": "29.48.2",
3
+ "version": "29.49.1",
4
4
  "description": "Netlify build module",
5
5
  "type": "module",
6
6
  "exports": "./lib/index.js",
@@ -70,8 +70,8 @@
70
70
  "@bugsnag/js": "^7.0.0",
71
71
  "@netlify/blobs": "^7.3.0",
72
72
  "@netlify/cache-utils": "^5.1.5",
73
- "@netlify/config": "^20.15.3",
74
- "@netlify/edge-bundler": "12.0.1",
73
+ "@netlify/config": "^20.15.4",
74
+ "@netlify/edge-bundler": "12.1.1",
75
75
  "@netlify/framework-info": "^9.8.13",
76
76
  "@netlify/functions-utils": "^5.2.69",
77
77
  "@netlify/git-utils": "^5.1.1",
@@ -165,5 +165,5 @@
165
165
  "engines": {
166
166
  "node": "^14.16.0 || >=16.0.0"
167
167
  },
168
- "gitHead": "bcf99673bb311e1e86fe44e8902875328e90cf88"
168
+ "gitHead": "b993b18469d4355e164f605e742e69dcb5612905"
169
169
  }