@netlify/config 18.2.5-rc → 18.2.5
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/bin.js +5 -0
- package/lib/api/build_settings.js +28 -0
- package/lib/api/client.js +12 -0
- package/lib/api/site_info.js +59 -0
- package/lib/base.js +26 -0
- package/lib/bin/flags.js +173 -0
- package/lib/bin/main.js +59 -0
- package/lib/build_dir.js +22 -0
- package/lib/cached_config.js +26 -0
- package/lib/case.js +18 -0
- package/lib/context.js +86 -0
- package/lib/default.js +27 -0
- package/lib/env/envelope.js +24 -0
- package/lib/env/git.js +23 -0
- package/lib/env/main.js +150 -0
- package/lib/error.js +28 -0
- package/lib/events.js +21 -0
- package/lib/files.js +83 -0
- package/lib/functions_config.js +67 -0
- package/lib/headers.js +20 -0
- package/lib/inline_config.js +8 -0
- package/lib/log/cleanup.js +64 -0
- package/lib/log/logger.js +36 -0
- package/lib/log/main.js +39 -0
- package/lib/log/messages.js +87 -0
- package/lib/log/options.js +29 -0
- package/lib/log/serialize.js +4 -0
- package/lib/log/theme.js +13 -0
- package/lib/main.js +210 -0
- package/lib/merge.js +43 -0
- package/lib/merge_normalize.js +24 -0
- package/lib/mutations/apply.js +66 -0
- package/lib/mutations/config_prop_name.js +14 -0
- package/lib/mutations/update.js +98 -0
- package/lib/normalize.js +32 -0
- package/lib/options/base.js +54 -0
- package/lib/options/branch.js +31 -0
- package/lib/options/feature_flags.js +12 -0
- package/lib/options/main.js +91 -0
- package/lib/options/repository_root.js +16 -0
- package/lib/origin.js +31 -0
- package/lib/parse.js +56 -0
- package/lib/path.js +41 -0
- package/lib/redirects.js +19 -0
- package/lib/simplify.js +77 -0
- package/lib/utils/group.js +9 -0
- package/lib/utils/remove_falsy.js +14 -0
- package/lib/utils/set.js +27 -0
- package/lib/utils/toml.js +20 -0
- package/lib/validate/context.js +38 -0
- package/lib/validate/example.js +30 -0
- package/lib/validate/helpers.js +25 -0
- package/lib/validate/identical.js +16 -0
- package/lib/validate/main.js +99 -0
- package/lib/validate/validations.js +275 -0
- package/package.json +14 -8
package/lib/main.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { getApiClient } from './api/client.js';
|
|
2
|
+
import { getSiteInfo } from './api/site_info.js';
|
|
3
|
+
import { getInitialBase, getBase, addBase } from './base.js';
|
|
4
|
+
import { getBuildDir } from './build_dir.js';
|
|
5
|
+
import { getCachedConfig } from './cached_config.js';
|
|
6
|
+
import { normalizeContextProps, mergeContext } from './context.js';
|
|
7
|
+
import { parseDefaultConfig } from './default.js';
|
|
8
|
+
import { getEnv } from './env/main.js';
|
|
9
|
+
import { resolveConfigPaths } from './files.js';
|
|
10
|
+
import { getHeadersPath, addHeaders } from './headers.js';
|
|
11
|
+
import { getInlineConfig } from './inline_config.js';
|
|
12
|
+
import { logResult } from './log/main.js';
|
|
13
|
+
import { mergeConfigs } from './merge.js';
|
|
14
|
+
import { normalizeBeforeConfigMerge, normalizeAfterConfigMerge } from './merge_normalize.js';
|
|
15
|
+
import { addDefaultOpts, normalizeOpts } from './options/main.js';
|
|
16
|
+
import { UI_ORIGIN, CONFIG_ORIGIN, INLINE_ORIGIN } from './origin.js';
|
|
17
|
+
import { parseConfig } from './parse.js';
|
|
18
|
+
import { getConfigPath } from './path.js';
|
|
19
|
+
import { getRedirectsPath, addRedirects } from './redirects.js';
|
|
20
|
+
export { DEV_EVENTS, EVENTS } from './events.js';
|
|
21
|
+
export { cleanupConfig } from './log/cleanup.js';
|
|
22
|
+
export { updateConfig, restoreConfig } from './mutations/update.js';
|
|
23
|
+
// Load the configuration file.
|
|
24
|
+
// Takes an optional configuration file path as input and return the resolved
|
|
25
|
+
// `config` together with related properties such as the `configPath`.
|
|
26
|
+
export const resolveConfig = async function (opts) {
|
|
27
|
+
const { cachedConfig, cachedConfigPath, host, scheme, pathPrefix, testOpts, token, offline, ...optsA } = addDefaultOpts(opts);
|
|
28
|
+
// `api` is not JSON-serializable, so we cannot cache it inside `cachedConfig`
|
|
29
|
+
const api = getApiClient({ token, offline, host, scheme, pathPrefix, testOpts });
|
|
30
|
+
const parsedCachedConfig = await getCachedConfig({ cachedConfig, cachedConfigPath, token, api });
|
|
31
|
+
if (parsedCachedConfig !== undefined) {
|
|
32
|
+
return parsedCachedConfig;
|
|
33
|
+
}
|
|
34
|
+
const { config: configOpt, defaultConfig, inlineConfig, configMutations, cwd, context, repositoryRoot, base, branch, siteId, deployId, buildId, baseRelDir, mode, debug, logs, featureFlags, } = await normalizeOpts(optsA);
|
|
35
|
+
const { siteInfo, accounts, addons } = await getSiteInfo({ api, siteId, mode, testOpts });
|
|
36
|
+
const { defaultConfig: defaultConfigA, baseRelDir: baseRelDirA } = parseDefaultConfig({
|
|
37
|
+
defaultConfig,
|
|
38
|
+
base,
|
|
39
|
+
baseRelDir,
|
|
40
|
+
siteInfo,
|
|
41
|
+
logs,
|
|
42
|
+
debug,
|
|
43
|
+
});
|
|
44
|
+
const inlineConfigA = getInlineConfig({ inlineConfig, configMutations, logs, debug });
|
|
45
|
+
const { configPath, config, buildDir, redirectsPath, headersPath } = await loadConfig({
|
|
46
|
+
configOpt,
|
|
47
|
+
cwd,
|
|
48
|
+
context,
|
|
49
|
+
repositoryRoot,
|
|
50
|
+
branch,
|
|
51
|
+
defaultConfig: defaultConfigA,
|
|
52
|
+
inlineConfig: inlineConfigA,
|
|
53
|
+
baseRelDir: baseRelDirA,
|
|
54
|
+
logs,
|
|
55
|
+
featureFlags,
|
|
56
|
+
});
|
|
57
|
+
const env = await getEnv({
|
|
58
|
+
api,
|
|
59
|
+
mode,
|
|
60
|
+
config,
|
|
61
|
+
siteInfo,
|
|
62
|
+
accounts,
|
|
63
|
+
addons,
|
|
64
|
+
buildDir,
|
|
65
|
+
branch,
|
|
66
|
+
deployId,
|
|
67
|
+
buildId,
|
|
68
|
+
context,
|
|
69
|
+
});
|
|
70
|
+
// @todo Remove in the next major version.
|
|
71
|
+
const configA = addLegacyFunctionsDirectory(config);
|
|
72
|
+
const result = {
|
|
73
|
+
siteInfo,
|
|
74
|
+
accounts,
|
|
75
|
+
addons,
|
|
76
|
+
env,
|
|
77
|
+
configPath,
|
|
78
|
+
redirectsPath,
|
|
79
|
+
headersPath,
|
|
80
|
+
buildDir,
|
|
81
|
+
repositoryRoot,
|
|
82
|
+
config: configA,
|
|
83
|
+
context,
|
|
84
|
+
branch,
|
|
85
|
+
token,
|
|
86
|
+
api,
|
|
87
|
+
logs,
|
|
88
|
+
};
|
|
89
|
+
logResult(result, { logs, debug });
|
|
90
|
+
return result;
|
|
91
|
+
};
|
|
92
|
+
// Adds a `build.functions` property that mirrors `functionsDirectory`, for
|
|
93
|
+
// backward compatibility.
|
|
94
|
+
const addLegacyFunctionsDirectory = (config) => {
|
|
95
|
+
if (!config.functionsDirectory) {
|
|
96
|
+
return config;
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
...config,
|
|
100
|
+
build: {
|
|
101
|
+
...config.build,
|
|
102
|
+
functions: config.functionsDirectory,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
// Try to load the configuration file in two passes.
|
|
107
|
+
// The first pass uses the `defaultConfig`'s `build.base` (if defined).
|
|
108
|
+
// The second pass uses the `build.base` from the first pass (if defined).
|
|
109
|
+
const loadConfig = async function ({ configOpt, cwd, context, repositoryRoot, branch, defaultConfig, inlineConfig, baseRelDir, logs, featureFlags, }) {
|
|
110
|
+
const initialBase = getInitialBase({ repositoryRoot, defaultConfig, inlineConfig });
|
|
111
|
+
const { configPath, config, buildDir, base, redirectsPath, headersPath } = await getFullConfig({
|
|
112
|
+
configOpt,
|
|
113
|
+
cwd,
|
|
114
|
+
context,
|
|
115
|
+
repositoryRoot,
|
|
116
|
+
branch,
|
|
117
|
+
defaultConfig,
|
|
118
|
+
inlineConfig,
|
|
119
|
+
baseRelDir,
|
|
120
|
+
configBase: initialBase,
|
|
121
|
+
logs,
|
|
122
|
+
featureFlags,
|
|
123
|
+
});
|
|
124
|
+
// No second pass needed if:
|
|
125
|
+
// - there is no `build.base` (in which case both `base` and `initialBase`
|
|
126
|
+
// are `undefined`)
|
|
127
|
+
// - `build.base` is the same as the `Base directory` UI setting (already
|
|
128
|
+
// used in the first round)
|
|
129
|
+
// - `baseRelDir` feature flag is not used. This feature flag was introduced
|
|
130
|
+
// to ensure backward compatibility.
|
|
131
|
+
if (!baseRelDir || base === initialBase) {
|
|
132
|
+
return { configPath, config, buildDir, redirectsPath, headersPath };
|
|
133
|
+
}
|
|
134
|
+
const { configPath: configPathA, config: configA, buildDir: buildDirA, redirectsPath: redirectsPathA, headersPath: headersPathA, } = await getFullConfig({
|
|
135
|
+
cwd,
|
|
136
|
+
context,
|
|
137
|
+
repositoryRoot,
|
|
138
|
+
branch,
|
|
139
|
+
defaultConfig,
|
|
140
|
+
inlineConfig,
|
|
141
|
+
baseRelDir,
|
|
142
|
+
configBase: base,
|
|
143
|
+
base,
|
|
144
|
+
logs,
|
|
145
|
+
featureFlags,
|
|
146
|
+
});
|
|
147
|
+
return {
|
|
148
|
+
configPath: configPathA,
|
|
149
|
+
config: configA,
|
|
150
|
+
buildDir: buildDirA,
|
|
151
|
+
redirectsPath: redirectsPathA,
|
|
152
|
+
headersPath: headersPathA,
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
// Load configuration file and normalize it, merge contexts, etc.
|
|
156
|
+
const getFullConfig = async function ({ configOpt, cwd, context, repositoryRoot, branch, defaultConfig, inlineConfig, baseRelDir, configBase, base, logs, featureFlags, }) {
|
|
157
|
+
const configPath = await getConfigPath({ configOpt, cwd, repositoryRoot, configBase });
|
|
158
|
+
try {
|
|
159
|
+
const config = await parseConfig(configPath);
|
|
160
|
+
const configA = mergeAndNormalizeConfig({
|
|
161
|
+
config,
|
|
162
|
+
defaultConfig,
|
|
163
|
+
inlineConfig,
|
|
164
|
+
context,
|
|
165
|
+
branch,
|
|
166
|
+
logs,
|
|
167
|
+
});
|
|
168
|
+
const { config: configB, buildDir, base: baseA, } = await resolveFiles({ config: configA, repositoryRoot, base, baseRelDir });
|
|
169
|
+
const headersPath = getHeadersPath(configB);
|
|
170
|
+
const configC = await addHeaders({ config: configB, headersPath, logs, featureFlags });
|
|
171
|
+
const redirectsPath = getRedirectsPath(configC);
|
|
172
|
+
const configD = await addRedirects({ config: configC, redirectsPath, logs, featureFlags });
|
|
173
|
+
return { configPath, config: configD, buildDir, base: baseA, redirectsPath, headersPath };
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
const configName = configPath === undefined ? '' : ` file ${configPath}`;
|
|
177
|
+
error.message = `When resolving config${configName}:\n${error.message}`;
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
// Merge:
|
|
182
|
+
// - `--defaultConfig`: UI build settings and UI-installed plugins
|
|
183
|
+
// - `inlineConfig`: Netlify CLI flags
|
|
184
|
+
// Then merge context-specific configuration.
|
|
185
|
+
// Before and after those steps, also performs validation and normalization.
|
|
186
|
+
// Those need to be done at different stages depending on whether they should
|
|
187
|
+
// happen before/after the merges mentioned above.
|
|
188
|
+
const mergeAndNormalizeConfig = function ({ config, defaultConfig, inlineConfig, context, branch, logs }) {
|
|
189
|
+
const configA = normalizeConfigAndContext(config, CONFIG_ORIGIN);
|
|
190
|
+
const defaultConfigA = normalizeConfigAndContext(defaultConfig, UI_ORIGIN);
|
|
191
|
+
const inlineConfigA = normalizeConfigAndContext(inlineConfig, INLINE_ORIGIN);
|
|
192
|
+
const configB = mergeConfigs([defaultConfigA, configA]);
|
|
193
|
+
const configC = mergeContext({ config: configB, context, branch, logs });
|
|
194
|
+
const configD = mergeConfigs([configC, inlineConfigA]);
|
|
195
|
+
const configE = normalizeAfterConfigMerge(configD);
|
|
196
|
+
return configE;
|
|
197
|
+
};
|
|
198
|
+
const normalizeConfigAndContext = function (config, origin) {
|
|
199
|
+
const configA = normalizeBeforeConfigMerge(config, origin);
|
|
200
|
+
const configB = normalizeContextProps({ config: configA, origin });
|
|
201
|
+
return configB;
|
|
202
|
+
};
|
|
203
|
+
// Find base directory, build directory and resolve all paths to absolute paths
|
|
204
|
+
const resolveFiles = async function ({ config, repositoryRoot, base, baseRelDir }) {
|
|
205
|
+
const baseA = getBase(base, repositoryRoot, config);
|
|
206
|
+
const buildDir = await getBuildDir(repositoryRoot, baseA);
|
|
207
|
+
const configA = await resolveConfigPaths({ config, repositoryRoot, buildDir, baseRelDir });
|
|
208
|
+
const configB = addBase(configA, baseA);
|
|
209
|
+
return { config: configB, buildDir, base: baseA };
|
|
210
|
+
};
|
package/lib/merge.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import deepmerge from 'deepmerge';
|
|
2
|
+
import isPlainObj from 'is-plain-obj';
|
|
3
|
+
import { groupBy } from './utils/group.js';
|
|
4
|
+
import { removeUndefined } from './utils/remove_falsy.js';
|
|
5
|
+
// Merge an array of configuration objects.
|
|
6
|
+
// Last items have higher priority.
|
|
7
|
+
// Configuration objects are deeply merged.
|
|
8
|
+
// - Arrays are overridden, not concatenated.
|
|
9
|
+
export const mergeConfigs = function (configs) {
|
|
10
|
+
const cleanedConfigs = configs.map(removeUndefinedProps);
|
|
11
|
+
return deepmerge.all(cleanedConfigs, { arrayMerge });
|
|
12
|
+
};
|
|
13
|
+
const removeUndefinedProps = function ({ build = {}, ...config }) {
|
|
14
|
+
return removeUndefined({ ...config, build: removeUndefined(build) });
|
|
15
|
+
};
|
|
16
|
+
// By default `deepmerge` concatenates arrays. We use the `arrayMerge` option
|
|
17
|
+
// to remove this behavior. Also, we merge some array properties differently,
|
|
18
|
+
// such as `plugins`.
|
|
19
|
+
const arrayMerge = function (arrayA, arrayB) {
|
|
20
|
+
if (isPluginsProperty(arrayA) && isPluginsProperty(arrayB)) {
|
|
21
|
+
return mergePlugins(arrayA, arrayB);
|
|
22
|
+
}
|
|
23
|
+
return arrayB;
|
|
24
|
+
};
|
|
25
|
+
// `deepmerge` does not allow retrieving the name of the array property being
|
|
26
|
+
// merged, so we need to do some heuristics.
|
|
27
|
+
const isPluginsProperty = function (array) {
|
|
28
|
+
return Array.isArray(array) && array.every(isPluginObject);
|
|
29
|
+
};
|
|
30
|
+
const isPluginObject = function (object) {
|
|
31
|
+
return isPlainObj(object) && typeof object.package === 'string';
|
|
32
|
+
};
|
|
33
|
+
// Merge two `config.plugins`. Merge plugins with the same `plugin.package`.
|
|
34
|
+
const mergePlugins = function (pluginsA, pluginsB) {
|
|
35
|
+
const plugins = [...pluginsA, ...pluginsB];
|
|
36
|
+
return groupBy(plugins, 'package').map(mergePluginConfigs);
|
|
37
|
+
};
|
|
38
|
+
const mergePluginConfigs = function (plugins) {
|
|
39
|
+
return plugins.reduce(mergePluginsPair, {});
|
|
40
|
+
};
|
|
41
|
+
const mergePluginsPair = function (pluginA, pluginB) {
|
|
42
|
+
return deepmerge(pluginA, pluginB, { arrayMerge });
|
|
43
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { normalizeConfigCase } from './case.js';
|
|
2
|
+
import { normalizeConfig } from './normalize.js';
|
|
3
|
+
import { addOrigins } from './origin.js';
|
|
4
|
+
import { validateIdenticalPlugins } from './validate/identical.js';
|
|
5
|
+
import { validatePreCaseNormalize, validatePreMergeConfig, validatePreNormalizeConfig, validatePostNormalizeConfig, } from './validate/main.js';
|
|
6
|
+
// Perform validation and normalization logic to apply to all of:
|
|
7
|
+
// - config, defaultConfig, inlineConfig
|
|
8
|
+
// - context-specific configs
|
|
9
|
+
// Therefore, this is performing before merging those together.
|
|
10
|
+
export const normalizeBeforeConfigMerge = function (config, origin) {
|
|
11
|
+
validatePreCaseNormalize(config);
|
|
12
|
+
const configA = normalizeConfigCase(config);
|
|
13
|
+
validatePreMergeConfig(configA);
|
|
14
|
+
const configB = addOrigins(configA, origin);
|
|
15
|
+
validateIdenticalPlugins(configB);
|
|
16
|
+
return configB;
|
|
17
|
+
};
|
|
18
|
+
// Validation and normalization logic performed after merging
|
|
19
|
+
export const normalizeAfterConfigMerge = function (config) {
|
|
20
|
+
validatePreNormalizeConfig(config);
|
|
21
|
+
const configA = normalizeConfig(config);
|
|
22
|
+
validatePostNormalizeConfig(configA);
|
|
23
|
+
return configA;
|
|
24
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { throwUserError } from '../error.js';
|
|
2
|
+
import { EVENTS } from '../events.js';
|
|
3
|
+
import { WILDCARD_ALL, FUNCTION_CONFIG_PROPERTIES } from '../functions_config.js';
|
|
4
|
+
import { setProp } from '../utils/set.js';
|
|
5
|
+
import { getPropName } from './config_prop_name.js';
|
|
6
|
+
// Apply a series of mutations to `inlineConfig`.
|
|
7
|
+
// Meant to be used to apply configuration changes at build time.
|
|
8
|
+
// Those are applied on the `inlineConfig` object after `@netlify/config`
|
|
9
|
+
// normalization. Therefore, this function also denormalizes (reverts that
|
|
10
|
+
// normalization) so that the final `config` object can be serialized back to
|
|
11
|
+
// a `netlify.toml`.
|
|
12
|
+
export const applyMutations = function (inlineConfig, configMutations) {
|
|
13
|
+
return configMutations.reduce(applyMutation, inlineConfig);
|
|
14
|
+
};
|
|
15
|
+
const applyMutation = function (inlineConfig, { keys, value, event }) {
|
|
16
|
+
const propName = getPropName(keys);
|
|
17
|
+
if (!(propName in MUTABLE_PROPS)) {
|
|
18
|
+
throwUserError(`"netlifyConfig.${propName}" is read-only.`);
|
|
19
|
+
}
|
|
20
|
+
const { lastEvent, denormalize } = MUTABLE_PROPS[propName];
|
|
21
|
+
validateEvent(lastEvent, event, propName);
|
|
22
|
+
return denormalize === undefined ? setProp(inlineConfig, keys, value) : denormalize(inlineConfig, value, keys);
|
|
23
|
+
};
|
|
24
|
+
const validateEvent = function (lastEvent, event, propName) {
|
|
25
|
+
if (EVENTS.indexOf(lastEvent) < EVENTS.indexOf(event)) {
|
|
26
|
+
throwUserError(`"netlifyConfig.${propName}" cannot be modified after "${lastEvent}".`);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
// `functions['*'].*` has higher priority than `functions.*` so we convert the
|
|
30
|
+
// latter to the former.
|
|
31
|
+
const denormalizeFunctionsTopProps = function ({ functions, functions: { [WILDCARD_ALL]: wildcardProps } = {}, ...inlineConfig }, value, [, key]) {
|
|
32
|
+
return FUNCTION_CONFIG_PROPERTIES.has(key)
|
|
33
|
+
? {
|
|
34
|
+
...inlineConfig,
|
|
35
|
+
functions: { ...functions, [WILDCARD_ALL]: { ...wildcardProps, [key]: value } },
|
|
36
|
+
}
|
|
37
|
+
: { ...inlineConfig, functions: { ...functions, [key]: value } };
|
|
38
|
+
};
|
|
39
|
+
// List of properties that are not read-only.
|
|
40
|
+
const MUTABLE_PROPS = {
|
|
41
|
+
'build.command': { lastEvent: 'onPreBuild' },
|
|
42
|
+
'build.edge_functions': { lastEvent: 'onPostBuild' },
|
|
43
|
+
'build.environment': { lastEvent: 'onPostBuild' },
|
|
44
|
+
'build.environment.*': { lastEvent: 'onPostBuild' },
|
|
45
|
+
'build.functions': { lastEvent: 'onBuild' },
|
|
46
|
+
'build.processing': { lastEvent: 'onPostBuild' },
|
|
47
|
+
'build.processing.css': { lastEvent: 'onPostBuild' },
|
|
48
|
+
'build.processing.css.bundle': { lastEvent: 'onPostBuild' },
|
|
49
|
+
'build.processing.css.minify': { lastEvent: 'onPostBuild' },
|
|
50
|
+
'build.processing.html': { lastEvent: 'onPostBuild' },
|
|
51
|
+
'build.processing.html.pretty_urls': { lastEvent: 'onPostBuild' },
|
|
52
|
+
'build.processing.images': { lastEvent: 'onPostBuild' },
|
|
53
|
+
'build.processing.images.compress': { lastEvent: 'onPostBuild' },
|
|
54
|
+
'build.processing.js': { lastEvent: 'onPostBuild' },
|
|
55
|
+
'build.processing.js.bundle': { lastEvent: 'onPostBuild' },
|
|
56
|
+
'build.processing.js.minify': { lastEvent: 'onPostBuild' },
|
|
57
|
+
'build.processing.skip_processing': { lastEvent: 'onPostBuild' },
|
|
58
|
+
'build.publish': { lastEvent: 'onPostBuild' },
|
|
59
|
+
'build.services': { lastEvent: 'onPostBuild' },
|
|
60
|
+
'build.services.*': { lastEvent: 'onPostBuild' },
|
|
61
|
+
edge_functions: { lastEvent: 'onPostBuild' },
|
|
62
|
+
'functions.*': { lastEvent: 'onBuild', denormalize: denormalizeFunctionsTopProps },
|
|
63
|
+
'functions.*.*': { lastEvent: 'onBuild' },
|
|
64
|
+
headers: { lastEvent: 'onPostBuild' },
|
|
65
|
+
redirects: { lastEvent: 'onPostBuild' },
|
|
66
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Retrieve normalized property name
|
|
2
|
+
export const getPropName = function (keys) {
|
|
3
|
+
return keys.reduce(normalizeDynamicProp, '');
|
|
4
|
+
};
|
|
5
|
+
// Some properties are user-defined, i.e. we need to replace them with a "*" token
|
|
6
|
+
// Check if a property name is dynamic, such as `functions.{functionName}`, or
|
|
7
|
+
// is an array index.
|
|
8
|
+
// In those cases, we replace it by "*".
|
|
9
|
+
const normalizeDynamicProp = function (propName, key) {
|
|
10
|
+
const normalizedKey = Number.isInteger(key) || DYNAMIC_OBJECT_PROPS.has(propName) ? '*' : String(key);
|
|
11
|
+
return propName === '' ? normalizedKey : `${propName}.${normalizedKey}`;
|
|
12
|
+
};
|
|
13
|
+
// Properties with dynamic children
|
|
14
|
+
const DYNAMIC_OBJECT_PROPS = new Set(['build.services', 'build.environment', 'functions', 'functions.*']);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { pathExists } from 'path-exists';
|
|
3
|
+
import { ensureConfigPriority } from '../context.js';
|
|
4
|
+
import { addHeaders } from '../headers.js';
|
|
5
|
+
import { mergeConfigs } from '../merge.js';
|
|
6
|
+
import { parseOptionalConfig } from '../parse.js';
|
|
7
|
+
import { addRedirects } from '../redirects.js';
|
|
8
|
+
import { simplifyConfig } from '../simplify.js';
|
|
9
|
+
import { serializeToml } from '../utils/toml.js';
|
|
10
|
+
import { applyMutations } from './apply.js';
|
|
11
|
+
// Persist configuration changes to `netlify.toml`.
|
|
12
|
+
// If `netlify.toml` does not exist, creates it. Otherwise, merges the changes.
|
|
13
|
+
export const updateConfig = async function (configMutations, { buildDir, configPath, headersPath, redirectsPath, context, branch, logs, featureFlags }) {
|
|
14
|
+
if (configMutations.length === 0) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const inlineConfig = applyMutations({}, configMutations);
|
|
18
|
+
const normalizedInlineConfig = ensureConfigPriority(inlineConfig, context, branch);
|
|
19
|
+
const updatedConfig = await mergeWithConfig(normalizedInlineConfig, configPath);
|
|
20
|
+
const configWithHeaders = await addHeaders({ config: updatedConfig, headersPath, logs, featureFlags });
|
|
21
|
+
const finalConfig = await addRedirects({ config: configWithHeaders, redirectsPath, logs, featureFlags });
|
|
22
|
+
const simplifiedConfig = simplifyConfig(finalConfig);
|
|
23
|
+
await backupConfig({ buildDir, configPath, headersPath, redirectsPath });
|
|
24
|
+
await Promise.all([
|
|
25
|
+
saveConfig(configPath, simplifiedConfig),
|
|
26
|
+
deleteSideFile(headersPath),
|
|
27
|
+
deleteSideFile(redirectsPath),
|
|
28
|
+
]);
|
|
29
|
+
};
|
|
30
|
+
// If `netlify.toml` exists, deeply merges the configuration changes.
|
|
31
|
+
const mergeWithConfig = async function (normalizedInlineConfig, configPath) {
|
|
32
|
+
const config = await parseOptionalConfig(configPath);
|
|
33
|
+
const updatedConfig = mergeConfigs([config, normalizedInlineConfig]);
|
|
34
|
+
return updatedConfig;
|
|
35
|
+
};
|
|
36
|
+
// Serialize the changes to `netlify.toml`
|
|
37
|
+
const saveConfig = async function (configPath, simplifiedConfig) {
|
|
38
|
+
const serializedConfig = serializeToml(simplifiedConfig);
|
|
39
|
+
await fs.writeFile(configPath, serializedConfig);
|
|
40
|
+
};
|
|
41
|
+
// Deletes `_headers/_redirects` since they are merged to `netlify.toml`,
|
|
42
|
+
// to fix any priority problem.
|
|
43
|
+
const deleteSideFile = async function (filePath) {
|
|
44
|
+
if (filePath === undefined || !(await pathExists(filePath))) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
await fs.unlink(filePath);
|
|
48
|
+
};
|
|
49
|
+
// Modifications to `netlify.toml` and `_headers/_redirects` are only meant for
|
|
50
|
+
// the deploy API call. After it's been performed, we restore their former
|
|
51
|
+
// state.
|
|
52
|
+
// We do this by backing them up inside some sibling directory.
|
|
53
|
+
const backupConfig = async function ({ buildDir, configPath, headersPath, redirectsPath }) {
|
|
54
|
+
const tempDir = getTempDir(buildDir);
|
|
55
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
56
|
+
await Promise.all([
|
|
57
|
+
backupFile(configPath, `${tempDir}/netlify.toml`),
|
|
58
|
+
backupFile(headersPath, `${tempDir}/_headers`),
|
|
59
|
+
backupFile(redirectsPath, `${tempDir}/_redirects`),
|
|
60
|
+
]);
|
|
61
|
+
};
|
|
62
|
+
export const restoreConfig = async function (configMutations, { buildDir, configPath, headersPath, redirectsPath }) {
|
|
63
|
+
if (configMutations.length === 0) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const tempDir = getTempDir(buildDir);
|
|
67
|
+
await Promise.all([
|
|
68
|
+
copyOrDelete(`${tempDir}/netlify.toml`, configPath),
|
|
69
|
+
copyOrDelete(`${tempDir}/_headers`, headersPath),
|
|
70
|
+
copyOrDelete(`${tempDir}/_redirects`, redirectsPath),
|
|
71
|
+
]);
|
|
72
|
+
};
|
|
73
|
+
const getTempDir = function (buildDir) {
|
|
74
|
+
return `${buildDir}/.netlify/deploy`;
|
|
75
|
+
};
|
|
76
|
+
const backupFile = async function (original, backup) {
|
|
77
|
+
// this makes sure we don't restore stale files
|
|
78
|
+
await deleteNoError(backup);
|
|
79
|
+
if (!(await pathExists(original))) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
await fs.copyFile(original, backup);
|
|
83
|
+
};
|
|
84
|
+
const deleteNoError = async (path) => {
|
|
85
|
+
try {
|
|
86
|
+
await fs.unlink(path);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// continue regardless error
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const copyOrDelete = async function (src, dest) {
|
|
93
|
+
if (await pathExists(src)) {
|
|
94
|
+
await fs.copyFile(src, dest);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
await deleteNoError(dest);
|
|
98
|
+
};
|
package/lib/normalize.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { normalizeFunctionsProps, WILDCARD_ALL } from './functions_config.js';
|
|
2
|
+
import { mergeConfigs } from './merge.js';
|
|
3
|
+
import { DEFAULT_ORIGIN } from './origin.js';
|
|
4
|
+
import { removeFalsy } from './utils/remove_falsy.js';
|
|
5
|
+
// Normalize configuration object
|
|
6
|
+
export const normalizeConfig = function (config) {
|
|
7
|
+
const configA = removeEmpty(config);
|
|
8
|
+
const { build, functions, plugins, ...configB } = mergeConfigs([DEFAULT_CONFIG, configA]);
|
|
9
|
+
const { build: buildA, functions: functionsA, functionsDirectoryProps } = normalizeFunctionsProps(build, functions);
|
|
10
|
+
const pluginsA = plugins.map(normalizePlugin);
|
|
11
|
+
return { ...configB, build: buildA, functions: functionsA, plugins: pluginsA, ...functionsDirectoryProps };
|
|
12
|
+
};
|
|
13
|
+
// Remove empty strings.
|
|
14
|
+
// This notably ensures that empty strings in the build command are removed.
|
|
15
|
+
// Otherwise those would be run during builds, making the build fail.
|
|
16
|
+
const removeEmpty = function ({ build, ...config }) {
|
|
17
|
+
return removeFalsy({ ...config, build: removeFalsy(build) });
|
|
18
|
+
};
|
|
19
|
+
const DEFAULT_CONFIG = {
|
|
20
|
+
build: {
|
|
21
|
+
environment: {},
|
|
22
|
+
publish: '.',
|
|
23
|
+
publishOrigin: DEFAULT_ORIGIN,
|
|
24
|
+
processing: { css: {}, html: {}, images: {}, js: {} },
|
|
25
|
+
services: {},
|
|
26
|
+
},
|
|
27
|
+
functions: { [WILDCARD_ALL]: {} },
|
|
28
|
+
plugins: [],
|
|
29
|
+
};
|
|
30
|
+
const normalizePlugin = function ({ inputs = {}, ...plugin }) {
|
|
31
|
+
return removeFalsy({ ...plugin, inputs });
|
|
32
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { dirname, relative, sep } from 'path';
|
|
3
|
+
import { pathExists } from 'path-exists';
|
|
4
|
+
// Retrieve `base` override.
|
|
5
|
+
// This uses any directory below `repositoryRoot` and above (or equal to)
|
|
6
|
+
// `cwd` that has a `.netlify` or `netlify.toml`. This allows Netlify CLI users
|
|
7
|
+
// to `cd` into monorepo directories to change their base and build directories.
|
|
8
|
+
// Do all checks in parallel for speed
|
|
9
|
+
export const getBaseOverride = async function ({ repositoryRoot, cwd }) {
|
|
10
|
+
// Performance optimization
|
|
11
|
+
if (repositoryRoot === cwd) {
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
const [repositoryRootA, cwdA] = await Promise.all([fs.realpath(repositoryRoot), fs.realpath(cwd)]);
|
|
15
|
+
const basePaths = getBasePaths(repositoryRootA, cwdA);
|
|
16
|
+
const basePath = await locatePath(basePaths);
|
|
17
|
+
if (basePath === undefined) {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
// `base` starting with a `/` are relative to `repositoryRoot`, so we cannot
|
|
21
|
+
// return an absolute path
|
|
22
|
+
const base = relative(repositoryRoot, dirname(basePath));
|
|
23
|
+
// When `base` is explicitely overridden, `baseRelDir: true` makes more sense
|
|
24
|
+
// since we want `publish`, `functions` and `edge_functions` to be relative to it.
|
|
25
|
+
return { base, baseRelDir: true };
|
|
26
|
+
};
|
|
27
|
+
// Returns list of files to check for the existence of a `base`
|
|
28
|
+
const getBasePaths = function (repositoryRoot, cwd) {
|
|
29
|
+
const subdirs = getSubdirs(repositoryRoot, cwd);
|
|
30
|
+
const basePaths = subdirs.flatMap((subdir) => BASE_FILENAMES.map((filename) => `${subdir}/${filename}`));
|
|
31
|
+
return basePaths;
|
|
32
|
+
};
|
|
33
|
+
// Retrieves list of directories between `repositoryRoot` and `cwd`, including
|
|
34
|
+
// `cwd` but excluding `repositoryRoot`
|
|
35
|
+
const getSubdirs = function (repositoryRoot, dir, subdirs = []) {
|
|
36
|
+
if (!dir.startsWith(`${repositoryRoot}${sep}`)) {
|
|
37
|
+
return subdirs;
|
|
38
|
+
}
|
|
39
|
+
return getSubdirs(repositoryRoot, dirname(dir), [...subdirs, dir]);
|
|
40
|
+
};
|
|
41
|
+
const BASE_FILENAMES = ['.netlify', 'netlify.toml', 'package.json'];
|
|
42
|
+
// Returns the first path that exists.
|
|
43
|
+
// Like `locate-path` library but works with mixed files/directories
|
|
44
|
+
const locatePath = async function (paths) {
|
|
45
|
+
const results = await Promise.all(paths.map(returnIfExists));
|
|
46
|
+
const path = results.find(Boolean);
|
|
47
|
+
return path;
|
|
48
|
+
};
|
|
49
|
+
const returnIfExists = async function (path) {
|
|
50
|
+
if (!(await pathExists(path))) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
return path;
|
|
54
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { execaCommand } from 'execa';
|
|
2
|
+
// Find out git branch among (in priority order):
|
|
3
|
+
// - `branch` option
|
|
4
|
+
// - `BRANCH` environment variable
|
|
5
|
+
// - `HEAD` branch (using `git`)
|
|
6
|
+
// - `main` (using `git`)
|
|
7
|
+
// - 'master' (fallback)
|
|
8
|
+
export const getBranch = async function ({ branch, repositoryRoot }) {
|
|
9
|
+
if (branch) {
|
|
10
|
+
return branch;
|
|
11
|
+
}
|
|
12
|
+
const headBranch = await getGitBranch(repositoryRoot, 'HEAD');
|
|
13
|
+
if (headBranch !== undefined) {
|
|
14
|
+
return headBranch;
|
|
15
|
+
}
|
|
16
|
+
const mainBranch = await getGitBranch(repositoryRoot, 'main');
|
|
17
|
+
if (mainBranch !== undefined) {
|
|
18
|
+
return mainBranch;
|
|
19
|
+
}
|
|
20
|
+
return FALLBACK_BRANCH;
|
|
21
|
+
};
|
|
22
|
+
const getGitBranch = async function (repositoryRoot, gitRef) {
|
|
23
|
+
try {
|
|
24
|
+
const { stdout } = await execaCommand(`git rev-parse --abbrev-ref ${gitRef}`, { cwd: repositoryRoot });
|
|
25
|
+
return stdout;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// continue regardless error
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const FALLBACK_BRANCH = 'master';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// From CLI `--featureFlags=a,b,c` to programmatic `{ a: true, b: true, c: true }`
|
|
2
|
+
export const normalizeCliFeatureFlags = function (cliFeatureFlags) {
|
|
3
|
+
return Object.assign({}, ...cliFeatureFlags.split(',').filter(isNotEmpty).map(getFeatureFlag));
|
|
4
|
+
};
|
|
5
|
+
const isNotEmpty = function (name) {
|
|
6
|
+
return name.trim() !== '';
|
|
7
|
+
};
|
|
8
|
+
const getFeatureFlag = function (name) {
|
|
9
|
+
return { [name]: true };
|
|
10
|
+
};
|
|
11
|
+
// Default values for feature flags
|
|
12
|
+
export const DEFAULT_FEATURE_FLAGS = {};
|