diff --git a/.wp-env.json b/.wp-env.json index d67cd9642135d..943f4d0399eea 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -1,4 +1,8 @@ { "core": "WordPress/WordPress", - "plugins": [ "." ] + "plugins": [ "." ], + "mappings": { + "wp-content/mu-plugins": "./packages/e2e-tests/mu-plugins", + "wp-content/plugins/gutenberg-test-plugins": "./packages/e2e-tests/plugins" + } } diff --git a/packages/env/CHANGELOG.md b/packages/env/CHANGELOG.md index 5c0c4e2230860..e918ff016cb58 100644 --- a/packages/env/CHANGELOG.md +++ b/packages/env/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New Feature + +- You may now mount local directories to any location within the WordPress install. For example, you may specify `"wp-content/mu-plugins": "./path/to/mu-plugins"` to add mu-plugins. + ## 1.1.0 (2020-04-01) ### New Feature diff --git a/packages/env/README.md b/packages/env/README.md index 05e2a22e334a0..3676bec78c595 100644 --- a/packages/env/README.md +++ b/packages/env/README.md @@ -210,7 +210,7 @@ Positionals: [string] [choices: "all", "development", "tests"] [default: "tests"] ``` -### `wp-env run [container] [command]` +### `wp-env run [container] [command]` ```sh wp-env run [command..] @@ -236,10 +236,10 @@ ID user_login display_name user_email user_registered roles ✔ Ran `wp user list` in 'cli'. (in 2s 374ms) ``` -### `docker logs -f [container_id] >/dev/null` +### `docker logs -f [container_id] >/dev/null` ```sh -docker logs -f >/dev/null +docker logs -f >/dev/null Shows the error logs of the specified container in the terminal. The container_id is the one that is visible with `docker ps -a` ``` @@ -250,23 +250,24 @@ You can customize the WordPress installation, plugins and themes that the develo `.wp-env.json` supports five fields: -| Field | Type | Default | Description | -| ------------- | ------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- | +| Field | Type | Default | Description | +| ------------- | -------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- | | `"core"` | `string\|null` | `null` | The WordPress installation to use. If `null` is specified, `wp-env` will use the latest production release of WordPress. | -| `"plugins"` | `string[]` | `[]` | A list of plugins to install and activate in the environment. | -| `"themes"` | `string[]` | `[]` | A list of themes to install in the environment. The first theme in the list will be activated. | -| `"port"` | `integer` | `8888` | The primary port number to use for the insallation. You'll access the instance through the port: 'http://localhost:8888'. | -| `"testsPort"` | `integer` | `8889` | The port number to use for the tests instance. | -| `"config"` | `Object` | `"{ WP_DEBUG: true, SCRIPT_DEBUG: true }"` | Mapping of wp-config.php constants to their desired values. | +| `"plugins"` | `string[]` | `[]` | A list of plugins to install and activate in the environment. | +| `"themes"` | `string[]` | `[]` | A list of themes to install in the environment. The first theme in the list will be activated. | +| `"port"` | `integer` | `8888` | The primary port number to use for the insallation. You'll access the instance through the port: 'http://localhost:8888'. | +| `"testsPort"` | `integer` | `8889` | The port number to use for the tests instance. | +| `"config"` | `Object` | `"{ WP_DEBUG: true, SCRIPT_DEBUG: true }"` | Mapping of wp-config.php constants to their desired values. | +| `"mappings"` | `Object` | `"{}"` | Mapping of WordPress directories to local directories to be mounted in the WordPress instance. | _Note: the port number environment variables (`WP_ENV_PORT` and `WP_ENV_TESTS_PORT`) take precedent over the .wp-env.json values._ -Several types of strings can be passed into the `core`, `plugins`, and `themes` fields: +Several types of strings can be passed into the `core`, `plugins`, `themes`, and `mappings` fields. | Type | Format | Example(s) | | ----------------- | ----------------------------- | -------------------------------------------------------- | -| Relative path | `.\|~` | `"./a/directory"`, `"../a/directory"`, `"~/a/directory"` | -| Absolute path | `/\|:\` | `"/a/directory"`, `"C:\\a\\directory"` | +| Relative path | `.\|~` | `"./a/directory"`, `"../a/directory"`, `"~/a/directory"` | +| Absolute path | `/\|:\` | `"/a/directory"`, `"C:\\a\\directory"` | | GitHub repository | `/[#]` | `"WordPress/WordPress"`, `"WordPress/gutenberg#master"` | | ZIP File | `http[s]:///.zip` | `"https://wordpress.org/wordpress-5.4-beta2.zip"` | @@ -323,6 +324,34 @@ This is useful for integration testing: that is, testing how old versions of Wor } ``` +#### Add mu-plugins and other mapped directories + +You can add mu-plugins via the mapping config. The mapping config also allows you to mount a directory to any location in the wordpress install, so you could even mount a subdirectory. Note here that theme-1, will not be activated, despite being the "first" mapped theme. + +```json +{ + "plugins": [ "." ], + "mappings": { + "wp-content/mu-plugins": "./path/to/local/mu-plugins", + "wp-content/themes": "./path/to/local/themes", + "wp-content/themes/specific-theme": "./path/to/local/theme-1" + } +} +``` + +#### Avoid activating plugins or themes on the instance + +Since all plugins in the `plugins` key are activated by default, you should use the `mappings` key to avoid this behavior. This might be helpful if you have a test plugin that should not be activated all the time. The same applies for a theme which should not be activated. + +```json +{ + "plugins": [ "." ], + "mappings": { + "wp-content/plugins/my-test-plugin": "./path/to/test/plugin" + } +} +``` + #### Custom Port Numbers You can tell `wp-env` to use a custom port number so that your instance does not conflict with other `wp-env` instances. diff --git a/packages/env/lib/build-docker-compose-config.js b/packages/env/lib/build-docker-compose-config.js index e5ab685f4b147..43b0b6a5f3069 100644 --- a/packages/env/lib/build-docker-compose-config.js +++ b/packages/env/lib/build-docker-compose-config.js @@ -17,30 +17,28 @@ const path = require( 'path' ); * @return {Object} A docker-compose config object, ready to serialize into YAML. */ module.exports = function buildDockerComposeConfig( config ) { - const pluginMounts = config.pluginSources.flatMap( ( source ) => [ - `${ source.path }:/var/www/html/wp-content/plugins/${ source.basename }`, + // Top-level WordPress directory mounts (like wp-content/themes) + const directoryMounts = Object.entries( config.mappings ).map( + ( [ wpDir, source ] ) => `${ source.path }:/var/www/html/${ wpDir }` + ); - // If this is is the Gutenberg plugin, then mount its E2E test plugins. - // TODO: Implement an API that lets Gutenberg mount test plugins without this workaround. - ...( fs.existsSync( path.resolve( source.path, 'gutenberg.php' ) ) - ? [ - `${ source.path }/packages/e2e-tests/plugins:/var/www/html/wp-content/plugins/gutenberg-test-plugins`, - `${ source.path }/packages/e2e-tests/mu-plugins:/var/www/html/wp-content/mu-plugins`, - ] - : [] ), - ] ); + const pluginMounts = config.pluginSources.map( + ( source ) => + `${ source.path }:/var/www/html/wp-content/plugins/${ source.basename }` + ); const themeMounts = config.themeSources.map( ( source ) => `${ source.path }:/var/www/html/wp-content/themes/${ source.basename }` ); + const localMounts = [ ...directoryMounts, ...pluginMounts, ...themeMounts ]; + const developmentMounts = [ `${ config.coreSource ? config.coreSource.path : 'wordpress' }:/var/www/html`, - ...pluginMounts, - ...themeMounts, + ...localMounts, ]; let testsMounts; @@ -83,15 +81,10 @@ module.exports = function buildDockerComposeConfig( config ) { ) : [] ), - ...pluginMounts, - ...themeMounts, + ...localMounts, ]; } else { - testsMounts = [ - 'tests-wordpress:/var/www/html', - ...pluginMounts, - ...themeMounts, - ]; + testsMounts = [ 'tests-wordpress:/var/www/html', ...localMounts ]; } // Set the default ports based on the config values. diff --git a/packages/env/lib/config.js b/packages/env/lib/config.js index 31e0d4bbb3715..73680fe2722fb 100644 --- a/packages/env/lib/config.js +++ b/packages/env/lib/config.js @@ -38,6 +38,7 @@ const HOME_PATH_PREFIX = `~${ path.sep }`; * @property {number} port The port on which to start the development WordPress environment. * @property {number} testsPort The port on which to start the testing WordPress environment. * @property {Object} config Mapping of wp-config.php constants to their desired values. + * @property {Object.} mappings Mapping of WordPress directories to local directories which should be mounted. * @property {boolean} debug True if debug mode is enabled. */ @@ -128,6 +129,7 @@ module.exports = { port: 8888, testsPort: 8889, config: { WP_DEBUG: true, SCRIPT_DEBUG: true }, + mappings: {}, }, config, overrideConfig @@ -185,6 +187,20 @@ module.exports = { ); } + if ( typeof config.mappings !== 'object' ) { + throw new ValidationError( + 'Invalid .wp-env.json: "mappings" must be an object.' + ); + } + + for ( const [ wpDir, localDir ] of Object.entries( config.mappings ) ) { + if ( ! localDir || typeof localDir !== 'string' ) { + throw new ValidationError( + `Invalid .wp-env.json: "mapping.${ wpDir }" should be a string.` + ); + } + } + const workDirectoryPath = path.resolve( getHomeDirectory(), md5( configPath ) @@ -217,6 +233,16 @@ module.exports = { } ) ), config: config.config, + mappings: Object.entries( config.mappings ).reduce( + ( result, [ wpDir, localDir ] ) => { + const source = parseSourceString( localDir, { + workDirectoryPath, + } ); + result[ wpDir ] = source; + return result; + }, + {} + ), }; }, }; diff --git a/packages/env/test/build-docker-compose-config.js b/packages/env/test/build-docker-compose-config.js new file mode 100644 index 0000000000000..6bfd3e2f7c897 --- /dev/null +++ b/packages/env/test/build-docker-compose-config.js @@ -0,0 +1,73 @@ +/** + * Internal dependencies + */ +const buildDockerComposeConfig = require( '../lib/build-docker-compose-config' ); + +// The basic config keys which build docker compose config requires. +const CONFIG = { + mappings: {}, + pluginSources: [], + themeSources: [], + port: 8888, + testsPort: 8889, + configDirectoryPath: '/path/to/config', +}; + +describe( 'buildDockerComposeConfig', () => { + it( 'should map directories before individual sources', () => { + const envConfig = { + ...CONFIG, + mappings: { + 'wp-content/plugins': { + path: '/path/to/wp-plugins', + }, + }, + pluginSources: [ + { path: '/path/to/local/plugin', basename: 'test-name' }, + ], + }; + const dockerConfig = buildDockerComposeConfig( envConfig ); + const { volumes } = dockerConfig.services.wordpress; + expect( volumes ).toEqual( [ + 'wordpress:/var/www/html', // WordPress root + '/path/to/wp-plugins:/var/www/html/wp-content/plugins', // Mapped plugins root + '/path/to/local/plugin:/var/www/html/wp-content/plugins/test-name', // Mapped plugin + ] ); + } ); + + it( 'should add all specified sources to tests, dev, and cli services', () => { + const envConfig = { + ...CONFIG, + mappings: { + 'wp-content/plugins': { + path: '/path/to/wp-plugins', + }, + }, + pluginSources: [ + { path: '/path/to/local/plugin', basename: 'test-name' }, + ], + themeSources: [ + { path: '/path/to/local/theme', basename: 'test-theme' }, + ], + }; + const dockerConfig = buildDockerComposeConfig( envConfig ); + const devVolumes = dockerConfig.services.wordpress.volumes; + const cliVolumes = dockerConfig.services.cli.volumes; + expect( devVolumes ).toEqual( cliVolumes ); + + const testsVolumes = dockerConfig.services[ 'tests-wordpress' ].volumes; + const testsCliVolumes = dockerConfig.services[ 'tests-cli' ].volumes; + expect( testsVolumes ).toEqual( testsCliVolumes ); + + const localSources = [ + '/path/to/wp-plugins:/var/www/html/wp-content/plugins', + '/path/to/local/plugin:/var/www/html/wp-content/plugins/test-name', + '/path/to/local/theme:/var/www/html/wp-content/themes/test-theme', + ]; + + expect( devVolumes ).toEqual( expect.arrayContaining( localSources ) ); + expect( testsVolumes ).toEqual( + expect.arrayContaining( localSources ) + ); + } ); +} ); diff --git a/packages/env/test/config.js b/packages/env/test/config.js index 34384e95b43d9..804180d9ed91e 100644 --- a/packages/env/test/config.js +++ b/packages/env/test/config.js @@ -219,6 +219,93 @@ describe( 'readConfig', () => { } } ); + it( 'should parse mappings into sources', async () => { + readFile.mockImplementation( () => + Promise.resolve( + JSON.stringify( { + mappings: { + test: './relative', + test2: 'WordPress/gutenberg#master', + }, + } ) + ) + ); + const { mappings } = await readConfig( '.wp-env.json' ); + expect( mappings ).toMatchObject( { + test: { + type: 'local', + path: expect.stringMatching( /^\/.*relative$/ ), + basename: 'relative', + }, + test2: { + type: 'git', + path: expect.stringMatching( /^\/.*gutenberg$/ ), + basename: 'gutenberg', + }, + } ); + } ); + + it( 'should throw a validaton error if there is an invalid mapping', async () => { + readFile.mockImplementation( () => + Promise.resolve( JSON.stringify( { mappings: { test: 'false' } } ) ) + ); + expect.assertions( 2 ); + try { + await readConfig( '.wp-env.json' ); + } catch ( error ) { + expect( error ).toBeInstanceOf( ValidationError ); + expect( error.message ).toContain( + 'Invalid or unrecognized source' + ); + } + } ); + + it( 'throws an error if a mapping is badly formatted', async () => { + readFile.mockImplementation( () => + Promise.resolve( + JSON.stringify( { + mappings: { test: null }, + } ) + ) + ); + expect.assertions( 2 ); + try { + await readConfig( '.wp-env.json' ); + } catch ( error ) { + expect( error ).toBeInstanceOf( ValidationError ); + expect( error.message ).toContain( + 'Invalid .wp-env.json: "mapping.test" should be a string.' + ); + } + } ); + + it( 'throws an error if mappings is not an object', async () => { + readFile.mockImplementation( () => + Promise.resolve( + JSON.stringify( { + mappings: 'not object', + } ) + ) + ); + expect.assertions( 2 ); + try { + await readConfig( '.wp-env.json' ); + } catch ( error ) { + expect( error ).toBeInstanceOf( ValidationError ); + expect( error.message ).toContain( + 'Invalid .wp-env.json: "mappings" must be an object.' + ); + } + } ); + + it( 'should return an empty mappings object if none are passed', async () => { + readFile.mockImplementation( () => + Promise.resolve( JSON.stringify( { mappings: {} } ) ) + ); + const { mappings } = await readConfig( '.wp-env.json' ); + expect( mappings ).toEqual( {} ); + } ); + it( 'should throw a validaton error if the ports are not numbers', async () => { expect.assertions( 10 ); await testPortNumberValidation( 'port', 'string' );