@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 };
};