@wordpress/env 10.38.1-next.v.0 → 11.0.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.
Files changed (37) hide show
  1. package/README.md +158 -75
  2. package/lib/cli.js +85 -24
  3. package/lib/commands/clean.js +16 -38
  4. package/lib/commands/cleanup.js +79 -0
  5. package/lib/commands/destroy.js +33 -41
  6. package/lib/commands/index.js +6 -2
  7. package/lib/commands/logs.js +20 -62
  8. package/lib/commands/reset.js +46 -0
  9. package/lib/commands/run.js +16 -117
  10. package/lib/commands/start.js +78 -243
  11. package/lib/commands/status.js +160 -0
  12. package/lib/commands/stop.js +17 -19
  13. package/lib/config/get-config-from-environment-vars.js +2 -5
  14. package/lib/config/load-config.js +35 -5
  15. package/lib/config/parse-config.js +53 -21
  16. package/lib/config/post-process-config.js +38 -16
  17. package/lib/config/test/__snapshots__/config-integration.js.snap +16 -0
  18. package/lib/config/test/parse-config.js +33 -0
  19. package/lib/config/test/post-process-config.js +52 -0
  20. package/lib/download-sources.js +9 -53
  21. package/lib/{build-docker-compose-config.js → runtime/docker/build-docker-compose-config.js} +167 -128
  22. package/lib/runtime/docker/download-sources.js +63 -0
  23. package/lib/{download-wp-phpunit.js → runtime/docker/download-wp-phpunit.js} +4 -1
  24. package/lib/runtime/docker/index.js +863 -0
  25. package/lib/{init-config.js → runtime/docker/init-config.js} +22 -14
  26. package/lib/runtime/docker/wordpress.js +309 -0
  27. package/lib/runtime/errors.js +28 -0
  28. package/lib/runtime/index.js +91 -0
  29. package/lib/runtime/playground/blueprint-builder.js +158 -0
  30. package/lib/runtime/playground/index.js +530 -0
  31. package/lib/test/build-docker-compose-config.js +150 -3
  32. package/lib/test/cli.js +49 -13
  33. package/lib/wordpress.js +1 -297
  34. package/package.json +6 -3
  35. package/lib/commands/install-path.js +0 -21
  36. /package/lib/{get-host-user.js → runtime/docker/get-host-user.js} +0 -0
  37. /package/lib/{validate-run-container.js → runtime/docker/validate-run-container.js} +0 -0
@@ -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
 
@@ -359,6 +382,14 @@ async function parseRootConfig( configFile, rawConfig, options ) {
359
382
  checkPort( configFile, `testsPort`, rawConfig.testsPort );
360
383
  parsedConfig.testsPort = rawConfig.testsPort;
361
384
  }
385
+ if ( rawConfig.testsEnvironment !== undefined ) {
386
+ if ( typeof rawConfig.testsEnvironment !== 'boolean' ) {
387
+ throw new ValidationError(
388
+ `Invalid ${ configFile }: "testsEnvironment" must be a boolean.`
389
+ );
390
+ }
391
+ parsedConfig.testsEnvironment = rawConfig.testsEnvironment;
392
+ }
362
393
  parsedConfig.lifecycleScripts = {};
363
394
  if ( rawConfig.lifecycleScripts ) {
364
395
  checkObjectWithValues(
@@ -436,6 +467,7 @@ async function parseEnvironmentConfig(
436
467
  // configuration options that we will parse.
437
468
  switch ( key ) {
438
469
  case 'testsPort':
470
+ case 'testsEnvironment':
439
471
  case 'lifecycleScripts':
440
472
  case 'env': {
441
473
  if ( options.rootConfig ) {
@@ -39,25 +39,32 @@ module.exports = function postProcessConfig( config ) {
39
39
  * @return {WPRootConfig} The config object with the root options merged together with the environment-specific options.
40
40
  */
41
41
  function mergeRootToEnvironments( config ) {
42
+ const testsDisabled = config.testsEnvironment === false;
43
+
42
44
  // Some root-level options need to be handled early because they have a special
43
45
  // cascade behavior that would break the normal merge. After merging we then
44
46
  // delete them to avoid that breakage and add them back before we return.
45
47
  const removedRootOptions = {};
46
- if (
47
- config.port !== undefined &&
48
- config.env.development.port === undefined
49
- ) {
50
- removedRootOptions.port = config.port;
51
- config.env.development.port = config.port;
52
- delete config.port;
53
- }
54
- if (
55
- config.testsPort !== undefined &&
56
- config.env.tests.port === undefined
57
- ) {
58
- removedRootOptions.testsPort = config.testsPort;
59
- config.env.tests.port = config.testsPort;
60
- delete config.testsPort;
48
+
49
+ // When tests are disabled, the env key is ignored entirely since there
50
+ // is only one environment. All config should be at the root level.
51
+ if ( ! testsDisabled ) {
52
+ if (
53
+ config.port !== undefined &&
54
+ config.env.development.port === undefined
55
+ ) {
56
+ removedRootOptions.port = config.port;
57
+ config.env.development.port = config.port;
58
+ delete config.port;
59
+ }
60
+ if (
61
+ config.testsPort !== undefined &&
62
+ config.env.tests.port === undefined
63
+ ) {
64
+ removedRootOptions.testsPort = config.testsPort;
65
+ config.env.tests.port = config.testsPort;
66
+ delete config.testsPort;
67
+ }
61
68
  }
62
69
  if ( config.lifecycleScripts !== undefined ) {
63
70
  removedRootOptions.lifecycleScripts = config.lifecycleScripts;
@@ -67,9 +74,15 @@ function mergeRootToEnvironments( config ) {
67
74
  // Merge the root config and the environment configs together so that
68
75
  // we can ignore the root config and have full environment configs.
69
76
  for ( const env in config.env ) {
77
+ // Skip merging root options into the tests environment when it's disabled.
78
+ if ( env === 'tests' && testsDisabled ) {
79
+ continue;
80
+ }
70
81
  config.env[ env ] = mergeConfigs(
71
82
  deepCopyRootOptions( config ),
72
- config.env[ env ]
83
+ // When tests are disabled, ignore env overrides — all config
84
+ // should be specified at the root level.
85
+ testsDisabled ? {} : config.env[ env ]
73
86
  );
74
87
  }
75
88
 
@@ -89,12 +102,16 @@ function mergeRootToEnvironments( config ) {
89
102
  * @return {WPRootConfig} The config after post-processing.
90
103
  */
91
104
  function appendPortToWPConfigs( config ) {
105
+ const testsDisabled = config.testsEnvironment === false;
92
106
  const options = [ 'WP_TESTS_DOMAIN', 'WP_SITEURL', 'WP_HOME' ];
93
107
 
94
108
  // We are only interested in editing the config options for environment-specific configs.
95
109
  // If we made this change to the root config it would cause problems since they would
96
110
  // be mapped to all environments even though the ports will be different.
97
111
  for ( const env in config.env ) {
112
+ if ( env === 'tests' && testsDisabled ) {
113
+ continue;
114
+ }
98
115
  // There's nothing to do without any wp-config options set.
99
116
  if ( config.env[ env ].config === undefined ) {
100
117
  continue;
@@ -128,6 +145,8 @@ function appendPortToWPConfigs( config ) {
128
145
  * @param {WPRootConfig} config The config to process.
129
146
  */
130
147
  function validatePortUniqueness( config ) {
148
+ const testsDisabled = config.testsEnvironment === false;
149
+
131
150
  // We're going to build a map of the environments and their port
132
151
  // so we can accommodate root-level config options more easily.
133
152
  const environmentPorts = {};
@@ -135,6 +154,9 @@ function validatePortUniqueness( config ) {
135
154
  // Add all of the environments to the map. This will
136
155
  // overwrite any root-level options if necessary.
137
156
  for ( const env in config.env ) {
157
+ if ( env === 'tests' && testsDisabled ) {
158
+ continue;
159
+ }
138
160
  if ( config.env[ env ].port === undefined ) {
139
161
  throw new ValidationError(
140
162
  `The "${ env }" environment has an invalid port.`
@@ -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,10 +72,13 @@ 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",
81
+ "testsEnvironment": true,
78
82
  "workDirectoryPath": "/cache/5fea4c5689ef6cc4a4e6eaaa39323338",
79
83
  }
80
84
  `;
@@ -82,6 +86,7 @@ exports[`Config Integration should load local and override configuration files 1
82
86
  exports[`Config Integration should load local configuration file 1`] = `
83
87
  {
84
88
  "configDirectoryPath": "/test/gutenberg",
89
+ "customConfigPath": null,
85
90
  "detectedLocalConfig": true,
86
91
  "dockerComposeConfigPath": "/cache/5fea4c5689ef6cc4a4e6eaaa39323338/docker-compose.yml",
87
92
  "env": {
@@ -150,10 +155,13 @@ exports[`Config Integration should load local configuration file 1`] = `
150
155
  },
151
156
  "lifecycleScripts": {
152
157
  "afterClean": null,
158
+ "afterCleanup": null,
153
159
  "afterDestroy": null,
160
+ "afterReset": null,
154
161
  "afterStart": "test",
155
162
  },
156
163
  "name": "gutenberg",
164
+ "testsEnvironment": true,
157
165
  "workDirectoryPath": "/cache/5fea4c5689ef6cc4a4e6eaaa39323338",
158
166
  }
159
167
  `;
@@ -161,6 +169,7 @@ exports[`Config Integration should load local configuration file 1`] = `
161
169
  exports[`Config Integration should use default configuration 1`] = `
162
170
  {
163
171
  "configDirectoryPath": "/test/gutenberg",
172
+ "customConfigPath": null,
164
173
  "detectedLocalConfig": true,
165
174
  "dockerComposeConfigPath": "/cache/5fea4c5689ef6cc4a4e6eaaa39323338/docker-compose.yml",
166
175
  "env": {
@@ -229,10 +238,13 @@ exports[`Config Integration should use default configuration 1`] = `
229
238
  },
230
239
  "lifecycleScripts": {
231
240
  "afterClean": null,
241
+ "afterCleanup": null,
232
242
  "afterDestroy": null,
243
+ "afterReset": null,
233
244
  "afterStart": null,
234
245
  },
235
246
  "name": "gutenberg",
247
+ "testsEnvironment": true,
236
248
  "workDirectoryPath": "/cache/5fea4c5689ef6cc4a4e6eaaa39323338",
237
249
  }
238
250
  `;
@@ -240,6 +252,7 @@ exports[`Config Integration should use default configuration 1`] = `
240
252
  exports[`Config Integration should use environment variables over local and override configuration files 1`] = `
241
253
  {
242
254
  "configDirectoryPath": "/test/gutenberg",
255
+ "customConfigPath": null,
243
256
  "detectedLocalConfig": true,
244
257
  "dockerComposeConfigPath": "/cache/5fea4c5689ef6cc4a4e6eaaa39323338/docker-compose.yml",
245
258
  "env": {
@@ -310,10 +323,13 @@ exports[`Config Integration should use environment variables over local and over
310
323
  },
311
324
  "lifecycleScripts": {
312
325
  "afterClean": null,
326
+ "afterCleanup": null,
313
327
  "afterDestroy": null,
328
+ "afterReset": null,
314
329
  "afterStart": "test",
315
330
  },
316
331
  "name": "gutenberg",
332
+ "testsEnvironment": true,
317
333
  "workDirectoryPath": "/cache/5fea4c5689ef6cc4a4e6eaaa39323338",
318
334
  }
319
335
  `;
@@ -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: {
@@ -455,4 +457,35 @@ describe( 'parseConfig', () => {
455
457
  };
456
458
  expect( parsed.env ).toEqual( expected );
457
459
  } );
460
+
461
+ it( 'should accept testsEnvironment as a boolean', async () => {
462
+ readRawConfigFile.mockImplementation( async ( configFile ) => {
463
+ if ( configFile === '/test/gutenberg/.wp-env.json' ) {
464
+ return {
465
+ testsEnvironment: false,
466
+ };
467
+ }
468
+ } );
469
+
470
+ const parsed = await parseConfig( '/test/gutenberg', '/cache' );
471
+ expect( parsed.testsEnvironment ).toEqual( false );
472
+ } );
473
+
474
+ it( 'throws for non-boolean testsEnvironment', async () => {
475
+ readRawConfigFile.mockImplementation( async ( configFile ) => {
476
+ if ( configFile === '/test/gutenberg/.wp-env.json' ) {
477
+ return {
478
+ testsEnvironment: 'false',
479
+ };
480
+ }
481
+ } );
482
+
483
+ await expect(
484
+ parseConfig( '/test/gutenberg', '/cache' )
485
+ ).rejects.toEqual(
486
+ new ValidationError(
487
+ `Invalid /test/gutenberg/.wp-env.json: "testsEnvironment" must be a boolean.`
488
+ )
489
+ );
490
+ } );
458
491
  } );
@@ -295,5 +295,57 @@ describe( 'postProcessConfig', () => {
295
295
  )
296
296
  );
297
297
  } );
298
+
299
+ it( 'should skip port validation for disabled tests environment', () => {
300
+ expect( () => {
301
+ postProcessConfig( {
302
+ testsEnvironment: false,
303
+ port: 123,
304
+ env: {
305
+ development: {},
306
+ tests: {},
307
+ },
308
+ } );
309
+ } ).not.toThrow();
310
+ } );
311
+ } );
312
+
313
+ describe( 'testsEnvironment', () => {
314
+ it( 'should ignore env overrides entirely when testsEnvironment is false', () => {
315
+ const processed = postProcessConfig( {
316
+ testsEnvironment: false,
317
+ port: 123,
318
+ testsPort: 456,
319
+ coreSource: {
320
+ type: 'test',
321
+ },
322
+ config: {
323
+ TESTS_ROOT: 'root',
324
+ },
325
+ pluginSources: [
326
+ {
327
+ type: 'root-plugin',
328
+ },
329
+ ],
330
+ env: {
331
+ development: {
332
+ config: {
333
+ TEST_ENV: 'development',
334
+ },
335
+ },
336
+ tests: {},
337
+ },
338
+ } );
339
+
340
+ // Development should get root options but NOT env overrides.
341
+ expect( processed.env.development.port ).toEqual( 123 );
342
+ expect( processed.env.development.config.TESTS_ROOT ).toEqual(
343
+ 'root'
344
+ );
345
+ expect( processed.env.development.config.TEST_ENV ).toBeUndefined();
346
+
347
+ // Tests should not get root options merged.
348
+ expect( processed.env.tests ).toEqual( {} );
349
+ } );
298
350
  } );
299
351
  } );
@@ -2,11 +2,11 @@
2
2
  /**
3
3
  * External dependencies
4
4
  */
5
- const util = require( 'util' );
6
- const SimpleGit = require( 'simple-git' );
7
5
  const fs = require( 'fs' );
8
- const got = require( 'got' );
9
6
  const path = require( 'path' );
7
+ const util = require( 'util' );
8
+ const got = require( 'got' );
9
+ const SimpleGit = require( 'simple-git' );
10
10
 
11
11
  /**
12
12
  * Promisified dependencies
@@ -16,59 +16,9 @@ const extractZip = util.promisify( require( 'extract-zip' ) );
16
16
  const { rimraf } = require( 'rimraf' );
17
17
 
18
18
  /**
19
- * @typedef {import('./config').WPConfig} WPConfig
20
19
  * @typedef {import('./config').WPSource} WPSource
21
20
  */
22
21
 
23
- /**
24
- * Download each source for each environment. If the same source is used in
25
- * multiple environments, it will only be downloaded once.
26
- *
27
- * @param {WPConfig} config The wp-env configuration object.
28
- * @param {Object} spinner The spinner object to show progress.
29
- * @return {Promise} Returns a promise which resolves when the downloads finish.
30
- */
31
- module.exports = function downloadSources( config, spinner ) {
32
- const progresses = {};
33
- const getProgressSetter = ( id ) => ( progress ) => {
34
- progresses[ id ] = progress;
35
- spinner.text =
36
- 'Downloading WordPress.\n' +
37
- Object.entries( progresses )
38
- .map(
39
- ( [ key, value ] ) =>
40
- ` - ${ key }: ${ ( value * 100 ).toFixed( 0 ) }%`
41
- )
42
- .join( '\n' );
43
- };
44
-
45
- // Will contain a unique array of sources to download.
46
- const sources = [];
47
- const addedSources = {};
48
- const addSource = ( source ) => {
49
- if ( source && source.url && ! addedSources[ source.url ] ) {
50
- sources.push( source );
51
- addedSources[ source.url ] = true;
52
- }
53
- };
54
-
55
- for ( const env of Object.values( config.env ) ) {
56
- env.pluginSources.forEach( addSource );
57
- env.themeSources.forEach( addSource );
58
- Object.values( env.mappings ).forEach( addSource );
59
- addSource( env.coreSource );
60
- }
61
-
62
- return Promise.all(
63
- sources.map( ( source ) =>
64
- downloadSource( source, {
65
- onProgress: getProgressSetter( source.basename ),
66
- spinner,
67
- } )
68
- )
69
- );
70
- };
71
-
72
22
  /**
73
23
  * Downloads the given source if necessary. The specific action taken depends
74
24
  * on the source type. The source is downloaded to source.path.
@@ -204,3 +154,9 @@ async function downloadZipSource( source, { onProgress, spinner, debug } ) {
204
154
 
205
155
  onProgress( 1 );
206
156
  }
157
+
158
+ module.exports = {
159
+ downloadSource,
160
+ downloadGitSource,
161
+ downloadZipSource,
162
+ };