@netlify/config
Version:
Netlify config module
152 lines (151 loc) • 6.99 kB
JavaScript
import * as z from 'zod';
import { getEnvelope } from '../env/envelope.js';
import { throwUserError } from '../error.js';
import { EXTENSION_API_BASE_URL, EXTENSION_API_STAGING_BASE_URL, NETLIFY_API_HOSTNAME, NETLIFY_API_STAGING_HOSTNAME, } from '../extensions.js';
import { ERROR_CALL_TO_ACTION } from '../log/messages.js';
import { ROOT_PACKAGE_JSON } from '../utils/json.js';
/**
* Retrieve Netlify Site information, if available.
* Used to retrieve local build environment variables and UI build settings.
* This is not used in production builds since the buildbot passes this
* information instead.
* Requires knowing the `siteId` and having the access `token`.
* Silently ignore API errors. For example the network connection might be down,
* but local builds should still work regardless.
*/
export const getSiteInfo = async function ({ api, siteId, accountId, mode, context, offline = false, testOpts = {}, siteFeatureFlagPrefix, token, featureFlags = {}, extensionApiBaseUrl, }) {
const { env: testEnv = false } = testOpts;
if (api === undefined || mode === 'buildbot' || testEnv) {
const siteInfo = {};
if (siteId !== undefined) {
siteInfo.id = siteId;
}
if (accountId !== undefined) {
siteInfo.account_id = accountId;
}
let extensions = [];
if (mode === 'buildbot' && !offline) {
extensions = await getExtensions({
siteId,
testOpts,
offline,
accountId,
token,
featureFlags,
extensionApiBaseUrl,
mode,
});
}
return { accounts: [], extensions, siteInfo };
}
const [siteInfo, accounts, extensions] = await Promise.all([
getSite(api, siteId, siteFeatureFlagPrefix),
getAccounts(api),
getExtensions({ siteId, testOpts, offline, accountId, token, featureFlags, extensionApiBaseUrl, mode }),
]);
// TODO(ndhoule): Investigate, but at this point, I'm fairly sure this is the default for all
// sites. If so, we can remove this conditional and always query for environment variables.
if (siteInfo.use_envelope) {
const envelope = await getEnvelope({ api, accountId: siteInfo.account_slug, siteId, context });
siteInfo.build_settings.env = envelope;
}
return { siteInfo, accounts, extensions };
};
const getSite = async function (api, siteId, siteFeatureFlagPrefix) {
if (siteId === undefined) {
return {};
}
try {
const site = await api.getSite({
// @ts-expect-error: Internal parameter that instructs the API to include all the site's
// feature flags in the response.
feature_flags: siteFeatureFlagPrefix,
siteId,
});
return { ...site, id: siteId };
}
catch (err) {
return throwUserError(`Failed retrieving site data for site ${siteId}: ${err.message}. ${ERROR_CALL_TO_ACTION}`);
}
};
const getAccounts = async function (api) {
try {
const accounts = (await api.listAccountsForUser(
// @ts-expect-error(ndhoule): This is an unpublished, internal querystring parameter
{ minimal: 'true' }));
return Array.isArray(accounts) ? accounts : [];
}
catch (error) {
return throwUserError(`Failed retrieving user account: ${error.message}. ${ERROR_CALL_TO_ACTION}`);
}
};
const ExtensionResponseSchema = z.array(z.object({
// ndhoule: The `author` and `extension_token` fields are not sent by the .../safe endpoint;
// we're normalizing them to empty values here to preserve...uh, whatever backward compatibility
// this is supposed to offer.
//
// At this point, I'm unsure if modern `@netlify/config` callers can end up in the .../safe
// codepath. This would be bad: extension-injected build hooks are far removed from this code
// path and have no way of knowing whether or not a specific consumer is in this legacy code
// path. They might call the Netlify API expecting to have an API token available to them when
// they really don't. For the time being, I've added instrumentation to Jigsaw to help us figure
// out if this is dead code or actually supports current users.
author: z.string().optional().default(undefined),
extension_token: z.string().optional().default(undefined),
has_build: z.boolean(),
name: z.string(),
slug: z.string(),
version: z.string(),
// Returned by API, but unused. Leaving this here for the sake of documentation.
// has_connector: z.boolean(),
}));
export const getExtensions = async function ({ siteId, accountId, testOpts, offline, token, featureFlags, extensionApiBaseUrl, mode, }) {
if (!siteId || offline) {
return [];
}
const sendBuildBotTokenToJigsaw = featureFlags?.send_build_bot_token_to_jigsaw;
const { host: originalHost, setBaseUrl } = testOpts;
// TODO(kh): I am adding this purely for local staging development.
// We should remove this once we have fixed https://github.com/netlify/cli/blob/b5a5c7525edd28925c5c2e3e5f0f00c4261eaba5/src/lib/build.ts#L125
let host = originalHost;
// If there is a host, we use it to fetch the extensions
// we check if the host is staging or production and set the host accordingly,
// sadly necessary because of https://github.com/netlify/cli/blob/b5a5c7525edd28925c5c2e3e5f0f00c4261eaba5/src/lib/build.ts#L125
if (originalHost) {
if (originalHost?.includes(NETLIFY_API_STAGING_HOSTNAME)) {
host = EXTENSION_API_STAGING_BASE_URL;
}
else if (originalHost?.includes(NETLIFY_API_HOSTNAME)) {
host = EXTENSION_API_BASE_URL;
}
else {
host = `http://${originalHost}`;
}
}
const baseUrl = new URL(host ?? extensionApiBaseUrl);
// We only use this for testing
if (host && setBaseUrl) {
setBaseUrl(extensionApiBaseUrl);
}
// if accountId isn't present, use safe v1 endpoint
const url = accountId
? `${baseUrl}team/${accountId}/integrations/installations/meta/${siteId}`
: `${baseUrl}site/${siteId}/integrations/safe`;
const headers = new Headers({
'Netlify-Config-Mode': mode,
'User-Agent': `Netlify Config (mode:${mode}) / ${ROOT_PACKAGE_JSON.version}`,
});
if (sendBuildBotTokenToJigsaw && token) {
headers.set('Netlify-SDK-Build-Bot-Token', token);
}
try {
const res = await fetch(url, { headers });
if (res.status !== 200) {
throw new Error(`Unexpected status code ${res.status} from fetching extensions`);
}
return ExtensionResponseSchema.parse(await res.json());
}
catch (err) {
return throwUserError(`Failed retrieving extensions for site ${siteId}: ${err instanceof Error ? err.message : 'unknown error'}. ${ERROR_CALL_TO_ACTION}`);
}
};