@wordpress/env 5.16.0 → 7.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 (54) hide show
  1. package/README.md +189 -120
  2. package/lib/build-docker-compose-config.js +67 -96
  3. package/lib/cache.js +1 -0
  4. package/lib/cli.js +23 -3
  5. package/lib/commands/clean.js +14 -1
  6. package/lib/commands/destroy.js +12 -37
  7. package/lib/commands/index.js +1 -0
  8. package/lib/commands/install-path.js +1 -0
  9. package/lib/commands/logs.js +1 -0
  10. package/lib/commands/run.js +79 -15
  11. package/lib/commands/start.js +32 -10
  12. package/lib/commands/stop.js +1 -0
  13. package/lib/config/add-or-replace-port.js +41 -0
  14. package/lib/config/db-env.js +1 -0
  15. package/lib/config/detect-directory-type.js +1 -1
  16. package/lib/config/get-cache-directory.js +39 -0
  17. package/lib/config/get-config-from-environment-vars.js +86 -0
  18. package/lib/config/index.js +7 -5
  19. package/lib/config/load-config.js +92 -0
  20. package/lib/config/merge-configs.js +104 -0
  21. package/lib/config/parse-config.js +418 -157
  22. package/lib/config/parse-source-string.js +164 -0
  23. package/lib/config/post-process-config.js +202 -0
  24. package/lib/config/read-raw-config-file.js +7 -33
  25. package/lib/config/test/__snapshots__/config-integration.js.snap +271 -0
  26. package/lib/config/test/add-or-replace-port.js +81 -0
  27. package/lib/config/test/config-integration.js +143 -0
  28. package/lib/config/test/get-cache-directory.js +57 -0
  29. package/lib/config/test/merge-configs.js +111 -0
  30. package/lib/config/test/parse-config.js +342 -0
  31. package/lib/config/test/parse-source-string.js +154 -0
  32. package/lib/config/test/post-process-config.js +295 -0
  33. package/lib/config/test/read-raw-config-file.js +19 -63
  34. package/lib/config/test/validate-config.js +305 -0
  35. package/lib/config/validate-config.js +103 -40
  36. package/lib/env.js +2 -0
  37. package/lib/execute-after-setup.js +51 -0
  38. package/lib/get-host-user.js +31 -0
  39. package/lib/init-config.js +181 -63
  40. package/lib/md5.js +1 -0
  41. package/lib/parse-xdebug-mode.js +1 -0
  42. package/lib/retry.js +1 -0
  43. package/lib/test/__snapshots__/md5.js.snap +9 -0
  44. package/lib/test/build-docker-compose-config.js +134 -0
  45. package/lib/test/cache.js +154 -0
  46. package/lib/test/cli.js +149 -0
  47. package/lib/test/execute-after-setup.js +66 -0
  48. package/lib/test/md5.js +35 -0
  49. package/lib/test/parse-xdebug-mode.js +41 -0
  50. package/lib/wordpress.js +5 -33
  51. package/package.json +2 -2
  52. package/lib/config/config.js +0 -357
  53. package/lib/config/test/__snapshots__/config.js.snap +0 -83
  54. package/lib/config/test/config.js +0 -1203
@@ -10,25 +10,28 @@ const path = require( 'path' );
10
10
  */
11
11
  const { hasSameCoreSource } = require( './wordpress' );
12
12
  const { dbEnv } = require( './config' );
13
+ const getHostUser = require( './get-host-user' );
13
14
 
14
15
  /**
15
16
  * @typedef {import('./config').WPConfig} WPConfig
16
- * @typedef {import('./config').WPServiceConfig} WPServiceConfig
17
+ * @typedef {import('./config').WPEnvironmentConfig} WPEnvironmentConfig
17
18
  */
18
19
 
19
20
  /**
20
21
  * Gets the volume mounts for an individual service.
21
22
  *
22
- * @param {string} workDirectoryPath The working directory for wp-env.
23
- * @param {WPServiceConfig} config The service config to get the mounts from.
24
- * @param {string} wordpressDefault The default internal path for the WordPress
25
- * source code (such as tests-wordpress).
23
+ * @param {string} workDirectoryPath The working directory for wp-env.
24
+ * @param {WPEnvironmentConfig} config The service config to get the mounts from.
25
+ * @param {string} hostUsername The username of the host running wp-env.
26
+ * @param {string} wordpressDefault The default internal path for the WordPress
27
+ * source code (such as tests-wordpress).
26
28
  *
27
29
  * @return {string[]} An array of volumes to mount in string format.
28
30
  */
29
31
  function getMounts(
30
32
  workDirectoryPath,
31
33
  config,
34
+ hostUsername,
32
35
  wordpressDefault = 'wordpress'
33
36
  ) {
34
37
  // Top-level WordPress directory mounts (like wp-content/themes)
@@ -46,9 +49,10 @@ function getMounts(
46
49
  `${ source.path }:/var/www/html/wp-content/themes/${ source.basename }`
47
50
  );
48
51
 
49
- const coreMount = `${
50
- config.coreSource ? config.coreSource.path : wordpressDefault
51
- }:/var/www/html`;
52
+ const userHomeMount =
53
+ wordpressDefault === 'wordpress'
54
+ ? `user-home:/home/${ hostUsername }`
55
+ : `tests-user-home:/home/${ hostUsername }`;
52
56
 
53
57
  const corePHPUnitMount = `${ path.join(
54
58
  workDirectoryPath,
@@ -59,10 +63,15 @@ function getMounts(
59
63
  'phpunit'
60
64
  ) }:/wordpress-phpunit`;
61
65
 
66
+ const coreMount = `${
67
+ config.coreSource ? config.coreSource.path : wordpressDefault
68
+ }:/var/www/html`;
69
+
62
70
  return [
63
71
  ...new Set( [
64
- coreMount,
72
+ coreMount, // Must be first because of some operations later that expect it to be!
65
73
  corePHPUnitMount,
74
+ userHomeMount,
66
75
  ...directoryMounts,
67
76
  ...pluginMounts,
68
77
  ...themeMounts,
@@ -79,16 +88,32 @@ function getMounts(
79
88
  * @return {Object} A docker-compose config object, ready to serialize into YAML.
80
89
  */
81
90
  module.exports = function buildDockerComposeConfig( config ) {
91
+ // Since we are mounting files from the host operating system
92
+ // we want to create the host user in some of our containers.
93
+ // This ensures ownership parity and lets us access files
94
+ // and folders between the containers and the host.
95
+ const hostUser = getHostUser();
96
+
82
97
  const developmentMounts = getMounts(
83
98
  config.workDirectoryPath,
84
- config.env.development
99
+ config.env.development,
100
+ hostUser.name
85
101
  );
86
102
  const testsMounts = getMounts(
87
103
  config.workDirectoryPath,
88
104
  config.env.tests,
105
+ hostUser.name,
89
106
  'tests-wordpress'
90
107
  );
91
108
 
109
+ // We use a custom Dockerfile in order to make sure that
110
+ // the current host user exists inside the container.
111
+ const imageBuildArgs = {
112
+ HOST_USERNAME: hostUser.name,
113
+ HOST_UID: hostUser.uid,
114
+ HOST_GID: hostUser.gid,
115
+ };
116
+
92
117
  // When both tests and development reference the same WP source, we need to
93
118
  // ensure that tests pulls from a copy of the files so that it maintains
94
119
  // a separate DB and config. Additionally, if the source type is local we
@@ -143,64 +168,6 @@ module.exports = function buildDockerComposeConfig( config ) {
143
168
  const developmentPorts = `\${WP_ENV_PORT:-${ config.env.development.port }}:80`;
144
169
  const testsPorts = `\${WP_ENV_TESTS_PORT:-${ config.env.tests.port }}:80`;
145
170
 
146
- // Set the WordPress, WP-CLI, PHPUnit PHP version if defined.
147
- const developmentPhpVersion = config.env.development.phpVersion
148
- ? config.env.development.phpVersion
149
- : '';
150
- const testsPhpVersion = config.env.tests.phpVersion
151
- ? config.env.tests.phpVersion
152
- : '';
153
-
154
- // Set the WordPress images with the PHP version tag.
155
- const developmentWpImage = `wordpress${
156
- developmentPhpVersion ? ':php' + developmentPhpVersion : ''
157
- }`;
158
- const testsWpImage = `wordpress${
159
- testsPhpVersion ? ':php' + testsPhpVersion : ''
160
- }`;
161
- // Set the WordPress CLI images with the PHP version tag.
162
- const developmentWpCliImage = `wordpress:cli${
163
- ! developmentPhpVersion || developmentPhpVersion.length === 0
164
- ? ''
165
- : '-php' + developmentPhpVersion
166
- }`;
167
- const testsWpCliImage = `wordpress:cli${
168
- ! testsPhpVersion || testsPhpVersion.length === 0
169
- ? ''
170
- : '-php' + testsPhpVersion
171
- }`;
172
-
173
- // Defaults are to use the most recent version of PHPUnit that provides
174
- // support for the specified version of PHP.
175
- // PHP Unit is assumed to be for Tests so use the testsPhpVersion.
176
- let phpunitTag = 'latest';
177
- const phpunitPhpVersion = '-php-' + testsPhpVersion + '-fpm';
178
- if ( testsPhpVersion === '5.6' ) {
179
- phpunitTag = '5' + phpunitPhpVersion;
180
- } else if ( testsPhpVersion === '7.0' ) {
181
- phpunitTag = '6' + phpunitPhpVersion;
182
- } else if ( testsPhpVersion === '7.1' ) {
183
- phpunitTag = '7' + phpunitPhpVersion;
184
- } else if ( testsPhpVersion === '7.2' ) {
185
- phpunitTag = '8' + phpunitPhpVersion;
186
- } else if (
187
- [ '7.3', '7.4', '8.0', '8.1', '8.2' ].indexOf( testsPhpVersion ) >= 0
188
- ) {
189
- phpunitTag = '9' + phpunitPhpVersion;
190
- }
191
- const phpunitImage = `wordpressdevelop/phpunit:${ phpunitTag }`;
192
-
193
- // The www-data user in wordpress:cli has a different UID (82) to the
194
- // www-data user in wordpress (33). Ensure we use the wordpress www-data
195
- // user for CLI commands.
196
- // https://github.com/docker-library/wordpress/issues/256
197
- const cliUser = '33:33';
198
-
199
- // If the user mounted their own uploads folder, we should not override it in the phpunit service.
200
- const isMappingTestUploads = testsMounts.some( ( mount ) =>
201
- mount.endsWith( ':/var/www/html/wp-content/uploads' )
202
- );
203
-
204
171
  return {
205
172
  version: '3.7',
206
173
  services: {
@@ -227,69 +194,72 @@ module.exports = function buildDockerComposeConfig( config ) {
227
194
  volumes: [ 'mysql-test:/var/lib/mysql' ],
228
195
  },
229
196
  wordpress: {
230
- build: '.',
231
197
  depends_on: [ 'mysql' ],
232
- image: developmentWpImage,
198
+ build: {
199
+ context: '.',
200
+ dockerfile: 'WordPress.Dockerfile',
201
+ args: imageBuildArgs,
202
+ },
233
203
  ports: [ developmentPorts ],
234
204
  environment: {
205
+ APACHE_RUN_USER: '#' + hostUser.uid,
206
+ APACHE_RUN_GROUP: '#' + hostUser.gid,
235
207
  ...dbEnv.credentials,
236
208
  ...dbEnv.development,
237
209
  WP_TESTS_DIR: '/wordpress-phpunit',
238
210
  },
239
211
  volumes: developmentMounts,
212
+ extra_hosts: [ 'host.docker.internal:host-gateway' ],
240
213
  },
241
214
  'tests-wordpress': {
242
215
  depends_on: [ 'tests-mysql' ],
243
- image: testsWpImage,
216
+ build: {
217
+ context: '.',
218
+ dockerfile: 'Tests-WordPress.Dockerfile',
219
+ args: imageBuildArgs,
220
+ },
244
221
  ports: [ testsPorts ],
245
222
  environment: {
223
+ APACHE_RUN_USER: '#' + hostUser.uid,
224
+ APACHE_RUN_GROUP: '#' + hostUser.gid,
246
225
  ...dbEnv.credentials,
247
226
  ...dbEnv.tests,
248
227
  WP_TESTS_DIR: '/wordpress-phpunit',
249
228
  },
250
229
  volumes: testsMounts,
230
+ extra_hosts: [ 'host.docker.internal:host-gateway' ],
251
231
  },
252
232
  cli: {
253
233
  depends_on: [ 'wordpress' ],
254
- image: developmentWpCliImage,
234
+ build: {
235
+ context: '.',
236
+ dockerfile: 'CLI.Dockerfile',
237
+ args: imageBuildArgs,
238
+ },
255
239
  volumes: developmentMounts,
256
- user: cliUser,
240
+ user: hostUser.fullUser,
257
241
  environment: {
258
242
  ...dbEnv.credentials,
259
243
  ...dbEnv.development,
260
244
  WP_TESTS_DIR: '/wordpress-phpunit',
261
245
  },
246
+ extra_hosts: [ 'host.docker.internal:host-gateway' ],
262
247
  },
263
248
  'tests-cli': {
264
249
  depends_on: [ 'tests-wordpress' ],
265
- image: testsWpCliImage,
250
+ build: {
251
+ context: '.',
252
+ dockerfile: 'Tests-CLI.Dockerfile',
253
+ args: imageBuildArgs,
254
+ },
266
255
  volumes: testsMounts,
267
- user: cliUser,
256
+ user: hostUser.fullUser,
268
257
  environment: {
269
258
  ...dbEnv.credentials,
270
259
  ...dbEnv.tests,
271
260
  WP_TESTS_DIR: '/wordpress-phpunit',
272
261
  },
273
- },
274
- composer: {
275
- image: 'composer',
276
- volumes: [ `${ config.configDirectoryPath }:/app` ],
277
- },
278
- phpunit: {
279
- image: phpunitImage,
280
- depends_on: [ 'tests-wordpress' ],
281
- volumes: [
282
- ...testsMounts,
283
- ...( ! isMappingTestUploads
284
- ? [ 'phpunit-uploads:/var/www/html/wp-content/uploads' ]
285
- : [] ),
286
- ],
287
- environment: {
288
- LOCAL_DIR: 'html',
289
- WP_TESTS_DIR: '/wordpress-phpunit',
290
- ...dbEnv.credentials,
291
- ...dbEnv.tests,
292
- },
262
+ extra_hosts: [ 'host.docker.internal:host-gateway' ],
293
263
  },
294
264
  },
295
265
  volumes: {
@@ -297,7 +267,8 @@ module.exports = function buildDockerComposeConfig( config ) {
297
267
  ...( ! config.env.tests.coreSource && { 'tests-wordpress': {} } ),
298
268
  mysql: {},
299
269
  'mysql-test': {},
300
- 'phpunit-uploads': {},
270
+ 'user-home': {},
271
+ 'tests-user-home': {},
301
272
  },
302
273
  };
303
274
  };
package/lib/cache.js CHANGED
@@ -1,3 +1,4 @@
1
+ 'use strict';
1
2
  /**
2
3
  * External dependencies
3
4
  */
package/lib/cli.js CHANGED
@@ -39,8 +39,11 @@ const withSpinner =
39
39
  process.exit( 0 );
40
40
  },
41
41
  ( error ) => {
42
- if ( error instanceof env.ValidationError ) {
43
- // Error is a validation error. That means the user did something wrong.
42
+ if (
43
+ error instanceof env.ValidationError ||
44
+ error instanceof env.AfterSetupError
45
+ ) {
46
+ // Error is a configuration error. That means the user did something wrong.
44
47
  spinner.fail( error.message );
45
48
  process.exit( 1 );
46
49
  } else if (
@@ -124,6 +127,11 @@ module.exports = function cli() {
124
127
  coerce: parseXdebugMode,
125
128
  type: 'string',
126
129
  } );
130
+ args.option( 'scripts', {
131
+ type: 'boolean',
132
+ describe: 'Execute any configured lifecycle scripts.',
133
+ default: true,
134
+ } );
127
135
  },
128
136
  withSpinner( env.start )
129
137
  );
@@ -145,6 +153,11 @@ module.exports = function cli() {
145
153
  choices: [ 'all', 'development', 'tests' ],
146
154
  default: 'tests',
147
155
  } );
156
+ args.option( 'scripts', {
157
+ type: 'boolean',
158
+ describe: 'Execute any configured lifecycle scripts.',
159
+ default: true,
160
+ } );
148
161
  },
149
162
  withSpinner( env.clean )
150
163
  );
@@ -174,6 +187,13 @@ module.exports = function cli() {
174
187
  'run <container> [command..]',
175
188
  'Runs an arbitrary command in one of the underlying Docker containers. The "container" param should reference one of the underlying Docker services like "development", "tests", or "cli". To run a wp-cli command, use the "cli" or "tests-cli" service. You can also use this command to open shell sessions like bash and the WordPress shell in the WordPress instance. For example, `wp-env run cli bash` will open bash in the development WordPress instance. When using long commands with arguments and quotation marks, you need to wrap the "command" param in quotation marks. For example: `wp-env run tests-cli "wp post create --post_type=page --post_title=\'Test\'"` will create a post on the tests WordPress instance.',
176
189
  ( args ) => {
190
+ args.option( 'env-cwd', {
191
+ type: 'string',
192
+ requiresArg: true,
193
+ default: '.',
194
+ describe:
195
+ "The command's working directory inside of the container. Paths without a leading slash are relative to the WordPress root.",
196
+ } );
177
197
  args.positional( 'container', {
178
198
  type: 'string',
179
199
  describe: 'The container to run the command on.',
@@ -208,7 +228,7 @@ module.exports = function cli() {
208
228
  );
209
229
  yargs.command(
210
230
  'install-path',
211
- 'Get the path where environment files are located.',
231
+ 'Get the path where all of the environment files are stored. This includes the Docker files, WordPress, PHPUnit files, and any sources that were downloaded.',
212
232
  () => {},
213
233
  withSpinner( env.installPath )
214
234
  );
@@ -1,3 +1,4 @@
1
+ 'use strict';
1
2
  /**
2
3
  * External dependencies
3
4
  */
@@ -8,6 +9,7 @@ const dockerCompose = require( 'docker-compose' );
8
9
  */
9
10
  const initConfig = require( '../init-config' );
10
11
  const { configureWordPress, resetDatabase } = require( '../wordpress' );
12
+ const { executeAfterSetup } = require( '../execute-after-setup' );
11
13
 
12
14
  /**
13
15
  * @typedef {import('../wordpress').WPEnvironment} WPEnvironment
@@ -20,9 +22,15 @@ const { configureWordPress, resetDatabase } = require( '../wordpress' );
20
22
  * @param {Object} options
21
23
  * @param {WPEnvironmentSelection} options.environment The environment to clean. Either 'development', 'tests', or 'all'.
22
24
  * @param {Object} options.spinner A CLI spinner which indicates progress.
25
+ * @param {boolean} options.scripts Indicates whether or not lifecycle scripts should be executed.
23
26
  * @param {boolean} options.debug True if debug mode is enabled.
24
27
  */
25
- module.exports = async function clean( { environment, spinner, debug } ) {
28
+ module.exports = async function clean( {
29
+ environment,
30
+ spinner,
31
+ scripts,
32
+ debug,
33
+ } ) {
26
34
  const config = await initConfig( { spinner, debug } );
27
35
 
28
36
  const description = `${ environment } environment${
@@ -57,5 +65,10 @@ module.exports = async function clean( { environment, spinner, debug } ) {
57
65
 
58
66
  await Promise.all( tasks );
59
67
 
68
+ // Execute any configured command that should run after the environment has finished being set up.
69
+ if ( scripts ) {
70
+ executeAfterSetup( config, spinner );
71
+ }
72
+
60
73
  spinner.text = `Cleaned ${ description }.`;
61
74
  };
@@ -1,3 +1,4 @@
1
+ 'use strict';
1
2
  /**
2
3
  * External dependencies
3
4
  */
@@ -11,12 +12,11 @@ const inquirer = require( 'inquirer' );
11
12
  * Promisified dependencies
12
13
  */
13
14
  const rimraf = util.promisify( require( 'rimraf' ) );
14
- const exec = util.promisify( require( 'child_process' ).exec );
15
15
 
16
16
  /**
17
17
  * Internal dependencies
18
18
  */
19
- const { readConfig } = require( '../../lib/config' );
19
+ const { loadConfig } = require( '../config' );
20
20
 
21
21
  /**
22
22
  * Destroy the development server.
@@ -26,9 +26,8 @@ const { readConfig } = require( '../../lib/config' );
26
26
  * @param {boolean} options.debug True if debug mode is enabled.
27
27
  */
28
28
  module.exports = async function destroy( { spinner, debug } ) {
29
- const configPath = path.resolve( '.wp-env.json' );
30
- const { dockerComposeConfigPath, workDirectoryPath } = await readConfig(
31
- configPath
29
+ const { dockerComposeConfigPath, workDirectoryPath } = await loadConfig(
30
+ path.resolve( '.' )
32
31
  );
33
32
 
34
33
  try {
@@ -39,7 +38,7 @@ module.exports = async function destroy( { spinner, debug } ) {
39
38
  }
40
39
 
41
40
  spinner.info(
42
- 'WARNING! This will remove Docker containers, volumes, and networks associated with the WordPress instance.'
41
+ 'WARNING! This will remove Docker containers, volumes, networks, and images associated with the WordPress instance.'
43
42
  );
44
43
 
45
44
  const { yesDelete } = await inquirer.prompt( [
@@ -58,44 +57,20 @@ module.exports = async function destroy( { spinner, debug } ) {
58
57
  return;
59
58
  }
60
59
 
61
- spinner.text = 'Removing WordPress docker containers.';
60
+ spinner.text = 'Removing docker images, volumes, and networks.';
62
61
 
63
- await dockerCompose.rm( {
62
+ await dockerCompose.down( {
64
63
  config: dockerComposeConfigPath,
65
- commandOptions: [ '--stop', '-v' ],
64
+ commandOptions: [ '--volumes', '--remove-orphans', '--rmi', 'all' ],
66
65
  log: debug,
67
66
  } );
68
67
 
69
- const directoryHash = path.basename( workDirectoryPath );
70
-
71
- spinner.text = 'Removing docker volumes.';
72
- await removeDockerItems( 'volume', directoryHash );
73
-
74
- spinner.text = 'Removing docker networks.';
75
- await removeDockerItems( 'network', directoryHash );
76
-
77
68
  spinner.text = 'Removing local files.';
78
-
69
+ // Note: there is a race condition where docker compose actually hasn't finished
70
+ // by this point, which causes rimraf to fail. We need to wait at least 2.5-5s,
71
+ // but using 10s in case it's dependant on the machine.
72
+ await new Promise( ( resolve ) => setTimeout( resolve, 10000 ) );
79
73
  await rimraf( workDirectoryPath );
80
74
 
81
75
  spinner.text = 'Removed WordPress environment.';
82
76
  };
83
-
84
- /**
85
- * Removes docker items, like networks or volumes, matching the given name.
86
- *
87
- * @param {string} itemType The item type, like "network" or "volume"
88
- * @param {string} name Remove items whose name match this string.
89
- */
90
- async function removeDockerItems( itemType, name ) {
91
- const { stdout: items } = await exec(
92
- `docker ${ itemType } ls -q --filter name=${ name }`
93
- );
94
- if ( items ) {
95
- await exec(
96
- `docker ${ itemType } rm ${ items
97
- .split( '\n' ) // TODO: use os.EOL?
98
- .join( ' ' ) }`
99
- );
100
- }
101
- }
@@ -1,3 +1,4 @@
1
+ 'use strict';
1
2
  /**
2
3
  * Internal dependencies
3
4
  */
@@ -1,3 +1,4 @@
1
+ 'use strict';
1
2
  /**
2
3
  * Internal dependencies
3
4
  */
@@ -1,3 +1,4 @@
1
+ 'use strict';
1
2
  /**
2
3
  * External dependencies
3
4
  */
@@ -1,12 +1,16 @@
1
+ 'use strict';
1
2
  /**
2
3
  * External dependencies
3
4
  */
4
5
  const { spawn } = require( 'child_process' );
6
+ const path = require( 'path' );
5
7
 
6
8
  /**
7
9
  * Internal dependencies
8
10
  */
9
11
  const initConfig = require( '../init-config' );
12
+ const getHostUser = require( '../get-host-user' );
13
+ const { ValidationError } = require( '../config' );
10
14
 
11
15
  /**
12
16
  * @typedef {import('../config').WPConfig} WPConfig
@@ -18,10 +22,19 @@ const initConfig = require( '../init-config' );
18
22
  * @param {Object} options
19
23
  * @param {string} options.container The Docker container to run the command on.
20
24
  * @param {string[]} options.command The command to run.
25
+ * @param {string} options.envCwd The working directory for the command to be executed from.
21
26
  * @param {Object} options.spinner A CLI spinner which indicates progress.
22
27
  * @param {boolean} options.debug True if debug mode is enabled.
23
28
  */
24
- module.exports = async function run( { container, command, spinner, debug } ) {
29
+ module.exports = async function run( {
30
+ container,
31
+ command,
32
+ envCwd,
33
+ spinner,
34
+ debug,
35
+ } ) {
36
+ validateContainerExistence( container );
37
+
25
38
  const config = await initConfig( { spinner, debug } );
26
39
 
27
40
  command = command.join( ' ' );
@@ -29,31 +42,82 @@ module.exports = async function run( { container, command, spinner, debug } ) {
29
42
  // Shows a contextual tip for the given command.
30
43
  showCommandTips( command, container, spinner );
31
44
 
32
- await spawnCommandDirectly( {
33
- container,
34
- command,
35
- spinner,
36
- config,
37
- } );
45
+ await spawnCommandDirectly( config, container, command, envCwd, spinner );
38
46
 
39
47
  spinner.text = `Ran \`${ command }\` in '${ container }'.`;
40
48
  };
41
49
 
50
+ /**
51
+ * Validates the container option and throws if it is invalid.
52
+ *
53
+ * @param {string} container The Docker container to run the command on.
54
+ */
55
+ function validateContainerExistence( container ) {
56
+ // Give better errors for containers that we have removed.
57
+ if ( container === 'phpunit' ) {
58
+ throw new ValidationError(
59
+ "The 'phpunit' container has been removed. Please use 'wp-env run tests-cli --env-cwd=wp-content/path/to/plugin phpunit' instead."
60
+ );
61
+ }
62
+ if ( container === 'composer' ) {
63
+ throw new ValidationError(
64
+ "The 'composer' container has been removed. Please use 'wp-env run cli --env-cwd=wp-content/path/to/plugin composer' instead."
65
+ );
66
+ }
67
+
68
+ // Provide better error output than Docker's "service does not exist" messaging.
69
+ const validContainers = [
70
+ 'mysql',
71
+ 'tests-mysql',
72
+ 'wordpress',
73
+ 'tests-wordpress',
74
+ 'cli',
75
+ 'tests-cli',
76
+ ];
77
+ if ( ! validContainers.includes( container ) ) {
78
+ throw new ValidationError(
79
+ `The '${ container }' container does not exist. Valid selections are: ${ validContainers.join(
80
+ ', '
81
+ ) }`
82
+ );
83
+ }
84
+ }
85
+
42
86
  /**
43
87
  * Runs an arbitrary command on the given Docker container.
44
88
  *
45
- * @param {Object} options
46
- * @param {string} options.container The Docker container to run the command on.
47
- * @param {string} options.command The command to run.
48
- * @param {WPConfig} options.config The wp-env configuration.
49
- * @param {Object} options.spinner A CLI spinner which indicates progress.
89
+ * @param {WPConfig} config The wp-env configuration.
90
+ * @param {string} container The Docker container to run the command on.
91
+ * @param {string} command The command to run.
92
+ * @param {string} envCwd The working directory for the command to be executed from.
93
+ * @param {Object} spinner A CLI spinner which indicates progress.
50
94
  */
51
- function spawnCommandDirectly( { container, command, config, spinner } ) {
95
+ function spawnCommandDirectly( config, container, command, envCwd, spinner ) {
96
+ // Both the `wordpress` and `tests-wordpress` containers have the host's
97
+ // user so that they can maintain ownership parity with the host OS.
98
+ // We should run any commands as that user so that they are able
99
+ // to interact with the files mounted from the host.
100
+ const hostUser = getHostUser();
101
+
102
+ // We need to pass absolute paths to the container.
103
+ envCwd = path.resolve(
104
+ // Not all containers have the same starting working directory.
105
+ container === 'mysql' || container === 'tests-mysql'
106
+ ? '/'
107
+ : '/var/www/html',
108
+ envCwd
109
+ );
110
+
111
+ const isTTY = process.stdout.isTTY;
52
112
  const composeCommand = [
53
113
  '-f',
54
114
  config.dockerComposeConfigPath,
55
- 'run',
56
- '--rm',
115
+ 'exec',
116
+ ! isTTY ? '-T' : '',
117
+ '-w',
118
+ envCwd,
119
+ '--user',
120
+ hostUser.fullUser,
57
121
  container,
58
122
  ...command.split( ' ' ), // The command will fail if passed as a complete string.
59
123
  ];