Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wp-env: granular volume mappings #22256

Merged
merged 7 commits into from
May 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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