Skip to content

Commit

Permalink
wp-env: granular volume mappings (#22256)
Browse files Browse the repository at this point in the history
  • Loading branch information
noahtallen authored May 12, 2020
1 parent 903e52c commit 3616a16
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 34 deletions.
6 changes: 5 additions & 1 deletion .wp-env.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
4 changes: 4 additions & 0 deletions packages/env/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 42 additions & 13 deletions packages/env/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <container> [command..]
Expand All @@ -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 <container_id> >/dev/null
docker logs -f <container_id> >/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`
```
Expand All @@ -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 | `.<path>\|~<path>` | `"./a/directory"`, `"../a/directory"`, `"~/a/directory"` |
| Absolute path | `/<path>\|<letter>:\<path>` | `"/a/directory"`, `"C:\\a\\directory"` |
| Relative path | `.<path>\|~<path>` | `"./a/directory"`, `"../a/directory"`, `"~/a/directory"` |
| Absolute path | `/<path>\|<letter>:\<path>` | `"/a/directory"`, `"C:\\a\\directory"` |
| GitHub repository | `<owner>/<repo>[#<ref>]` | `"WordPress/WordPress"`, `"WordPress/gutenberg#master"` |
| ZIP File | `http[s]://<host>/<path>.zip` | `"https://wordpress.org/wordpress-5.4-beta2.zip"` |
Expand Down Expand Up @@ -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.
Expand Down
33 changes: 13 additions & 20 deletions packages/env/lib/build-docker-compose-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
26 changes: 26 additions & 0 deletions packages/env/lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.<string, Source>} mappings Mapping of WordPress directories to local directories which should be mounted.
* @property {boolean} debug True if debug mode is enabled.
*/

Expand Down Expand Up @@ -128,6 +129,7 @@ module.exports = {
port: 8888,
testsPort: 8889,
config: { WP_DEBUG: true, SCRIPT_DEBUG: true },
mappings: {},
},
config,
overrideConfig
Expand Down Expand Up @@ -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 )
Expand Down Expand Up @@ -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;
},
{}
),
};
},
};
Expand Down
73 changes: 73 additions & 0 deletions packages/env/test/build-docker-compose-config.js
Original file line number Diff line number Diff line change
@@ -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 )
);
} );
} );
87 changes: 87 additions & 0 deletions packages/env/test/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' );
Expand Down

0 comments on commit 3616a16

Please sign in to comment.