@netlify/config
Version:
Netlify config module
251 lines (250 loc) • 10.7 kB
JavaScript
import { getApiClient } from './api/client.js';
import { getSiteInfo } from './api/site_info.js';
import { getInitialBase, getBase, addBase } from './base.js';
import { getBuildDir } from './build_dir.js';
import { getCachedConfig } from './cached_config.js';
import { normalizeContextProps, mergeContext } from './context.js';
import { parseDefaultConfig } from './default.js';
import { getEnv } from './env/main.js';
import { resolveConfigPaths } from './files.js';
import { getHeadersPath, addHeaders } from './headers.js';
import { getInlineConfig } from './inline_config.js';
import { EXTENSION_API_BASE_URL, EXTENSION_API_STAGING_BASE_URL, mergeIntegrations, NETLIFY_API_STAGING_BASE_URL, } from './integrations.js';
import { logResult } from './log/main.js';
import { mergeConfigs } from './merge.js';
import { normalizeBeforeConfigMerge, normalizeAfterConfigMerge } from './merge_normalize.js';
import { addDefaultOpts, normalizeOpts } from './options/main.js';
import { UI_ORIGIN, CONFIG_ORIGIN, INLINE_ORIGIN } from './origin.js';
import { parseConfig } from './parse.js';
import { getConfigPath } from './path.js';
import { getRedirectsPath, addRedirects } from './redirects.js';
/**
* Load the configuration file.
* Takes an optional configuration file path as input and return the resolved
* `config` together with related properties such as the `configPath`.
*/
export const resolveConfig = async function (opts) {
const { cachedConfig, cachedConfigPath, host, scheme, packagePath, pathPrefix, testOpts, token, offline, siteFeatureFlagPrefix, ...optsA } = addDefaultOpts(opts);
// `api` is not JSON-serializable, so we cannot cache it inside `cachedConfig`
const api = getApiClient({ token, offline, host, scheme, pathPrefix, testOpts });
const parsedCachedConfig = await getCachedConfig({ cachedConfig, cachedConfigPath, token, api });
// If there is a cached config, use it. The exception is when a default config,
// which consumers like the CLI can set, is present. In those cases, let the
// flow continue so that the default config is parsed and used.
if (parsedCachedConfig !== undefined && opts.defaultConfig === undefined) {
return parsedCachedConfig;
}
// TODO(kh): remove this mapping and get the extensionApiHost from the opts
const extensionApiBaseUrl = host?.includes(NETLIFY_API_STAGING_BASE_URL)
? EXTENSION_API_STAGING_BASE_URL
: EXTENSION_API_BASE_URL;
const { config: configOpt, defaultConfig, inlineConfig, configMutations, cwd, context, repositoryRoot, base, branch, siteId, accountId, deployId, buildId, baseRelDir, mode, debug, logs, featureFlags, } = await normalizeOpts(optsA);
let { siteInfo, accounts, addons, integrations } = parsedCachedConfig || {};
// If we have cached site info, we don't need to fetch it again
const useCachedSiteInfo = Boolean(featureFlags?.use_cached_site_info && siteInfo && accounts && addons && integrations);
// I'm adding some debug logging to see if the logic is working as expected
if (featureFlags?.use_cached_site_info_logging) {
console.log('Checking site information', { useCachedSiteInfo, siteInfo, accounts, addons, integrations });
}
if (!useCachedSiteInfo) {
const updatedSiteInfo = await getSiteInfo({
api,
context,
siteId,
accountId,
mode,
siteFeatureFlagPrefix,
offline,
featureFlags,
testOpts,
token,
extensionApiBaseUrl,
});
siteInfo = updatedSiteInfo.siteInfo;
accounts = updatedSiteInfo.accounts;
addons = updatedSiteInfo.addons;
integrations = updatedSiteInfo.integrations;
}
const { defaultConfig: defaultConfigA, baseRelDir: baseRelDirA } = parseDefaultConfig({
defaultConfig,
base,
baseRelDir,
siteInfo,
logs,
debug,
});
const inlineConfigA = getInlineConfig({ inlineConfig, configMutations, logs, debug });
const { configPath, config, buildDir, redirectsPath, headersPath } = await loadConfig({
configOpt,
cwd,
context,
repositoryRoot,
packagePath,
branch,
defaultConfig: defaultConfigA,
inlineConfig: inlineConfigA,
baseRelDir: baseRelDirA,
logs,
featureFlags,
});
const env = await getEnv({
api,
mode,
config,
siteInfo,
accounts,
addons,
buildDir,
branch,
deployId,
buildId,
context,
cachedEnv: parsedCachedConfig?.env || {},
});
// @todo Remove in the next major version.
const configA = addLegacyFunctionsDirectory(config);
const mergedIntegrations = await mergeIntegrations({
apiIntegrations: integrations,
configIntegrations: configA.integrations,
context: context,
testOpts,
offline,
extensionApiBaseUrl,
});
const result = {
siteInfo,
integrations: mergedIntegrations,
accounts,
addons,
env,
configPath,
redirectsPath,
headersPath,
buildDir,
repositoryRoot,
config: configA,
context,
branch,
token,
api,
logs,
};
logResult(result, { logs, debug });
return result;
};
/**
* Adds a `build.functions` property that mirrors `functionsDirectory`, for
* backward compatibility.
*/
const addLegacyFunctionsDirectory = (config) => {
if (!config.functionsDirectory) {
return config;
}
return { ...config, build: { ...config.build, functions: config.functionsDirectory } };
};
/**
* Try to load the configuration file in two passes.
* The first pass uses the `defaultConfig`'s `build.base` (if defined).
* The second pass uses the `build.base` from the first pass (if defined).
*/
const loadConfig = async function ({ configOpt, cwd, context, repositoryRoot, packagePath, branch, defaultConfig, inlineConfig, baseRelDir, logs, featureFlags, }) {
const initialBase = getInitialBase({ repositoryRoot, defaultConfig, inlineConfig });
const { configPath, config, buildDir, base, redirectsPath, headersPath } = await getFullConfig({
configOpt,
cwd,
context,
repositoryRoot,
branch,
defaultConfig,
inlineConfig,
baseRelDir,
packagePath,
configBase: initialBase,
logs,
featureFlags,
});
// No second pass needed if:
// - there is no `build.base` (in which case both `base` and `initialBase`
// are `undefined`)
// - `build.base` is the same as the `Base directory` UI setting (already
// used in the first round)
// - `baseRelDir` feature flag is not used. This feature flag was introduced
// to ensure backward compatibility.
if (!baseRelDir || base === initialBase) {
return { configPath, config, buildDir, redirectsPath, headersPath };
}
const { configPath: configPathA, config: configA, buildDir: buildDirA, redirectsPath: redirectsPathA, headersPath: headersPathA, } = await getFullConfig({
cwd,
context,
repositoryRoot,
branch,
defaultConfig,
inlineConfig,
baseRelDir,
configBase: base,
base,
logs,
featureFlags,
});
return {
configPath: configPathA,
config: configA,
buildDir: buildDirA,
redirectsPath: redirectsPathA,
headersPath: headersPathA,
};
};
/**
* Load configuration file and normalize it, merge contexts, etc.
*/
const getFullConfig = async function ({ configOpt, cwd, context, repositoryRoot, packagePath, branch, defaultConfig, inlineConfig, baseRelDir, configBase, base, logs, featureFlags, }) {
const configPath = await getConfigPath({ configOpt, cwd, repositoryRoot, packagePath, configBase });
try {
const config = await parseConfig(configPath);
const configA = mergeAndNormalizeConfig({ config, defaultConfig, inlineConfig, context, branch, logs, packagePath });
const { config: configB, buildDir, base: baseA, } = await resolveFiles({ packagePath, config: configA, repositoryRoot, base, baseRelDir });
const headersPath = getHeadersPath(configB);
const configC = await addHeaders({ config: configB, headersPath, logs });
const redirectsPath = getRedirectsPath(configC);
const configD = await addRedirects({ config: configC, redirectsPath, logs, featureFlags });
return { configPath, config: configD, buildDir, base: baseA, redirectsPath, headersPath };
}
catch (error) {
const configName = configPath === undefined ? '' : ` file ${configPath}`;
error.message = `When resolving config${configName}:\n${error.message}`;
throw error;
}
};
/**
* Merge:
* - `--defaultConfig`: UI build settings and UI-installed plugins
* - `inlineConfig`: Netlify CLI flags
* Then merge context-specific configuration.
* Before and after those steps, also performs validation and normalization.
* Those need to be done at different stages depending on whether they should
* happen before/after the merges mentioned above.
*/
const mergeAndNormalizeConfig = function ({ config, defaultConfig, inlineConfig, context, branch, logs, packagePath }) {
const configA = normalizeConfigAndContext(config, CONFIG_ORIGIN);
const defaultConfigA = normalizeConfigAndContext(defaultConfig, UI_ORIGIN);
const inlineConfigA = normalizeConfigAndContext(inlineConfig, INLINE_ORIGIN);
const configB = mergeConfigs([defaultConfigA, configA]);
const configC = mergeContext({ config: configB, context, branch, logs });
const configD = mergeConfigs([configC, inlineConfigA]);
return normalizeAfterConfigMerge(configD, packagePath);
};
const normalizeConfigAndContext = function (config, origin) {
const configA = normalizeBeforeConfigMerge(config, origin);
const configB = normalizeContextProps({ config: configA, origin });
return configB;
};
/**
* Find base directory, build directory and resolve all paths to absolute paths
*/
const resolveFiles = async function ({ config, repositoryRoot, base, packagePath, baseRelDir, }) {
const baseA = getBase(base, repositoryRoot, config);
const buildDir = await getBuildDir(repositoryRoot, baseA);
const configA = resolveConfigPaths({ config, packagePath, repositoryRoot, buildDir, baseRelDir });
const configB = addBase(configA, baseA);
return { config: configB, buildDir, base: baseA };
};