@netlify/config 22.1.0 → 22.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,4 +40,15 @@ export type MinimalAccount = {
40
40
  type_slug: string;
41
41
  members_count: number;
42
42
  };
43
+ type GetIntegrationsOpts = {
44
+ siteId?: string;
45
+ accountId?: string;
46
+ testOpts: TestOptions;
47
+ offline: boolean;
48
+ token?: string;
49
+ featureFlags?: Record<string, boolean>;
50
+ extensionApiBaseUrl: string;
51
+ mode: ModeOption;
52
+ };
53
+ export declare const getIntegrations: ({ siteId, accountId, testOpts, offline, token, featureFlags, extensionApiBaseUrl, mode, }: GetIntegrationsOpts) => Promise<IntegrationResponse[]>;
43
54
  export {};
@@ -68,7 +68,7 @@ const getAccounts = async function (api) {
68
68
  return throwUserError(`Failed retrieving user account: ${error.message}. ${ERROR_CALL_TO_ACTION}`);
69
69
  }
70
70
  };
71
- const getIntegrations = async function ({ siteId, accountId, testOpts, offline, token, featureFlags, extensionApiBaseUrl, mode, }) {
71
+ export const getIntegrations = async function ({ siteId, accountId, testOpts, offline, token, featureFlags, extensionApiBaseUrl, mode, }) {
72
72
  if (!siteId || offline) {
73
73
  return [];
74
74
  }
package/lib/main.js CHANGED
@@ -18,6 +18,7 @@ import { UI_ORIGIN, CONFIG_ORIGIN, INLINE_ORIGIN } from './origin.js';
18
18
  import { parseConfig } from './parse.js';
19
19
  import { getConfigPath } from './path.js';
20
20
  import { getRedirectsPath, addRedirects } from './redirects.js';
21
+ import { handleAutoInstallExtensions } from './utils/extensions/auto-install-extensions.js';
21
22
  /**
22
23
  * Load the configuration file.
23
24
  * Takes an optional configuration file path as input and return the resolved
@@ -101,8 +102,21 @@ export const resolveConfig = async function (opts) {
101
102
  });
102
103
  // @todo Remove in the next major version.
103
104
  const configA = addLegacyFunctionsDirectory(config);
105
+ const updatedIntegrations = await handleAutoInstallExtensions({
106
+ featureFlags,
107
+ accounts,
108
+ integrations,
109
+ siteId,
110
+ accountId,
111
+ token,
112
+ cwd,
113
+ extensionApiBaseUrl,
114
+ testOpts,
115
+ offline,
116
+ mode,
117
+ });
104
118
  const mergedIntegrations = await mergeIntegrations({
105
- apiIntegrations: integrations,
119
+ apiIntegrations: updatedIntegrations,
106
120
  configIntegrations: configA.integrations,
107
121
  context: context,
108
122
  });
@@ -0,0 +1,17 @@
1
+ import { type IntegrationResponse } from '../../types/api.js';
2
+ import { type ModeOption } from '../../types/options.js';
3
+ interface AutoInstallOptions {
4
+ featureFlags: any;
5
+ siteId: string;
6
+ accountId: string;
7
+ token: string;
8
+ cwd: string;
9
+ accounts: any;
10
+ integrations: IntegrationResponse[];
11
+ offline: boolean;
12
+ testOpts: any;
13
+ mode: ModeOption;
14
+ extensionApiBaseUrl: string;
15
+ }
16
+ export declare function handleAutoInstallExtensions({ featureFlags, siteId, accountId, token, cwd, accounts, integrations, offline, testOpts, mode, extensionApiBaseUrl, }: AutoInstallOptions): Promise<IntegrationResponse[]>;
17
+ export {};
@@ -0,0 +1,58 @@
1
+ import { createRequire } from 'module';
2
+ import { join } from 'path';
3
+ import { getIntegrations } from '../../api/site_info.js';
4
+ import { fetchAutoInstallableExtensionsMeta, installExtension } from './utils.js';
5
+ function getPackageJSON(directory) {
6
+ const require = createRequire(join(directory, 'package.json'));
7
+ return require('./package.json');
8
+ }
9
+ export async function handleAutoInstallExtensions({ featureFlags, siteId, accountId, token, cwd, accounts, integrations, offline, testOpts = {}, mode, extensionApiBaseUrl, }) {
10
+ if (!featureFlags?.auto_install_required_extensions || !accountId || !siteId || !token || !cwd || offline) {
11
+ return integrations;
12
+ }
13
+ const account = accounts?.find((account) => account.id === accountId);
14
+ if (!account) {
15
+ return integrations;
16
+ }
17
+ try {
18
+ const packageJson = getPackageJSON(cwd);
19
+ if (!packageJson?.dependencies ||
20
+ typeof packageJson?.dependencies !== 'object' ||
21
+ Object.keys(packageJson?.dependencies)?.length === 0) {
22
+ return integrations;
23
+ }
24
+ const autoInstallableExtensions = await fetchAutoInstallableExtensionsMeta();
25
+ const extensionsToInstall = autoInstallableExtensions.filter((ext) => {
26
+ return !integrations?.some((integration) => integration.slug === ext.slug);
27
+ });
28
+ if (extensionsToInstall.length === 0) {
29
+ return integrations;
30
+ }
31
+ const results = await Promise.all(extensionsToInstall.map(async (ext) => {
32
+ console.log(`Installing extension "${ext.slug}" on team "${account.name}" required by package(s): "${ext.packages.join('",')}"`);
33
+ return installExtension({
34
+ accountId,
35
+ netlifyToken: token,
36
+ slug: ext.slug,
37
+ hostSiteUrl: ext.hostSiteUrl,
38
+ });
39
+ }));
40
+ if (results.length > 0 && results.some((result) => !result.error)) {
41
+ return getIntegrations({
42
+ siteId,
43
+ accountId,
44
+ testOpts,
45
+ offline,
46
+ token,
47
+ featureFlags,
48
+ extensionApiBaseUrl,
49
+ mode,
50
+ });
51
+ }
52
+ return integrations;
53
+ }
54
+ catch (error) {
55
+ console.error(`Failed to auto install extension(s): ${error.message}`, error);
56
+ return integrations;
57
+ }
58
+ }
@@ -0,0 +1,31 @@
1
+ export type InstallExtensionResult = {
2
+ slug: string;
3
+ error: null;
4
+ } | {
5
+ slug: string;
6
+ error: {
7
+ code: string;
8
+ message: string;
9
+ };
10
+ };
11
+ export declare const installExtension: ({ netlifyToken, accountId, slug, hostSiteUrl, }: {
12
+ netlifyToken: string;
13
+ accountId: string;
14
+ slug: string;
15
+ hostSiteUrl: string;
16
+ }) => Promise<InstallExtensionResult>;
17
+ type AutoInstallableExtensionMeta = {
18
+ slug: string;
19
+ hostSiteUrl: string;
20
+ packages: string[];
21
+ };
22
+ /**
23
+ * Fetches the list of extensions from Jigsaw that declare associated packages.
24
+ * Used to determine which extensions should be auto-installed based on the packages
25
+ * present in the package.json (e.g., if an extension lists '@netlify/neon',
26
+ * and that package exists in package.json, the extension will be auto-installed).
27
+ *
28
+ * @returns Array of extensions with their associated packages
29
+ */
30
+ export declare function fetchAutoInstallableExtensionsMeta(): Promise<AutoInstallableExtensionMeta[]>;
31
+ export {};
@@ -0,0 +1,44 @@
1
+ import { EXTENSION_API_BASE_URL } from '../../integrations.js';
2
+ export const installExtension = async ({ netlifyToken, accountId, slug, hostSiteUrl, }) => {
3
+ const extensionOnInstallUrl = new URL('/.netlify/functions/handler/on-install', hostSiteUrl);
4
+ const installedResponse = await fetch(extensionOnInstallUrl, {
5
+ method: 'POST',
6
+ body: JSON.stringify({
7
+ teamId: accountId,
8
+ }),
9
+ headers: {
10
+ 'netlify-token': netlifyToken,
11
+ },
12
+ });
13
+ if (!installedResponse.ok && installedResponse.status !== 409) {
14
+ const text = await installedResponse.text();
15
+ return {
16
+ slug,
17
+ error: {
18
+ code: installedResponse.status.toString(),
19
+ message: text,
20
+ },
21
+ };
22
+ }
23
+ return {
24
+ slug,
25
+ error: null,
26
+ };
27
+ };
28
+ /**
29
+ * Fetches the list of extensions from Jigsaw that declare associated packages.
30
+ * Used to determine which extensions should be auto-installed based on the packages
31
+ * present in the package.json (e.g., if an extension lists '@netlify/neon',
32
+ * and that package exists in package.json, the extension will be auto-installed).
33
+ *
34
+ * @returns Array of extensions with their associated packages
35
+ */
36
+ export async function fetchAutoInstallableExtensionsMeta() {
37
+ const url = new URL(`/meta/auto-installable`, process.env.EXTENSION_API_BASE_URL ?? EXTENSION_API_BASE_URL);
38
+ const response = await fetch(url.toString());
39
+ if (!response.ok) {
40
+ throw new Error(`Failed to fetch extensions meta`);
41
+ }
42
+ const data = await response.json();
43
+ return data;
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/config",
3
- "version": "22.1.0",
3
+ "version": "22.2.0",
4
4
  "description": "Netlify config module",
5
5
  "type": "module",
6
6
  "exports": "./lib/index.js",
@@ -95,5 +95,5 @@
95
95
  "engines": {
96
96
  "node": "^14.16.0 || >=16.0.0"
97
97
  },
98
- "gitHead": "82cf0f22248692da58acd77d8b7ba0e10ddcd1fe"
98
+ "gitHead": "03eae326c598d4d31b28fe8f20038c5d9508f772"
99
99
  }