@netlify/config 23.2.0 → 24.0.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.
- package/lib/api/site_info.d.ts +30 -9
- package/lib/api/site_info.js +63 -40
- package/lib/env/envelope.js +3 -0
- package/lib/events.d.ts +2 -2
- package/lib/extensions.d.ts +52 -0
- package/lib/extensions.js +91 -0
- package/lib/log/logger.js +3 -1
- package/lib/main.d.ts +2 -1
- package/lib/main.js +14 -13
- package/lib/utils/extensions/auto-install-extensions.d.ts +3 -3
- package/lib/utils/extensions/auto-install-extensions.js +10 -10
- package/lib/utils/extensions/utils.js +1 -1
- package/package.json +4 -3
- package/lib/integrations.d.ts +0 -19
- package/lib/integrations.js +0 -33
- package/lib/types/api.d.ts +0 -5
- package/lib/types/api.js +0 -1
- package/lib/types/integrations.d.ts +0 -9
- package/lib/types/integrations.js +0 -1
package/lib/api/site_info.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NetlifyAPI } from '@netlify/api';
|
|
2
|
-
import
|
|
2
|
+
import * as z from 'zod';
|
|
3
3
|
import { ModeOption, TestOptions } from '../types/options.js';
|
|
4
|
-
type
|
|
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, }:
|
|
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
|
-
|
|
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
|
|
74
|
+
export declare const getExtensions: ({ siteId, accountId, testOpts, offline, token, featureFlags, extensionApiBaseUrl, mode, }: GetExtensionsOptions) => Promise<Extension[]>;
|
|
54
75
|
export {};
|
package/lib/api/site_info.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
22
|
+
}
|
|
23
|
+
if (accountId !== undefined) {
|
|
21
24
|
siteInfo.account_id = accountId;
|
|
22
|
-
|
|
23
|
-
|
|
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 {
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return { accounts: [], extensions, siteInfo };
|
|
35
40
|
}
|
|
36
|
-
const [siteInfo, accounts,
|
|
41
|
+
const [siteInfo, accounts, extensions] = await Promise.all([
|
|
37
42
|
getSite(api, siteId, siteFeatureFlagPrefix),
|
|
38
43
|
getAccounts(api),
|
|
39
|
-
|
|
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,
|
|
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({
|
|
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 (
|
|
56
|
-
throwUserError(`Failed retrieving site data for site ${siteId}: ${
|
|
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
|
-
|
|
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
|
|
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(
|
|
115
|
+
if (originalHost?.includes(NETLIFY_API_STAGING_HOSTNAME)) {
|
|
84
116
|
host = EXTENSION_API_STAGING_BASE_URL;
|
|
85
117
|
}
|
|
86
|
-
else if (originalHost?.includes(
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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 (
|
|
126
|
-
return throwUserError(`Failed retrieving extensions for site ${siteId}: ${
|
|
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
|
};
|
package/lib/env/envelope.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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,
|
|
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(
|
|
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 &&
|
|
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,
|
|
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
|
-
|
|
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
|
|
105
|
+
const updatedExtensions = await handleAutoInstallExtensions({
|
|
106
106
|
featureFlags,
|
|
107
|
-
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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:
|
|
126
|
+
integrations: mergedExtensions,
|
|
126
127
|
accounts,
|
|
127
128
|
env,
|
|
128
129
|
configPath,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
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
|
-
|
|
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,
|
|
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 {
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
45
|
+
return extensions;
|
|
46
46
|
}
|
|
47
47
|
const autoInstallableExtensions = await fetchAutoInstallableExtensionsMeta();
|
|
48
|
-
const enabledExtensionSlugs = new Set((
|
|
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
|
|
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
|
|
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
|
|
83
|
+
return extensions;
|
|
84
84
|
}
|
|
85
85
|
catch (error) {
|
|
86
86
|
console.error(`Failed to auto install extension(s): ${error.message}`, error);
|
|
87
|
-
return
|
|
87
|
+
return extensions;
|
|
88
88
|
}
|
|
89
89
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EXTENSION_API_BASE_URL } from '../../
|
|
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": "
|
|
3
|
+
"version": "24.0.0",
|
|
4
4
|
"description": "Netlify config module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": "./lib/index.js",
|
|
@@ -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": "
|
|
99
|
+
"gitHead": "5100d463cb9b606b91a6a9b5c3fbe9c143716701"
|
|
99
100
|
}
|
package/lib/integrations.d.ts
DELETED
|
@@ -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 {};
|
package/lib/integrations.js
DELETED
|
@@ -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
|
-
};
|
package/lib/types/api.d.ts
DELETED
package/lib/types/api.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|