@netlify/config 23.2.0 → 24.0.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.
@@ -1,7 +1,7 @@
1
1
  import { NetlifyAPI } from '@netlify/api';
2
- import { IntegrationResponse } from '../types/api.js';
2
+ import * as z from 'zod';
3
3
  import { ModeOption, TestOptions } from '../types/options.js';
4
- type GetSiteInfoOpts = {
4
+ type GetSiteInfoOptions = {
5
5
  siteId: string;
6
6
  accountId?: string;
7
7
  mode: ModeOption;
@@ -14,6 +14,22 @@ type GetSiteInfoOpts = {
14
14
  token: string;
15
15
  extensionApiBaseUrl: string;
16
16
  };
17
+ export type Extension = {
18
+ author: string | undefined;
19
+ extension_token: string | undefined;
20
+ has_build: boolean;
21
+ name: string;
22
+ slug: string;
23
+ version: string;
24
+ };
25
+ export type SiteInfo = {
26
+ accounts: MinimalAccount[];
27
+ extensions: Extension[];
28
+ siteInfo: Awaited<ReturnType<NetlifyAPI['getSite']>> & {
29
+ feature_flags?: Record<string, string | number | boolean>;
30
+ use_envelope?: boolean;
31
+ };
32
+ };
17
33
  /**
18
34
  * Retrieve Netlify Site information, if available.
19
35
  * Used to retrieve local build environment variables and UI build settings.
@@ -23,11 +39,7 @@ type GetSiteInfoOpts = {
23
39
  * Silently ignore API errors. For example the network connection might be down,
24
40
  * but local builds should still work regardless.
25
41
  */
26
- export declare const getSiteInfo: ({ api, siteId, accountId, mode, context, offline, testOpts, siteFeatureFlagPrefix, token, featureFlags, extensionApiBaseUrl, }: GetSiteInfoOpts) => Promise<{
27
- siteInfo: any;
28
- accounts: MinimalAccount[];
29
- integrations: IntegrationResponse[];
30
- }>;
42
+ export declare const getSiteInfo: ({ api, siteId, accountId, mode, context, offline, testOpts, siteFeatureFlagPrefix, token, featureFlags, extensionApiBaseUrl, }: GetSiteInfoOptions) => Promise<SiteInfo>;
31
43
  export type MinimalAccount = {
32
44
  id: string;
33
45
  name: string;
@@ -40,7 +52,16 @@ export type MinimalAccount = {
40
52
  type_slug: string;
41
53
  members_count: number;
42
54
  };
43
- type GetIntegrationsOpts = {
55
+ declare const ExtensionResponseSchema: z.ZodArray<z.ZodObject<{
56
+ author: z.ZodDefault<z.ZodOptional<z.ZodString>>;
57
+ extension_token: z.ZodDefault<z.ZodOptional<z.ZodString>>;
58
+ has_build: z.ZodBoolean;
59
+ name: z.ZodString;
60
+ slug: z.ZodString;
61
+ version: z.ZodString;
62
+ }, z.core.$strip>>;
63
+ export type ExtensionResponse = z.output<typeof ExtensionResponseSchema>;
64
+ type GetExtensionsOptions = {
44
65
  siteId?: string;
45
66
  accountId?: string;
46
67
  testOpts: TestOptions;
@@ -50,5 +71,5 @@ type GetIntegrationsOpts = {
50
71
  extensionApiBaseUrl: string;
51
72
  mode: ModeOption;
52
73
  };
53
- export declare const getIntegrations: ({ siteId, accountId, testOpts, offline, token, featureFlags, extensionApiBaseUrl, mode, }: GetIntegrationsOpts) => Promise<IntegrationResponse[]>;
74
+ export declare const getExtensions: ({ siteId, accountId, testOpts, offline, token, featureFlags, extensionApiBaseUrl, mode, }: GetExtensionsOptions) => Promise<Extension[]>;
54
75
  export {};
@@ -1,7 +1,9 @@
1
+ import * as z from 'zod';
1
2
  import { getEnvelope } from '../env/envelope.js';
2
3
  import { throwUserError } from '../error.js';
3
- import { EXTENSION_API_BASE_URL, EXTENSION_API_STAGING_BASE_URL, NETLIFY_API_BASE_URL, NETLIFY_API_STAGING_BASE_URL, } from '../integrations.js';
4
+ import { EXTENSION_API_BASE_URL, EXTENSION_API_STAGING_BASE_URL, NETLIFY_API_HOSTNAME, NETLIFY_API_STAGING_HOSTNAME, } from '../extensions.js';
4
5
  import { ERROR_CALL_TO_ACTION } from '../log/messages.js';
6
+ import { ROOT_PACKAGE_JSON } from '../utils/json.js';
5
7
  /**
6
8
  * Retrieve Netlify Site information, if available.
7
9
  * Used to retrieve local build environment variables and UI build settings.
@@ -15,12 +17,15 @@ export const getSiteInfo = async function ({ api, siteId, accountId, mode, conte
15
17
  const { env: testEnv = false } = testOpts;
16
18
  if (api === undefined || mode === 'buildbot' || testEnv) {
17
19
  const siteInfo = {};
18
- if (siteId !== undefined)
20
+ if (siteId !== undefined) {
19
21
  siteInfo.id = siteId;
20
- if (accountId !== undefined)
22
+ }
23
+ if (accountId !== undefined) {
21
24
  siteInfo.account_id = accountId;
22
- const integrations = mode === 'buildbot' && !offline
23
- ? await getIntegrations({
25
+ }
26
+ let extensions = [];
27
+ if (mode === 'buildbot' && !offline) {
28
+ extensions = await getExtensions({
24
29
  siteId,
25
30
  testOpts,
26
31
  offline,
@@ -29,31 +34,38 @@ export const getSiteInfo = async function ({ api, siteId, accountId, mode, conte
29
34
  featureFlags,
30
35
  extensionApiBaseUrl,
31
36
  mode,
32
- })
33
- : [];
34
- return { siteInfo, accounts: [], integrations };
37
+ });
38
+ }
39
+ return { accounts: [], extensions, siteInfo };
35
40
  }
36
- const [siteInfo, accounts, integrations] = await Promise.all([
41
+ const [siteInfo, accounts, extensions] = await Promise.all([
37
42
  getSite(api, siteId, siteFeatureFlagPrefix),
38
43
  getAccounts(api),
39
- getIntegrations({ siteId, testOpts, offline, accountId, token, featureFlags, extensionApiBaseUrl, mode }),
44
+ getExtensions({ siteId, testOpts, offline, accountId, token, featureFlags, extensionApiBaseUrl, mode }),
40
45
  ]);
46
+ // TODO(ndhoule): Investigate, but at this point, I'm fairly sure this is the default for all
47
+ // sites. If so, we can remove this conditional and always query for environment variables.
41
48
  if (siteInfo.use_envelope) {
42
49
  const envelope = await getEnvelope({ api, accountId: siteInfo.account_slug, siteId, context });
43
50
  siteInfo.build_settings.env = envelope;
44
51
  }
45
- return { siteInfo, accounts, integrations };
52
+ return { siteInfo, accounts, extensions };
46
53
  };
47
54
  const getSite = async function (api, siteId, siteFeatureFlagPrefix) {
48
55
  if (siteId === undefined) {
49
56
  return {};
50
57
  }
51
58
  try {
52
- const site = await api.getSite({ siteId, feature_flags: siteFeatureFlagPrefix });
59
+ const site = await api.getSite({
60
+ // @ts-expect-error: Internal parameter that instructs the API to include all the site's
61
+ // feature flags in the response.
62
+ feature_flags: siteFeatureFlagPrefix,
63
+ siteId,
64
+ });
53
65
  return { ...site, id: siteId };
54
66
  }
55
- catch (error) {
56
- throwUserError(`Failed retrieving site data for site ${siteId}: ${error.message}. ${ERROR_CALL_TO_ACTION}`);
67
+ catch (err) {
68
+ return throwUserError(`Failed retrieving site data for site ${siteId}: ${err.message}. ${ERROR_CALL_TO_ACTION}`);
57
69
  }
58
70
  };
59
71
  const getAccounts = async function (api) {
@@ -67,7 +79,27 @@ const getAccounts = async function (api) {
67
79
  return throwUserError(`Failed retrieving user account: ${error.message}. ${ERROR_CALL_TO_ACTION}`);
68
80
  }
69
81
  };
70
- export const getIntegrations = async function ({ siteId, accountId, testOpts, offline, token, featureFlags, extensionApiBaseUrl, mode, }) {
82
+ const ExtensionResponseSchema = z.array(z.object({
83
+ // ndhoule: The `author` and `extension_token` fields are not sent by the .../safe endpoint;
84
+ // we're normalizing them to empty values here to preserve...uh, whatever backward compatibility
85
+ // this is supposed to offer.
86
+ //
87
+ // At this point, I'm unsure if modern `@netlify/config` callers can end up in the .../safe
88
+ // codepath. This would be bad: extension-injected build hooks are far removed from this code
89
+ // path and have no way of knowing whether or not a specific consumer is in this legacy code
90
+ // path. They might call the Netlify API expecting to have an API token available to them when
91
+ // they really don't. For the time being, I've added instrumentation to Jigsaw to help us figure
92
+ // out if this is dead code or actually supports current users.
93
+ author: z.string().optional().default(undefined),
94
+ extension_token: z.string().optional().default(undefined),
95
+ has_build: z.boolean(),
96
+ name: z.string(),
97
+ slug: z.string(),
98
+ version: z.string(),
99
+ // Returned by API, but unused. Leaving this here for the sake of documentation.
100
+ // has_connector: z.boolean(),
101
+ }));
102
+ export const getExtensions = async function ({ siteId, accountId, testOpts, offline, token, featureFlags, extensionApiBaseUrl, mode, }) {
71
103
  if (!siteId || offline) {
72
104
  return [];
73
105
  }
@@ -76,14 +108,14 @@ export const getIntegrations = async function ({ siteId, accountId, testOpts, of
76
108
  // TODO(kh): I am adding this purely for local staging development.
77
109
  // We should remove this once we have fixed https://github.com/netlify/cli/blob/b5a5c7525edd28925c5c2e3e5f0f00c4261eaba5/src/lib/build.ts#L125
78
110
  let host = originalHost;
79
- // If there is a host, we use it to fetch the integrations
111
+ // If there is a host, we use it to fetch the extensions
80
112
  // we check if the host is staging or production and set the host accordingly,
81
113
  // sadly necessary because of https://github.com/netlify/cli/blob/b5a5c7525edd28925c5c2e3e5f0f00c4261eaba5/src/lib/build.ts#L125
82
114
  if (originalHost) {
83
- if (originalHost?.includes(NETLIFY_API_STAGING_BASE_URL)) {
115
+ if (originalHost?.includes(NETLIFY_API_STAGING_HOSTNAME)) {
84
116
  host = EXTENSION_API_STAGING_BASE_URL;
85
117
  }
86
- else if (originalHost?.includes(NETLIFY_API_BASE_URL)) {
118
+ else if (originalHost?.includes(NETLIFY_API_HOSTNAME)) {
87
119
  host = EXTENSION_API_BASE_URL;
88
120
  }
89
121
  else {
@@ -99,30 +131,21 @@ export const getIntegrations = async function ({ siteId, accountId, testOpts, of
99
131
  const url = accountId
100
132
  ? `${baseUrl}team/${accountId}/integrations/installations/meta/${siteId}`
101
133
  : `${baseUrl}site/${siteId}/integrations/safe`;
134
+ const headers = new Headers({
135
+ 'Netlify-Config-Mode': mode,
136
+ 'User-Agent': `Netlify Config (mode:${mode}) / ${ROOT_PACKAGE_JSON.version}`,
137
+ });
138
+ if (sendBuildBotTokenToJigsaw && token) {
139
+ headers.set('Netlify-SDK-Build-Bot-Token', token);
140
+ }
102
141
  try {
103
- const requestOptions = {};
104
- // This is used to identify where the request is coming from
105
- requestOptions.headers = {
106
- 'netlify-config-mode': mode,
107
- };
108
- if (sendBuildBotTokenToJigsaw && token) {
109
- requestOptions.headers = {
110
- ...requestOptions.headers,
111
- 'netlify-sdk-build-bot-token': token,
112
- };
113
- }
114
- const response = await fetch(url, requestOptions);
115
- if (!response.ok) {
116
- throw new Error(`Unexpected status code ${response.status} from fetching extensions`);
142
+ const res = await fetch(url, { headers });
143
+ if (res.status !== 200) {
144
+ throw new Error(`Unexpected status code ${res.status} from fetching extensions`);
117
145
  }
118
- const bodyText = await response.text();
119
- if (bodyText === '') {
120
- return [];
121
- }
122
- const integrations = await JSON.parse(bodyText);
123
- return Array.isArray(integrations) ? integrations : [];
146
+ return ExtensionResponseSchema.parse(await res.json());
124
147
  }
125
- catch (error) {
126
- return throwUserError(`Failed retrieving extensions for site ${siteId}: ${error.message}. ${ERROR_CALL_TO_ACTION}`);
148
+ catch (err) {
149
+ return throwUserError(`Failed retrieving extensions for site ${siteId}: ${err instanceof Error ? err.message : 'unknown error'}. ${ERROR_CALL_TO_ACTION}`);
127
150
  }
128
151
  };
@@ -3,6 +3,7 @@ export const getEnvelope = async function ({ api, accountId, siteId, context, })
3
3
  return {};
4
4
  }
5
5
  try {
6
+ // TODO(ndhoule): The api client now has types; remove this type assertion to any and fix errors
6
7
  const environmentVariables = await api.getEnvVars({ accountId, siteId, context_name: context });
7
8
  const sortedEnvVarsFromContext = environmentVariables
8
9
  .sort((left, right) => (left.key.toLowerCase() < right.key.toLowerCase() ? -1 : 1))
@@ -19,6 +20,8 @@ export const getEnvelope = async function ({ api, accountId, siteId, context, })
19
20
  return sortedEnvVarsFromContext;
20
21
  }
21
22
  catch {
23
+ // TODO(ndhoule): We should probably not quietly fail to retrieve environment variables: This
24
+ // will produce confusingly inconsistent builds.
22
25
  return {};
23
26
  }
24
27
  };
package/lib/events.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export const EVENTS: string[];
2
- export const DEV_EVENTS: string[];
1
+ export declare const EVENTS: string[];
2
+ export declare const DEV_EVENTS: string[];
@@ -0,0 +1,52 @@
1
+ import { type Extension } from './api/site_info.js';
2
+ export declare const NETLIFY_API_STAGING_HOSTNAME = "api-staging.netlify.com";
3
+ export declare const NETLIFY_API_HOSTNAME = "api.netlify.com";
4
+ export declare const EXTENSION_API_BASE_URL = "https://api.netlifysdk.com";
5
+ export declare const EXTENSION_API_STAGING_BASE_URL = "https://api-staging.netlifysdk.com";
6
+ export type MergeExtensionsOptions = {
7
+ /**
8
+ * Extensions loaded via the Netlify API. These are extensions enabled
9
+ */
10
+ apiExtensions: Extension[];
11
+ /**
12
+ * Development extensions loaded via the build target's netlify.toml file. Only used when the
13
+ * build context is set to `dev` (e.g. when `netlify build` is run with `--context=dev`).
14
+ */
15
+ configExtensions?: {
16
+ name: string;
17
+ dev?: {
18
+ path: string;
19
+ force_run_in_build?: boolean;
20
+ };
21
+ }[];
22
+ /**
23
+ * A path to the build target's build directory. We use this in dev mode to resolve non-absolute
24
+ * build plugin paths.
25
+ */
26
+ buildDir: string;
27
+ /**
28
+ * The current build context, set e.g. via `netlify build --context=<context>`.
29
+ */
30
+ context: string;
31
+ };
32
+ export type ExtensionWithDev = Extension & {
33
+ buildPlugin: {
34
+ origin: 'local' | 'remote';
35
+ packageURL: URL;
36
+ } | null;
37
+ dev?: {
38
+ path: string;
39
+ force_run_in_build?: boolean;
40
+ } | null;
41
+ };
42
+ /**
43
+ * normalizeAndMergeExtensions accepts several lists of extensions configured for the current build
44
+ * target, normalizes them to compensate for some differences between the various APIs we load this
45
+ * data from (one of two API endpoints and the user's config file), and merges them into a single
46
+ * list.
47
+ *
48
+ * Note that it merges extension data provided by the config file (configExtensions) only when
49
+ * context=dev. When it does so, config file data will be merged into any available API data, giving
50
+ * a preference to config file data.
51
+ */
52
+ export declare const normalizeAndMergeExtensions: ({ apiExtensions, buildDir, configExtensions, context, }: MergeExtensionsOptions) => ExtensionWithDev[];
@@ -0,0 +1,91 @@
1
+ import path from 'node:path';
2
+ import { throwUserError } from './error.js';
3
+ export const NETLIFY_API_STAGING_HOSTNAME = 'api-staging.netlify.com';
4
+ export const NETLIFY_API_HOSTNAME = 'api.netlify.com';
5
+ export const EXTENSION_API_BASE_URL = 'https://api.netlifysdk.com';
6
+ export const EXTENSION_API_STAGING_BASE_URL = 'https://api-staging.netlifysdk.com';
7
+ /**
8
+ * normalizeAndMergeExtensions accepts several lists of extensions configured for the current build
9
+ * target, normalizes them to compensate for some differences between the various APIs we load this
10
+ * data from (one of two API endpoints and the user's config file), and merges them into a single
11
+ * list.
12
+ *
13
+ * Note that it merges extension data provided by the config file (configExtensions) only when
14
+ * context=dev. When it does so, config file data will be merged into any available API data, giving
15
+ * a preference to config file data.
16
+ */
17
+ export const normalizeAndMergeExtensions = ({ apiExtensions, buildDir, configExtensions = [], context, }) => {
18
+ const apiExtensionsBySlug = new Map(apiExtensions.map((extension) => [
19
+ extension.slug,
20
+ {
21
+ ...extension,
22
+ buildPlugin: extension.has_build
23
+ ? { origin: 'remote', packageURL: new URL('/packages/buildhooks.tgz', extension.version) }
24
+ : null,
25
+ dev: null,
26
+ },
27
+ ]));
28
+ const configExtensionsBySlug = new Map(
29
+ // Only use configuration-file data in development mode
30
+ (context === 'dev' ? configExtensions : []).map((extension) => {
31
+ let buildPluginPackageURL = null;
32
+ if (extension.dev?.path) {
33
+ let resolvedPath = path.isAbsolute(extension.dev.path)
34
+ ? extension.dev.path
35
+ : path.resolve(buildDir, extension.dev.path);
36
+ const normalizedExtname = path.extname(resolvedPath).toLowerCase();
37
+ // If the user has specified a path to an extension directory rather than to a tarball
38
+ // package, interpret this as a shortcut for "the default Netlify Extension build plugin
39
+ // artifact path, please."
40
+ //
41
+ // This sort of emulates SDK v1/2/3 behavior, and is an effort at making extension dev mode
42
+ // friendlier to use. We can feel free to revisit this chocie at a later date.
43
+ if (normalizedExtname === '') {
44
+ resolvedPath = path.join(resolvedPath, '.ntli/site/static/packages/buildhooks.tgz');
45
+ }
46
+ buildPluginPackageURL = new URL(`file://${resolvedPath}`);
47
+ }
48
+ return [
49
+ extension.name,
50
+ // Normalize dev extensions to a similar shape as an API extension
51
+ {
52
+ author: undefined,
53
+ dev: extension.dev,
54
+ extension_token: undefined,
55
+ has_build: buildPluginPackageURL !== null,
56
+ name: extension.name,
57
+ slug: extension.name,
58
+ version: undefined,
59
+ buildPlugin: buildPluginPackageURL !== null ? { origin: 'local', packageURL: buildPluginPackageURL } : null,
60
+ },
61
+ ];
62
+ }));
63
+ // Merge API and configuration file metadata together by merging development metadata onto API
64
+ // metadata.
65
+ //
66
+ // Explicitly allow the configuration file to reference an extension that doesn't yet exist in the
67
+ // API so users can test their build hooks without publishing the extension first.
68
+ const mergedExtensions = [...new Set([...apiExtensionsBySlug.keys(), ...configExtensionsBySlug.keys()])]
69
+ .map((slug) => [apiExtensionsBySlug.get(slug), configExtensionsBySlug.get(slug)])
70
+ .map(([apiExtension, configExtension]) => {
71
+ return {
72
+ author: configExtension?.author ?? apiExtension?.author ?? '',
73
+ buildPlugin: configExtension?.buildPlugin ?? apiExtension?.buildPlugin ?? null,
74
+ dev: configExtension?.dev,
75
+ extension_token: configExtension?.extension_token ?? apiExtension?.extension_token ?? '',
76
+ has_build: configExtension?.has_build ?? apiExtension?.has_build ?? false,
77
+ name: configExtension?.name ?? apiExtension?.name ?? '',
78
+ slug: configExtension?.slug ?? apiExtension?.slug ?? '',
79
+ version: configExtension?.version ?? apiExtension?.version ?? '',
80
+ };
81
+ });
82
+ for (const extension of mergedExtensions) {
83
+ if (extension.buildPlugin !== null) {
84
+ const normalizedExtname = path.extname(extension.buildPlugin.packageURL.toString()).toLowerCase();
85
+ if (normalizedExtname !== '.tgz') {
86
+ throwUserError(`Extension ${extension.slug} contains unexpected build plugin URL: '${extension.buildPlugin.packageURL.toString()}'. Build plugin URLs must end in '.tgz'.`);
87
+ }
88
+ }
89
+ }
90
+ return mergedExtensions;
91
+ };
package/lib/log/logger.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import figures from 'figures';
2
- import { serializeObject } from '../../lib/log/serialize.js';
2
+ // FIXME: This error will go away once this file is converted to TypeScript.
3
+ // eslint-disable-next-line n/no-missing-import
4
+ import { serializeObject } from './serialize.js';
3
5
  import { THEME } from './theme.js';
4
6
  export const logsAreBuffered = (logs) => {
5
7
  return logs !== undefined && 'stdout' in logs;
package/lib/main.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type MinimalAccount } from './api/site_info.js';
2
+ import { type ExtensionWithDev } from './extensions.js';
2
3
  export type Config = {
3
4
  accounts: MinimalAccount[] | undefined;
4
5
  api: any;
@@ -9,7 +10,7 @@ export type Config = {
9
10
  context: any;
10
11
  env: any;
11
12
  headersPath: any;
12
- integrations: any;
13
+ integrations: ExtensionWithDev[];
13
14
  logs: any;
14
15
  redirectsPath: any;
15
16
  repositoryRoot: any;
package/lib/main.js CHANGED
@@ -9,7 +9,7 @@ import { getEnv } from './env/main.js';
9
9
  import { resolveConfigPaths } from './files.js';
10
10
  import { getHeadersPath, addHeaders } from './headers.js';
11
11
  import { getInlineConfig } from './inline_config.js';
12
- import { EXTENSION_API_BASE_URL, EXTENSION_API_STAGING_BASE_URL, mergeIntegrations, NETLIFY_API_STAGING_BASE_URL, } from './integrations.js';
12
+ import { EXTENSION_API_BASE_URL, EXTENSION_API_STAGING_BASE_URL, NETLIFY_API_STAGING_HOSTNAME, normalizeAndMergeExtensions, } from './extensions.js';
13
13
  import { logResult } from './log/main.js';
14
14
  import { mergeConfigs } from './merge.js';
15
15
  import { normalizeBeforeConfigMerge, normalizeAfterConfigMerge } from './merge_normalize.js';
@@ -36,16 +36,16 @@ export const resolveConfig = async function (opts) {
36
36
  return parsedCachedConfig;
37
37
  }
38
38
  // TODO(kh): remove this mapping and get the extensionApiHost from the opts
39
- const extensionApiBaseUrl = host?.includes(NETLIFY_API_STAGING_BASE_URL)
39
+ const extensionApiBaseUrl = host?.includes(NETLIFY_API_STAGING_HOSTNAME)
40
40
  ? EXTENSION_API_STAGING_BASE_URL
41
41
  : EXTENSION_API_BASE_URL;
42
42
  const { config: configOpt, defaultConfig, inlineConfig, configMutations, cwd, context, repositoryRoot, base, branch, siteId, accountId, deployId, buildId, baseRelDir, mode, debug, logs, featureFlags, } = await normalizeOpts(optsA);
43
- let { siteInfo, accounts, integrations } = parsedCachedConfig || {};
43
+ let { siteInfo, accounts, integrations: extensions } = parsedCachedConfig || {};
44
44
  // If we have cached site info, we don't need to fetch it again
45
- const useCachedSiteInfo = Boolean(featureFlags?.use_cached_site_info && siteInfo && accounts && integrations);
45
+ const useCachedSiteInfo = Boolean(featureFlags?.use_cached_site_info && siteInfo && accounts && extensions);
46
46
  // I'm adding some debug logging to see if the logic is working as expected
47
47
  if (featureFlags?.use_cached_site_info_logging) {
48
- console.log('Checking site information', { useCachedSiteInfo, siteInfo, accounts, integrations });
48
+ console.log('Checking site information', { useCachedSiteInfo, siteInfo, accounts, extensions });
49
49
  }
50
50
  if (!useCachedSiteInfo) {
51
51
  const updatedSiteInfo = await getSiteInfo({
@@ -63,7 +63,7 @@ export const resolveConfig = async function (opts) {
63
63
  });
64
64
  siteInfo = updatedSiteInfo.siteInfo;
65
65
  accounts = updatedSiteInfo.accounts;
66
- integrations = updatedSiteInfo.integrations;
66
+ extensions = updatedSiteInfo.extensions;
67
67
  }
68
68
  const { defaultConfig: defaultConfigA, baseRelDir: baseRelDirA } = parseDefaultConfig({
69
69
  defaultConfig,
@@ -102,9 +102,9 @@ export const resolveConfig = async function (opts) {
102
102
  });
103
103
  // @todo Remove in the next major version.
104
104
  const configA = addLegacyFunctionsDirectory(config);
105
- const updatedIntegrations = await handleAutoInstallExtensions({
105
+ const updatedExtensions = await handleAutoInstallExtensions({
106
106
  featureFlags,
107
- integrations,
107
+ extensions,
108
108
  siteId,
109
109
  accountId,
110
110
  token,
@@ -115,14 +115,15 @@ export const resolveConfig = async function (opts) {
115
115
  mode,
116
116
  debug,
117
117
  });
118
- const mergedIntegrations = await mergeIntegrations({
119
- apiIntegrations: updatedIntegrations,
120
- configIntegrations: configA.integrations,
121
- context: context,
118
+ const mergedExtensions = normalizeAndMergeExtensions({
119
+ apiExtensions: updatedExtensions,
120
+ configExtensions: configA.integrations,
121
+ buildDir,
122
+ context,
122
123
  });
123
124
  const result = {
124
125
  siteInfo,
125
- integrations: mergedIntegrations,
126
+ integrations: mergedExtensions,
126
127
  accounts,
127
128
  env,
128
129
  configPath,
@@ -1,4 +1,4 @@
1
- import { type IntegrationResponse } from '../../types/api.js';
1
+ import { type Extension } from '../../api/site_info.js';
2
2
  import { type ModeOption } from '../../types/options.js';
3
3
  interface AutoInstallOptions {
4
4
  featureFlags: any;
@@ -6,12 +6,12 @@ interface AutoInstallOptions {
6
6
  accountId: string;
7
7
  token: string;
8
8
  buildDir: string;
9
- integrations: IntegrationResponse[];
9
+ extensions: Extension[];
10
10
  offline: boolean;
11
11
  testOpts: any;
12
12
  mode: ModeOption;
13
13
  extensionApiBaseUrl: string;
14
14
  debug?: boolean;
15
15
  }
16
- export declare function handleAutoInstallExtensions({ featureFlags, siteId, accountId, token, buildDir, integrations, offline, testOpts, mode, extensionApiBaseUrl, debug, }: AutoInstallOptions): Promise<IntegrationResponse[]>;
16
+ export declare function handleAutoInstallExtensions({ featureFlags, siteId, accountId, token, buildDir, extensions, offline, testOpts, mode, extensionApiBaseUrl, debug, }: AutoInstallOptions): Promise<Extension[]>;
17
17
  export {};
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from 'module';
2
2
  import { join } from 'path';
3
- import { getIntegrations } from '../../api/site_info.js';
3
+ import { getExtensions } from '../../api/site_info.js';
4
4
  import { fetchAutoInstallableExtensionsMeta, installExtension } from './utils.js';
5
5
  function getPackageJSON(directory) {
6
6
  try {
@@ -12,9 +12,9 @@ function getPackageJSON(directory) {
12
12
  return {};
13
13
  }
14
14
  }
15
- export async function handleAutoInstallExtensions({ featureFlags, siteId, accountId, token, buildDir, integrations, offline, testOpts = {}, mode, extensionApiBaseUrl, debug = false, }) {
15
+ export async function handleAutoInstallExtensions({ featureFlags, siteId, accountId, token, buildDir, extensions, offline, testOpts = {}, mode, extensionApiBaseUrl, debug = false, }) {
16
16
  if (!featureFlags?.auto_install_required_extensions_v2) {
17
- return integrations;
17
+ return extensions;
18
18
  }
19
19
  if (!accountId || !siteId || !token || !buildDir || offline) {
20
20
  const reason = !accountId
@@ -35,17 +35,17 @@ export async function handleAutoInstallExtensions({ featureFlags, siteId, accoun
35
35
  mode,
36
36
  });
37
37
  }
38
- return integrations;
38
+ return extensions;
39
39
  }
40
40
  try {
41
41
  const packageJson = getPackageJSON(buildDir);
42
42
  if (!packageJson?.dependencies ||
43
43
  typeof packageJson?.dependencies !== 'object' ||
44
44
  Object.keys(packageJson?.dependencies)?.length === 0) {
45
- return integrations;
45
+ return extensions;
46
46
  }
47
47
  const autoInstallableExtensions = await fetchAutoInstallableExtensionsMeta();
48
- const enabledExtensionSlugs = new Set((integrations ?? []).map(({ slug }) => slug));
48
+ const enabledExtensionSlugs = new Set((extensions ?? []).map(({ slug }) => slug));
49
49
  const extensionsToInstallCandidates = autoInstallableExtensions.filter(({ slug }) => !enabledExtensionSlugs.has(slug));
50
50
  const extensionsToInstall = extensionsToInstallCandidates.filter(({ packages }) => {
51
51
  for (const pkg of packages) {
@@ -56,7 +56,7 @@ export async function handleAutoInstallExtensions({ featureFlags, siteId, accoun
56
56
  return false;
57
57
  });
58
58
  if (extensionsToInstall.length === 0) {
59
- return integrations;
59
+ return extensions;
60
60
  }
61
61
  const results = await Promise.all(extensionsToInstall.map(async (ext) => {
62
62
  console.log(`Installing extension "${ext.slug}" on team "${accountId}" required by package(s): "${ext.packages.join('",')}"`);
@@ -69,7 +69,7 @@ export async function handleAutoInstallExtensions({ featureFlags, siteId, accoun
69
69
  });
70
70
  }));
71
71
  if (results.length > 0 && results.some((result) => !result.error)) {
72
- return getIntegrations({
72
+ return getExtensions({
73
73
  siteId,
74
74
  accountId,
75
75
  testOpts,
@@ -80,10 +80,10 @@ export async function handleAutoInstallExtensions({ featureFlags, siteId, accoun
80
80
  mode,
81
81
  });
82
82
  }
83
- return integrations;
83
+ return extensions;
84
84
  }
85
85
  catch (error) {
86
86
  console.error(`Failed to auto install extension(s): ${error.message}`, error);
87
- return integrations;
87
+ return extensions;
88
88
  }
89
89
  }
@@ -1,4 +1,4 @@
1
- import { EXTENSION_API_BASE_URL } from '../../integrations.js';
1
+ import { EXTENSION_API_BASE_URL } from '../../extensions.js';
2
2
  import { ROOT_PACKAGE_JSON } from '../json.js';
3
3
  export const installExtension = async ({ netlifyToken, accountId, slug, hostSiteUrl, extensionInstallationSource, }) => {
4
4
  const userAgent = `Netlify Config (mode:${extensionInstallationSource}) / ${ROOT_PACKAGE_JSON.version}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/config",
3
- "version": "23.2.0",
3
+ "version": "24.0.1",
4
4
  "description": "Netlify config module",
5
5
  "type": "module",
6
6
  "exports": "./lib/index.js",
@@ -61,7 +61,7 @@
61
61
  "@iarna/toml": "^2.2.5",
62
62
  "@netlify/api": "^14.0.3",
63
63
  "@netlify/headers-parser": "^9.0.1",
64
- "@netlify/redirect-parser": "^15.0.2",
64
+ "@netlify/redirect-parser": "^15.0.3",
65
65
  "chalk": "^5.0.0",
66
66
  "cron-parser": "^4.1.0",
67
67
  "deepmerge": "^4.2.2",
@@ -81,7 +81,8 @@
81
81
  "tomlify-j0.4": "^3.0.0",
82
82
  "validate-npm-package-name": "^5.0.0",
83
83
  "yaml": "^2.8.0",
84
- "yargs": "^17.6.0"
84
+ "yargs": "^17.6.0",
85
+ "zod": "^4.0.5"
85
86
  },
86
87
  "devDependencies": {
87
88
  "@types/node": "^18.19.111",
@@ -95,5 +96,5 @@
95
96
  "engines": {
96
97
  "node": ">=18.14.0"
97
98
  },
98
- "gitHead": "243d65788aca4867c78403ac15c267f76feb6c00"
99
+ "gitHead": "f65a08178a04db0ad274aa62f7d46319f2ef661a"
99
100
  }
@@ -1,19 +0,0 @@
1
- import { IntegrationResponse } from './types/api.js';
2
- import { Integration } from './types/integrations.js';
3
- export declare const NETLIFY_API_STAGING_BASE_URL = "api-staging.netlify.com";
4
- export declare const NETLIFY_API_BASE_URL = "api.netlify.com";
5
- export declare const EXTENSION_API_BASE_URL = "https://api.netlifysdk.com";
6
- export declare const EXTENSION_API_STAGING_BASE_URL = "https://api-staging.netlifysdk.com";
7
- type MergeIntegrationsOpts = {
8
- configIntegrations?: {
9
- name: string;
10
- dev?: {
11
- path: string;
12
- force_run_in_build?: boolean;
13
- };
14
- }[];
15
- apiIntegrations: IntegrationResponse[];
16
- context: string;
17
- };
18
- export declare const mergeIntegrations: ({ configIntegrations, apiIntegrations, context, }: MergeIntegrationsOpts) => Promise<Integration[]>;
19
- export {};
@@ -1,33 +0,0 @@
1
- export const NETLIFY_API_STAGING_BASE_URL = 'api-staging.netlify.com';
2
- export const NETLIFY_API_BASE_URL = 'api.netlify.com';
3
- export const EXTENSION_API_BASE_URL = 'https://api.netlifysdk.com';
4
- export const EXTENSION_API_STAGING_BASE_URL = 'https://api-staging.netlifysdk.com';
5
- export const mergeIntegrations = async function ({ configIntegrations = [], apiIntegrations, context, }) {
6
- // Include all API integrations, unless they have a `dev` property and we are in the `dev` context
7
- const resolvedApiIntegrations = apiIntegrations.filter((integration) => !configIntegrations.some((configIntegration) => configIntegration.name === integration.slug &&
8
- typeof configIntegration.dev !== 'undefined' &&
9
- context === 'dev'));
10
- // For integrations loaded from the TOML, we will use the local reference in the `dev` context,
11
- // otherwise we will fetch from the API and match the slug
12
- const resolvedConfigIntegrations = configIntegrations
13
- .filter((configIntegration) => apiIntegrations.every((apiIntegration) => apiIntegration.slug !== configIntegration.name) ||
14
- ('dev' in configIntegration && context === 'dev'))
15
- .map((configIntegration) => {
16
- const apiIntegration = apiIntegrations.find((apiIntegration) => apiIntegration.slug === configIntegration.name);
17
- if (configIntegration.dev && context === 'dev') {
18
- return {
19
- slug: configIntegration.name,
20
- dev: configIntegration.dev,
21
- // TODO(kh): has_build should become irrelevant soon as we are only returning extensions that have a build event handler.
22
- has_build: apiIntegration?.has_build ?? configIntegration.dev?.force_run_in_build ?? false,
23
- ...apiIntegration,
24
- };
25
- }
26
- if (!apiIntegration) {
27
- return undefined;
28
- }
29
- return apiIntegration;
30
- })
31
- .filter((i) => typeof i !== 'undefined');
32
- return [...resolvedApiIntegrations, ...resolvedConfigIntegrations];
33
- };
@@ -1,5 +0,0 @@
1
- export type IntegrationResponse = {
2
- slug: string;
3
- version: string;
4
- has_build: boolean;
5
- };
package/lib/types/api.js DELETED
@@ -1 +0,0 @@
1
- export {};
@@ -1,9 +0,0 @@
1
- export type Integration = {
2
- slug: string;
3
- version?: string;
4
- has_build?: boolean;
5
- dev?: {
6
- path: string;
7
- };
8
- author?: string;
9
- };
@@ -1 +0,0 @@
1
- export {};