@wordpress/env 11.0.1-next.v.0 → 11.0.1-next.v.202602091733.0

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.
@@ -39,15 +39,21 @@ function isEnvironmentInitialized( config ) {
39
39
  /**
40
40
  * Outputs the status of the wp-env environment.
41
41
  *
42
- * @param {Object} options
43
- * @param {Object} options.spinner A CLI spinner which indicates progress.
44
- * @param {boolean} options.debug True if debug mode is enabled.
45
- * @param {boolean} options.json True to output as JSON.
42
+ * @param {Object} options
43
+ * @param {Object} options.spinner A CLI spinner which indicates progress.
44
+ * @param {boolean} options.debug True if debug mode is enabled.
45
+ * @param {boolean} options.json True to output as JSON.
46
+ * @param {string|null} options.config Path to a custom .wp-env.json configuration file.
46
47
  */
47
- module.exports = async function status( { spinner, debug, json } ) {
48
+ module.exports = async function status( {
49
+ spinner,
50
+ debug,
51
+ json,
52
+ config: customConfigPath,
53
+ } ) {
48
54
  spinner.text = 'Getting environment status.';
49
55
 
50
- const config = await loadConfig( path.resolve( '.' ) );
56
+ const config = await loadConfig( path.resolve( '.' ), customConfigPath );
51
57
 
52
58
  // Check if environment is initialized by looking for runtime-specific files.
53
59
  // We check for these files specifically because the work directory may exist
@@ -70,7 +76,7 @@ module.exports = async function status( { spinner, debug, json } ) {
70
76
  }
71
77
 
72
78
  // Detect and get runtime.
73
- const runtimeName = detectRuntime( config.workDirectoryPath );
79
+ const runtimeName = await detectRuntime( config.workDirectoryPath );
74
80
  const runtime = getRuntime( runtimeName );
75
81
 
76
82
  // Get status from runtime.
@@ -13,12 +13,19 @@ const { getRuntime, detectRuntime } = require( '../runtime' );
13
13
  /**
14
14
  * Stops the development server.
15
15
  *
16
- * @param {Object} options
17
- * @param {Object} options.spinner A CLI spinner which indicates progress.
18
- * @param {boolean} options.debug True if debug mode is enabled.
16
+ * @param {Object} options
17
+ * @param {Object} options.spinner A CLI spinner which indicates progress.
18
+ * @param {boolean} options.debug True if debug mode is enabled.
19
+ * @param {string|null} options.config Path to a custom .wp-env.json configuration file.
19
20
  */
20
- module.exports = async function stop( { spinner, debug } ) {
21
- const config = await loadConfig( path.resolve( '.' ) );
22
- const runtime = getRuntime( detectRuntime( config.workDirectoryPath ) );
21
+ module.exports = async function stop( {
22
+ spinner,
23
+ debug,
24
+ config: customConfigPath,
25
+ } ) {
26
+ const config = await loadConfig( path.resolve( '.' ), customConfigPath );
27
+ const runtime = getRuntime(
28
+ await detectRuntime( config.workDirectoryPath )
29
+ );
23
30
  await runtime.stop( config, { spinner, debug } );
24
31
  };
@@ -23,7 +23,6 @@ const { checkPort, checkVersion, checkString } = require( './validate-config' );
23
23
  * @property {?number} phpmyadminPort An override for the development environment's phpMyAdmin port.
24
24
  * @property {?WPSource} coreSource An override for all environment's coreSource.
25
25
  * @property {?string} phpVersion An override for all environment's PHP version.
26
- * @property {?boolean} multisite An override for if environmen should be multisite.
27
26
  * @property {?Object.<string, string>} lifecycleScripts An override for various lifecycle scripts.
28
27
  */
29
28
 
@@ -69,10 +68,6 @@ module.exports = function getConfigFromEnvironmentVars( cacheDirectoryPath ) {
69
68
  environmentConfig.phpVersion = process.env.WP_ENV_PHP_VERSION;
70
69
  }
71
70
 
72
- if ( process.env.WP_ENV_MULTISITE ) {
73
- environmentConfig.multisite = !! process.env.WP_ENV_MULTISITE;
74
- }
75
-
76
71
  return environmentConfig;
77
72
  };
78
73
 
@@ -108,6 +103,8 @@ function getLifecycleScriptOverrides() {
108
103
  const lifecycleEnvironmentVars = {
109
104
  WP_ENV_LIFECYCLE_SCRIPT_AFTER_START: 'afterStart',
110
105
  WP_ENV_LIFECYCLE_SCRIPT_AFTER_CLEAN: 'afterClean',
106
+ WP_ENV_LIFECYCLE_SCRIPT_AFTER_RESET: 'afterReset',
107
+ WP_ENV_LIFECYCLE_SCRIPT_AFTER_CLEANUP: 'afterCleanup',
111
108
  WP_ENV_LIFECYCLE_SCRIPT_AFTER_DESTROY: 'afterDestroy',
112
109
  };
113
110
  for ( const envVar in lifecycleEnvironmentVars ) {
@@ -11,6 +11,7 @@ const fs = require( 'fs' ).promises;
11
11
  const getCacheDirectory = require( './get-cache-directory' );
12
12
  const md5 = require( '../md5' );
13
13
  const { parseConfig, getConfigFilePath } = require( './parse-config' );
14
+ const { ValidationError } = require( './validate-config' );
14
15
  const postProcessConfig = require( './post-process-config' );
15
16
 
16
17
  /**
@@ -35,12 +36,31 @@ const postProcessConfig = require( './post-process-config' );
35
36
  /**
36
37
  * Loads any configuration from a given directory.
37
38
  *
38
- * @param {string} configDirectoryPath The directory we want to load the config from.
39
+ * @param {string} configDirectoryPath The directory we want to load the config from.
40
+ * @param {string|null} customConfigPath Optional custom config file path.
39
41
  *
40
42
  * @return {Promise<WPConfig>} The config object we've loaded.
41
43
  */
42
- module.exports = async function loadConfig( configDirectoryPath ) {
43
- const configFilePath = getConfigFilePath( configDirectoryPath );
44
+ module.exports = async function loadConfig(
45
+ configDirectoryPath,
46
+ customConfigPath = null
47
+ ) {
48
+ const configFilePath = getConfigFilePath(
49
+ configDirectoryPath,
50
+ 'local',
51
+ customConfigPath
52
+ );
53
+
54
+ // If a custom config path was provided, verify the file exists.
55
+ if ( customConfigPath ) {
56
+ try {
57
+ await fs.stat( configFilePath );
58
+ } catch ( error ) {
59
+ throw new ValidationError(
60
+ `Config file not found: ${ configFilePath }`
61
+ );
62
+ }
63
+ }
44
64
 
45
65
  const cacheDirectoryPath = path.resolve(
46
66
  await getCacheDirectory(),
@@ -49,7 +69,11 @@ module.exports = async function loadConfig( configDirectoryPath ) {
49
69
 
50
70
  // Parse any configuration we found in the given directory.
51
71
  // This comes merged and prepared for internal consumption.
52
- let config = await parseConfig( configDirectoryPath, cacheDirectoryPath );
72
+ let config = await parseConfig(
73
+ configDirectoryPath,
74
+ cacheDirectoryPath,
75
+ customConfigPath
76
+ );
53
77
 
54
78
  // Make sure to perform any additional post-processing that
55
79
  // may be needed before the config object is ready for
@@ -64,9 +88,14 @@ module.exports = async function loadConfig( configDirectoryPath ) {
64
88
  ),
65
89
  configDirectoryPath,
66
90
  workDirectoryPath: cacheDirectoryPath,
91
+ customConfigPath,
67
92
  detectedLocalConfig: await hasLocalConfig( [
68
93
  configFilePath,
69
- getConfigFilePath( configDirectoryPath, 'override' ),
94
+ getConfigFilePath(
95
+ configDirectoryPath,
96
+ 'override',
97
+ customConfigPath
98
+ ),
70
99
  ] ),
71
100
  lifecycleScripts: config.lifecycleScripts,
72
101
  env: config.env,
@@ -111,22 +111,27 @@ const DEFAULT_ENVIRONMENT_CONFIG = {
111
111
  * constructs an object in the format used internally.
112
112
  *
113
113
  *
114
- * @param {string} configDirectoryPath A path to the directory we are parsing the config for.
115
- * @param {string} cacheDirectoryPath Path to the work directory located in ~/.wp-env.
114
+ * @param {string} configDirectoryPath A path to the directory we are parsing the config for.
115
+ * @param {string} cacheDirectoryPath Path to the work directory located in ~/.wp-env.
116
+ * @param {string|null} customConfigPath Optional custom config file path.
116
117
  *
117
118
  * @return {Promise<WPRootConfig>} Parsed config.
118
119
  */
119
- async function parseConfig( configDirectoryPath, cacheDirectoryPath ) {
120
+ async function parseConfig(
121
+ configDirectoryPath,
122
+ cacheDirectoryPath,
123
+ customConfigPath = null
124
+ ) {
120
125
  // The local config will be used to override any defaults.
121
126
  const localConfig = await parseConfigFile(
122
- getConfigFilePath( configDirectoryPath ),
127
+ getConfigFilePath( configDirectoryPath, 'local', customConfigPath ),
123
128
  { cacheDirectoryPath }
124
129
  );
125
130
 
126
131
  // Any overrides that can be used in place
127
132
  // of properties set by the local config.
128
133
  const overrideConfig = await parseConfigFile(
129
- getConfigFilePath( configDirectoryPath, 'override' ),
134
+ getConfigFilePath( configDirectoryPath, 'override', customConfigPath ),
130
135
  { cacheDirectoryPath }
131
136
  );
132
137
 
@@ -161,27 +166,37 @@ async function parseConfig( configDirectoryPath, cacheDirectoryPath ) {
161
166
  /**
162
167
  * Gets the path to the config file.
163
168
  *
164
- * @param {string} configDirectoryPath The path to the directory containing config files.
165
- * @param {string} type The type of config file we're interested in: 'local' or 'override'.
169
+ * @param {string} configDirectoryPath The path to the directory containing config files.
170
+ * @param {string} type The type of config file we're interested in: 'local' or 'override'.
171
+ * @param {string|null} customConfigPath Optional custom config file path (only used for 'local' type).
166
172
  *
167
173
  * @return {string} The path to the config file.
168
174
  */
169
- function getConfigFilePath( configDirectoryPath, type = 'local' ) {
170
- let fileName;
171
- switch ( type ) {
172
- case 'local': {
173
- fileName = '.wp-env.json';
174
- break;
175
- }
175
+ function getConfigFilePath(
176
+ configDirectoryPath,
177
+ type = 'local',
178
+ customConfigPath = null
179
+ ) {
180
+ // If a custom config path is provided for the local config, use it.
181
+ if ( type === 'local' && customConfigPath ) {
182
+ return path.resolve( customConfigPath );
183
+ }
176
184
 
177
- case 'override': {
178
- fileName = '.wp-env.override.json';
179
- break;
180
- }
185
+ // For override, derive from custom config: staging.json -> staging.override.json
186
+ if ( type === 'override' && customConfigPath ) {
187
+ const resolved = path.resolve( customConfigPath );
188
+ const ext = path.extname( resolved );
189
+ const base = path.basename( resolved, ext );
190
+ const dir = path.dirname( resolved );
191
+ return path.join( dir, `${ base }.override${ ext }` );
192
+ }
181
193
 
182
- default: {
183
- throw new Error( `Invalid config file type "${ type }.` );
184
- }
194
+ // Default behavior.
195
+ const fileName =
196
+ type === 'local' ? '.wp-env.json' : '.wp-env.override.json';
197
+
198
+ if ( type !== 'local' && type !== 'override' ) {
199
+ throw new Error( `Invalid config file type "${ type }.` );
185
200
  }
186
201
 
187
202
  return path.resolve( configDirectoryPath, fileName );
@@ -235,6 +250,8 @@ async function getDefaultConfig(
235
250
  lifecycleScripts: {
236
251
  afterStart: null,
237
252
  afterClean: null,
253
+ afterReset: null,
254
+ afterCleanup: null,
238
255
  afterDestroy: null,
239
256
  },
240
257
  env: {
@@ -312,6 +329,12 @@ function getEnvironmentVarOverrides( cacheDirectoryPath ) {
312
329
  overrideConfig.env.tests.phpVersion = overrides.phpVersion;
313
330
  }
314
331
 
332
+ if ( overrides.multisite ) {
333
+ overrideConfig.multisite = overrides.multisite;
334
+ overrideConfig.env.development.multisite = overrides.multisite;
335
+ overrideConfig.env.tests.multisite = overrides.multisite;
336
+ }
337
+
315
338
  return overrideConfig;
316
339
  }
317
340
 
@@ -3,6 +3,7 @@
3
3
  exports[`Config Integration should load local and override configuration files 1`] = `
4
4
  {
5
5
  "configDirectoryPath": "/test/gutenberg",
6
+ "customConfigPath": null,
6
7
  "detectedLocalConfig": true,
7
8
  "dockerComposeConfigPath": "/cache/5fea4c5689ef6cc4a4e6eaaa39323338/docker-compose.yml",
8
9
  "env": {
@@ -71,7 +72,9 @@ exports[`Config Integration should load local and override configuration files 1
71
72
  },
72
73
  "lifecycleScripts": {
73
74
  "afterClean": null,
75
+ "afterCleanup": null,
74
76
  "afterDestroy": "test",
77
+ "afterReset": null,
75
78
  "afterStart": null,
76
79
  },
77
80
  "name": "gutenberg",
@@ -82,6 +85,7 @@ exports[`Config Integration should load local and override configuration files 1
82
85
  exports[`Config Integration should load local configuration file 1`] = `
83
86
  {
84
87
  "configDirectoryPath": "/test/gutenberg",
88
+ "customConfigPath": null,
85
89
  "detectedLocalConfig": true,
86
90
  "dockerComposeConfigPath": "/cache/5fea4c5689ef6cc4a4e6eaaa39323338/docker-compose.yml",
87
91
  "env": {
@@ -150,7 +154,9 @@ exports[`Config Integration should load local configuration file 1`] = `
150
154
  },
151
155
  "lifecycleScripts": {
152
156
  "afterClean": null,
157
+ "afterCleanup": null,
153
158
  "afterDestroy": null,
159
+ "afterReset": null,
154
160
  "afterStart": "test",
155
161
  },
156
162
  "name": "gutenberg",
@@ -161,6 +167,7 @@ exports[`Config Integration should load local configuration file 1`] = `
161
167
  exports[`Config Integration should use default configuration 1`] = `
162
168
  {
163
169
  "configDirectoryPath": "/test/gutenberg",
170
+ "customConfigPath": null,
164
171
  "detectedLocalConfig": true,
165
172
  "dockerComposeConfigPath": "/cache/5fea4c5689ef6cc4a4e6eaaa39323338/docker-compose.yml",
166
173
  "env": {
@@ -229,7 +236,9 @@ exports[`Config Integration should use default configuration 1`] = `
229
236
  },
230
237
  "lifecycleScripts": {
231
238
  "afterClean": null,
239
+ "afterCleanup": null,
232
240
  "afterDestroy": null,
241
+ "afterReset": null,
233
242
  "afterStart": null,
234
243
  },
235
244
  "name": "gutenberg",
@@ -240,6 +249,7 @@ exports[`Config Integration should use default configuration 1`] = `
240
249
  exports[`Config Integration should use environment variables over local and override configuration files 1`] = `
241
250
  {
242
251
  "configDirectoryPath": "/test/gutenberg",
252
+ "customConfigPath": null,
243
253
  "detectedLocalConfig": true,
244
254
  "dockerComposeConfigPath": "/cache/5fea4c5689ef6cc4a4e6eaaa39323338/docker-compose.yml",
245
255
  "env": {
@@ -310,7 +320,9 @@ exports[`Config Integration should use environment variables over local and over
310
320
  },
311
321
  "lifecycleScripts": {
312
322
  "afterClean": null,
323
+ "afterCleanup": null,
313
324
  "afterDestroy": null,
325
+ "afterReset": null,
314
326
  "afterStart": "test",
315
327
  },
316
328
  "name": "gutenberg",
@@ -52,6 +52,8 @@ const DEFAULT_CONFIG = {
52
52
  lifecycleScripts: {
53
53
  afterStart: null,
54
54
  afterClean: null,
55
+ afterReset: null,
56
+ afterCleanup: null,
55
57
  afterDestroy: null,
56
58
  },
57
59
  env: {
@@ -181,6 +181,19 @@ module.exports = function buildDockerComposeConfig( config ) {
181
181
  config.env.tests.phpmyadminPort ?? ''
182
182
  }}:80`;
183
183
 
184
+ // MySQL healthcheck using MariaDB's official healthcheck.sh script.
185
+ // --connect: verifies TCP connection and that entrypoint has finished
186
+ // --innodb_initialized: ensures InnoDB storage engine is fully initialized
187
+ // MARIADB_AUTO_UPGRADE env var ensures healthcheck user exists for existing installations.
188
+ // Timing is generous to support slow CI environments.
189
+ const mysqlHealthcheck = {
190
+ test: [ 'CMD', 'healthcheck.sh', '--connect', '--innodb_initialized' ],
191
+ interval: '5s',
192
+ timeout: '10s',
193
+ retries: 12,
194
+ start_period: '60s',
195
+ };
196
+
184
197
  return {
185
198
  services: {
186
199
  mysql: {
@@ -191,8 +204,11 @@ module.exports = function buildDockerComposeConfig( config ) {
191
204
  MYSQL_ROOT_PASSWORD:
192
205
  dbEnv.credentials.WORDPRESS_DB_PASSWORD,
193
206
  MYSQL_DATABASE: dbEnv.development.WORDPRESS_DB_NAME,
207
+ // Ensures healthcheck user is created for existing installations.
208
+ MARIADB_AUTO_UPGRADE: '1',
194
209
  },
195
210
  volumes: [ 'mysql:/var/lib/mysql' ],
211
+ healthcheck: mysqlHealthcheck,
196
212
  },
197
213
  'tests-mysql': {
198
214
  image: 'mariadb:lts',
@@ -202,11 +218,18 @@ module.exports = function buildDockerComposeConfig( config ) {
202
218
  MYSQL_ROOT_PASSWORD:
203
219
  dbEnv.credentials.WORDPRESS_DB_PASSWORD,
204
220
  MYSQL_DATABASE: dbEnv.tests.WORDPRESS_DB_NAME,
221
+ // Ensures healthcheck user is created for existing installations.
222
+ MARIADB_AUTO_UPGRADE: '1',
205
223
  },
206
224
  volumes: [ 'mysql-test:/var/lib/mysql' ],
225
+ healthcheck: mysqlHealthcheck,
207
226
  },
208
227
  wordpress: {
209
- depends_on: [ 'mysql' ],
228
+ depends_on: {
229
+ mysql: {
230
+ condition: 'service_healthy',
231
+ },
232
+ },
210
233
  build: {
211
234
  context: '.',
212
235
  dockerfile: 'WordPress.Dockerfile',
@@ -224,7 +247,11 @@ module.exports = function buildDockerComposeConfig( config ) {
224
247
  extra_hosts: [ 'host.docker.internal:host-gateway' ],
225
248
  },
226
249
  'tests-wordpress': {
227
- depends_on: [ 'tests-mysql' ],
250
+ depends_on: {
251
+ 'tests-mysql': {
252
+ condition: 'service_healthy',
253
+ },
254
+ },
228
255
  build: {
229
256
  context: '.',
230
257
  dockerfile: 'Tests-WordPress.Dockerfile',
@@ -11,7 +11,6 @@ const { rimraf } = require( 'rimraf' );
11
11
  /**
12
12
  * Promisified dependencies
13
13
  */
14
- const sleep = util.promisify( setTimeout );
15
14
  const exec = util.promisify( require( 'child_process' ).exec );
16
15
 
17
16
  /**
@@ -26,7 +25,6 @@ const {
26
25
  validateRunContainer,
27
26
  } = require( './validate-run-container' );
28
27
  const {
29
- checkDatabaseConnection,
30
28
  configureWordPress,
31
29
  resetDatabase,
32
30
  setupWordPressDirectories,
@@ -109,6 +107,7 @@ class DockerRuntime {
109
107
  xdebug,
110
108
  spx,
111
109
  writeChanges: true,
110
+ customConfigPath: config.customConfigPath,
112
111
  } );
113
112
 
114
113
  // Check if the hash of the config has changed. If so, run configuration.
@@ -174,7 +173,7 @@ class DockerRuntime {
174
173
  }
175
174
 
176
175
  await Promise.all( [
177
- dockerCompose.upOne( 'mysql', {
176
+ dockerCompose.upMany( [ 'mysql', 'tests-mysql' ], {
178
177
  ...dockerComposeConfig,
179
178
  commandOptions: shouldConfigureWp
180
179
  ? [ '--build', '--force-recreate' ]
@@ -250,19 +249,6 @@ class DockerRuntime {
250
249
  if ( shouldConfigureWp ) {
251
250
  spinner.text = 'Configuring WordPress.';
252
251
 
253
- try {
254
- await checkDatabaseConnection( fullConfig );
255
- } catch ( error ) {
256
- // Wait 30 seconds for MySQL to accept connections.
257
- await retry( () => checkDatabaseConnection( fullConfig ), {
258
- times: 30,
259
- delay: 1000,
260
- } );
261
-
262
- // It takes 3-4 seconds for MySQL to be ready after it starts accepting connections.
263
- await sleep( 4000 );
264
- }
265
-
266
252
  // Retry WordPress installation in case MySQL *still* wasn't ready.
267
253
  await Promise.all( [
268
254
  retry(
@@ -369,6 +355,15 @@ class DockerRuntime {
369
355
  return 'WARNING! This will remove Docker containers, volumes, networks, and images associated with the WordPress instance.';
370
356
  }
371
357
 
358
+ /**
359
+ * Get the warning message for cleanup confirmation.
360
+ *
361
+ * @return {string} Warning message.
362
+ */
363
+ getCleanupWarningMessage() {
364
+ return 'WARNING! This will remove Docker containers, volumes, networks, and local files associated with the WordPress instance. Docker images will be preserved.';
365
+ }
366
+
372
367
  /**
373
368
  * Stop the Docker containers.
374
369
  *
@@ -381,6 +376,7 @@ class DockerRuntime {
381
376
  const { dockerComposeConfigPath } = await initConfig( {
382
377
  spinner,
383
378
  debug,
379
+ customConfigPath: config.customConfigPath,
384
380
  } );
385
381
 
386
382
  spinner.text = 'Stopping WordPress.';
@@ -413,7 +409,8 @@ class DockerRuntime {
413
409
  spinner.text = 'Removing local files.';
414
410
  // Note: there is a race condition where docker compose actually hasn't finished
415
411
  // by this point, which causes rimraf to fail. We need to wait at least 2.5-5s,
416
- // but using 10s in case it's dependant on the machine.
412
+ // but using 10s in case it's dependent on the machine. Removing images takes
413
+ // longer so we use a longer wait time here.
417
414
  await new Promise( ( resolve ) => setTimeout( resolve, 10000 ) );
418
415
  await rimraf( config.workDirectoryPath );
419
416
 
@@ -421,27 +418,68 @@ class DockerRuntime {
421
418
  }
422
419
 
423
420
  /**
424
- * Clean/reset the WordPress database.
421
+ * Cleanup the Docker containers and remove local files, but preserve images.
422
+ *
423
+ * @param {WPConfig} config The wp-env config object.
424
+ * @param {Object} options Cleanup options.
425
+ * @param {Object} options.spinner A CLI spinner which indicates progress.
426
+ * @param {boolean} options.debug True if debug mode is enabled.
427
+ */
428
+ async cleanup( config, { spinner, debug } ) {
429
+ spinner.text = 'Removing docker containers, volumes, and networks.';
430
+
431
+ await dockerCompose.down( {
432
+ config: config.dockerComposeConfigPath,
433
+ commandOptions: [ '--volumes', '--remove-orphans' ],
434
+ log: debug,
435
+ } );
436
+
437
+ spinner.text = 'Removing local files.';
438
+ // Note: there is a race condition where docker compose actually hasn't finished
439
+ // by this point, which causes rimraf to fail. We need to wait at least 2.5-5s,
440
+ // but since we're not removing images, the wait can be shorter.
441
+ await new Promise( ( resolve ) => setTimeout( resolve, 3000 ) );
442
+ await rimraf( config.workDirectoryPath );
443
+
444
+ spinner.text = 'Cleaned up WordPress environment.';
445
+ }
446
+
447
+ /**
448
+ * Reset the WordPress database.
425
449
  *
426
450
  * @param {WPConfig} config The wp-env config object.
427
- * @param {Object} options Clean options.
428
- * @param {string} options.environment The environment to clean.
451
+ * @param {Object} options Reset options.
452
+ * @param {string} options.environment The environment to reset.
429
453
  * @param {Object} options.spinner A CLI spinner which indicates progress.
430
454
  * @param {boolean} options.debug True if debug mode is enabled.
431
455
  */
432
456
  async clean( config, { environment, spinner, debug } ) {
433
- const fullConfig = await initConfig( { spinner, debug } );
457
+ const fullConfig = await initConfig( {
458
+ spinner,
459
+ debug,
460
+ customConfigPath: config.customConfigPath,
461
+ } );
434
462
 
435
463
  const description = `${ environment } environment${
436
464
  environment === 'all' ? 's' : ''
437
465
  }`;
438
- spinner.text = `Cleaning ${ description }.`;
466
+ spinner.text = `Resetting ${ description }.`;
439
467
 
440
468
  const tasks = [];
441
469
 
442
- // Start the database first to avoid race conditions where all tasks create
443
- // different docker networks with the same name.
444
- await dockerCompose.upOne( 'mysql', {
470
+ // Start the appropriate MySQL service(s) first to avoid race conditions
471
+ // where parallel tasks try to create docker networks with the same name.
472
+ // The dependency chain (cli -> wordpress -> mysql with service_healthy)
473
+ // ensures MySQL is ready before database operations run.
474
+ const mysqlServices = [];
475
+ if ( environment === 'all' || environment === 'development' ) {
476
+ mysqlServices.push( 'mysql' );
477
+ }
478
+ if ( environment === 'all' || environment === 'tests' ) {
479
+ mysqlServices.push( 'tests-mysql' );
480
+ }
481
+
482
+ await dockerCompose.upMany( mysqlServices, {
445
483
  config: fullConfig.dockerComposeConfigPath,
446
484
  log: fullConfig.debug,
447
485
  } );
@@ -466,7 +504,7 @@ class DockerRuntime {
466
504
 
467
505
  await Promise.all( tasks );
468
506
 
469
- spinner.text = `Cleaned ${ description }.`;
507
+ spinner.text = `Reset ${ description }.`;
470
508
  }
471
509
 
472
510
  /**
@@ -493,7 +531,11 @@ class DockerRuntime {
493
531
  // Validate the container name (throws for deprecated containers)
494
532
  validateRunContainer( container );
495
533
 
496
- const fullConfig = await initConfig( { spinner, debug } );
534
+ const fullConfig = await initConfig( {
535
+ spinner,
536
+ debug,
537
+ customConfigPath: config.customConfigPath,
538
+ } );
497
539
 
498
540
  // Shows a contextual tip for the given command.
499
541
  const joinedCommand = command.join( ' ' );
@@ -520,7 +562,11 @@ class DockerRuntime {
520
562
  * @param {boolean} options.debug True if debug mode is enabled.
521
563
  */
522
564
  async logs( config, { environment, watch, spinner, debug } ) {
523
- const fullConfig = await initConfig( { spinner, debug } );
565
+ const fullConfig = await initConfig( {
566
+ spinner,
567
+ debug,
568
+ customConfigPath: config.customConfigPath,
569
+ } );
524
570
 
525
571
  // If we show text while watching the logs, it will continue showing up every
526
572
  // few lines in the logs as they happen, which isn't a good look. So only
@@ -589,7 +635,11 @@ class DockerRuntime {
589
635
  async getStatus( config, { spinner, debug } ) {
590
636
  spinner.text = 'Getting environment status.';
591
637
 
592
- const fullConfig = await initConfig( { spinner, debug } );
638
+ const fullConfig = await initConfig( {
639
+ spinner,
640
+ debug,
641
+ customConfigPath: config.customConfigPath,
642
+ } );
593
643
  const dockerComposeConfig = {
594
644
  config: fullConfig.dockerComposeConfigPath,
595
645
  log: debug,
@@ -22,16 +22,17 @@ const buildDockerComposeConfig = require( './build-docker-compose-config' );
22
22
  * ./.wp-env.json, creates ~/.wp-env, ~/.wp-env/docker-compose.yml, and
23
23
  * ~/.wp-env/Dockerfile.
24
24
  *
25
- * @param {Object} options
26
- * @param {Object} options.spinner A CLI spinner which indicates progress.
27
- * @param {boolean} options.debug True if debug mode is enabled.
28
- * @param {string} options.xdebug The Xdebug mode to set. Defaults to "off".
29
- * @param {string} options.spx The SPX mode to set. Defaults to "off".
30
- * @param {boolean} options.writeChanges If true, writes the parsed config to the
31
- * required docker files like docker-compose
32
- * and Dockerfile. By default, this is false
33
- * and only the `start` command writes any
34
- * changes.
25
+ * @param {Object} options
26
+ * @param {Object} options.spinner A CLI spinner which indicates progress.
27
+ * @param {boolean} options.debug True if debug mode is enabled.
28
+ * @param {string} options.xdebug The Xdebug mode to set. Defaults to "off".
29
+ * @param {string} options.spx The SPX mode to set. Defaults to "off".
30
+ * @param {boolean} options.writeChanges If true, writes the parsed config to the
31
+ * required docker files like docker-compose
32
+ * and Dockerfile. By default, this is false
33
+ * and only the `start` command writes any
34
+ * changes.
35
+ * @param {string|null} options.customConfigPath Path to a custom .wp-env.json configuration file.
35
36
  * @return {WPConfig} The-env config object.
36
37
  */
37
38
  module.exports = async function initConfig( {
@@ -40,8 +41,9 @@ module.exports = async function initConfig( {
40
41
  xdebug = 'off',
41
42
  spx = 'off',
42
43
  writeChanges = false,
44
+ customConfigPath = null,
43
45
  } ) {
44
- const config = await loadConfig( path.resolve( '.' ) );
46
+ const config = await loadConfig( path.resolve( '.' ), customConfigPath );
45
47
  config.debug = debug;
46
48
 
47
49
  // Adding this to the config allows the start command to understand that the