-
Notifications
You must be signed in to change notification settings - Fork 4.3k
/
Copy pathconfig.js
334 lines (303 loc) · 10.6 KB
/
config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
'use strict';
/**
* External dependencies
*/
const fs = require( 'fs' ).promises;
const path = require( 'path' );
const os = require( 'os' );
/**
* Internal dependencies
*/
const detectDirectoryType = require( './detect-directory-type' );
const { validateConfig, ValidationError } = require( './validate-config' );
const readRawConfigFile = require( './read-raw-config-file' );
const parseConfig = require( './parse-config' );
const md5 = require( '../md5' );
/**
* wp-env configuration.
*
* @typedef WPConfig
* @property {string} name Name of the environment.
* @property {string} configDirectoryPath Path to the .wp-env.json file.
* @property {string} workDirectoryPath Path to the work directory located in ~/.wp-env.
* @property {string} dockerComposeConfigPath Path to the docker-compose.yml file.
* @property {boolean} detectedLocalConfig If true, wp-env detected local config and used it.
* @property {Object.<string, WPServiceConfig>} env Specific config for different environments.
* @property {boolean} debug True if debug mode is enabled.
*/
/**
* Base-level config for any particular environment. (development/tests/etc)
*
* @typedef WPServiceConfig
* @property {?WPSource} coreSource The WordPress installation to load in the environment.
* @property {WPSource[]} pluginSources Plugins to load in the environment.
* @property {WPSource[]} themeSources Themes to load in the environment.
* @property {number} port The port to use.
* @property {Object} config Mapping of wp-config.php constants to their desired values.
* @property {Object.<string, WPSource>} mappings Mapping of WordPress directories to local directories which should be mounted.
*/
/**
* A WordPress installation, plugin or theme to be loaded into the environment.
*
* @typedef WPSource
* @property {'local'|'git'|'zip'} type The source type.
* @property {string} path The path to the WordPress installation, plugin or theme.
* @property {?string} url The URL to the source download if the source type is not local.
* @property {?string} ref The git ref for the source if the source type is 'git'.
* @property {string} basename Name that identifies the WordPress installation, plugin or theme.
*/
/**
* Reads, parses, and validates the given .wp-env.json file into a wp-env config
* object for internal use.
*
* @param {string} configPath Path to the .wp-env.json file.
*
* @return {WPConfig} A parsed and validated wp-env config object.
*/
module.exports = async function readConfig( configPath ) {
const configDirectoryPath = path.dirname( configPath );
const workDirectoryPath = path.resolve(
await getHomeDirectory(),
md5( configPath )
);
// Default configuration which is overridden by .wp-env.json files.
const defaultConfiguration = {
core: null,
plugins: [],
themes: [],
port: 8888,
mappings: {},
config: {
WP_DEBUG: true,
SCRIPT_DEBUG: true,
WP_PHP_BINARY: 'php',
WP_TESTS_EMAIL: 'admin@example.org',
WP_TESTS_TITLE: 'Test Blog',
WP_TESTS_DOMAIN: 'http://localhost',
WP_SITEURL: 'http://localhost',
WP_HOME: 'http://localhost',
},
env: {
development: {}, // No overrides needed, but it should exist.
tests: {
config: { WP_DEBUG: false, SCRIPT_DEBUG: false },
port: 8889,
},
},
};
// The specified base configuration from .wp-env.json or from the local
// source type which was automatically detected.
const baseConfig =
( await readRawConfigFile( '.wp-env.json', configPath ) ) ||
( await getDefaultBaseConfig( configPath ) );
// Overriden .wp-env.json on a per-user case.
const overrideConfig =
( await readRawConfigFile(
'.wp-env.override.json',
configPath.replace( /\.wp-env\.json$/, '.wp-env.override.json' )
) ) || {};
const detectedLocalConfig =
Object.keys( { ...baseConfig, ...overrideConfig } ).length > 0;
// A quick validation before merging on a service by service level allows us
// to check the root configuration options and provide more helpful errors.
validateConfig(
mergeWpServiceConfigs( [
defaultConfiguration,
baseConfig,
overrideConfig,
] )
);
// A unique array of the environments specified in the config options.
// Needed so that we can override settings per-environment, rather than
// overwriting each environment key.
const getEnvKeys = ( config ) => Object.keys( config.env || {} );
const allEnvs = [
...new Set( [
...getEnvKeys( defaultConfiguration ),
...getEnvKeys( baseConfig ),
...getEnvKeys( overrideConfig ),
] ),
];
// Returns a pair with the root config options and the specific environment config options.
const getEnvConfig = ( config, envName ) => [
config,
config.env && config.env[ envName ] ? config.env[ envName ] : {},
];
// Merge each of the specified environment-level overrides.
const allPorts = new Set(); // Keep track of unique ports for validation.
const env = allEnvs.reduce( ( result, environment ) => {
result[ environment ] = parseConfig(
validateConfig(
mergeWpServiceConfigs( [
...getEnvConfig( defaultConfiguration, environment ),
...getEnvConfig( baseConfig, environment ),
...getEnvConfig( overrideConfig, environment ),
] ),
environment
),
{
workDirectoryPath,
}
);
allPorts.add( result[ environment ].port );
return result;
}, {} );
if ( allPorts.size !== allEnvs.length ) {
throw new ValidationError(
'Invalid .wp-env.json: Each port value must be unique.'
);
}
return withOverrides( {
name: path.basename( configDirectoryPath ),
dockerComposeConfigPath: path.resolve(
workDirectoryPath,
'docker-compose.yml'
),
configDirectoryPath,
workDirectoryPath,
detectedLocalConfig,
env,
} );
};
/**
* Deep-merges the values in the given service environment. This allows us to
* merge the wp-config.php values instead of overwriting them. Note that this
* merges configs before they have been validated, so the passed config shape
* will not match the WPServiceConfig type.
*
* @param {Object[]} configs Array of raw service config objects to merge.
*
* @return {Object} The merged configuration object.
*/
function mergeWpServiceConfigs( configs ) {
// Returns an array of nested values in the config object. For example,
// an array of all the wp-config objects.
const mergeNestedObjs = ( key ) =>
Object.assign(
{},
...configs.map( ( config ) => {
if ( ! config[ key ] ) {
return {};
} else if ( typeof config[ key ] === 'object' ) {
return config[ key ];
}
throw new ValidationError(
`Invalid .wp-env.json: "${ key }" must be an object.`
);
} )
);
const mergedConfig = {
...Object.assign( {}, ...configs ),
config: mergeNestedObjs( 'config' ),
mappings: mergeNestedObjs( 'mappings' ),
};
delete mergedConfig.env;
return mergedConfig;
}
/**
* Detects basic config options to use if the .wp-env.json config file does not
* exist. For example, if the local directory contains a plugin, that will be
* added to the default plugin sources.
*
* @param {string} configPath A path to the config file for the source to detect.
* @return {Object} Basic config options for the detected source type. Empty
* object if no config detected.
*/
async function getDefaultBaseConfig( configPath ) {
const configDirectoryPath = path.dirname( configPath );
const type = await detectDirectoryType( configDirectoryPath );
if ( type === 'core' ) {
return { core: '.' };
} else if ( type === 'plugin' ) {
return { plugins: [ '.' ] };
} else if ( type === 'theme' ) {
return { themes: [ '.' ] };
}
return {};
}
/**
* Overrides keys in the config object with set environment variables or options
* which should be merged.
*
* @param {WPConfig} config fully parsed configuration object.
* @return {WPConfig} configuration object with overrides applied.
*/
function withOverrides( config ) {
// Override port numbers with environment variables.
config.env.development.port =
getNumberFromEnvVariable( 'WP_ENV_PORT' ) ||
config.env.development.port;
config.env.tests.port =
getNumberFromEnvVariable( 'WP_ENV_TESTS_PORT' ) ||
config.env.tests.port;
const updateEnvUrl = ( configKey ) => {
[ 'development', 'tests' ].forEach( ( envKey ) => {
try {
const baseUrl = new URL(
config.env[ envKey ].config[ configKey ]
);
baseUrl.port = config.env[ envKey ].port;
config.env[ envKey ].config[ configKey ] = baseUrl.toString();
} catch ( error ) {
throw new ValidationError(
`Invalid .wp-env.json: config.${ configKey } must be a valid URL.`
);
}
} );
};
// Update wp config options to include the correct port number in the URL.
updateEnvUrl( 'WP_TESTS_DOMAIN' );
updateEnvUrl( 'WP_SITEURL' );
updateEnvUrl( 'WP_HOME' );
return config;
}
/**
* Parses an environment variable which should be a number.
*
* Throws an error if the variable cannot be parsed to a number.
* Returns null if the environment variable has not been specified.
*
* @param {string} varName The environment variable to check (e.g. WP_ENV_PORT).
* @return {null|number} The number. Null if it does not exist.
*/
function getNumberFromEnvVariable( varName ) {
// Allow use of the default if it does not exist.
if ( ! process.env[ varName ] ) {
return null;
}
const maybeNumber = parseInt( process.env[ varName ] );
// Throw an error if it is not parseable as a number.
if ( isNaN( maybeNumber ) ) {
throw new ValidationError(
`Invalid environment variable: ${ varName } must be a number.`
);
}
return maybeNumber;
}
/**
* Gets the `wp-env` home directory in which generated files are created.
*
* By default: '~/.wp-env/'. On Linux with snap packages: '~/wp-env/'. Can be
* overriden with the WP_ENV_HOME environment variable.
*
* @return {Promise<string>} The absolute path to the `wp-env` home directory.
*/
async function getHomeDirectory() {
// Allow user to override download location.
if ( process.env.WP_ENV_HOME ) {
return path.resolve( process.env.WP_ENV_HOME );
}
/**
* Installing docker with Snap Packages on Linux is common, but does not
* support hidden directories. Therefore we use a public directory when
* snap packages exist.
*
* @see https://github.com/WordPress/gutenberg/issues/20180#issuecomment-587046325
*/
return path.resolve(
os.homedir(),
!! ( await fs.stat( '/snap' ).catch( () => false ) )
? 'wp-env'
: '.wp-env'
);
}