From 012b0e4d550ba457866bdc48d1184f4ed423a876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Thu, 6 Feb 2020 11:49:27 -0800 Subject: [PATCH 01/15] navigation: set inherit color to anchor elements (#20038) It sets the color of the anchor elements when the Navigation menu has defined a text color. It fixes coloring issues for some themes. --- packages/block-library/src/navigation/style.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/block-library/src/navigation/style.scss b/packages/block-library/src/navigation/style.scss index f3c0415a6244c9..c1490185a7c9d0 100644 --- a/packages/block-library/src/navigation/style.scss +++ b/packages/block-library/src/navigation/style.scss @@ -192,6 +192,10 @@ $navigation-sub-menu-width: $grid-size * 25; .wp-block-navigation-link svg { transform: rotate(0); } + + &.has-text-color .wp-block-navigation-link__content { + color: inherit; + } } } From f0b42207bb57ed1cdaeeec3f0d97698685fed11e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Thu, 6 Feb 2020 13:47:01 -0800 Subject: [PATCH 02/15] navgation-link: set width in order to show caret (#20075) --- packages/block-library/src/navigation/editor.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/block-library/src/navigation/editor.scss b/packages/block-library/src/navigation/editor.scss index 845f7dc147f42f..50ff75a2389d82 100644 --- a/packages/block-library/src/navigation/editor.scss +++ b/packages/block-library/src/navigation/editor.scss @@ -57,6 +57,11 @@ $navigation-item-height: 46px; background: none; } } + + // Set a min width when the item is focused and empty in order to show the caret. + .wp-block-navigation .wp-block-navigation-link.is-selected .block-editor-rich-text__editable:focus { + min-width: 20px; + } } .wp-block-navigation__inserter-content { From fab9e026862eda9653da12d695871001c2570e76 Mon Sep 17 00:00:00 2001 From: Lara Schenck Date: Thu, 6 Feb 2020 18:22:15 -0500 Subject: [PATCH 03/15] Docs: Fix broken link (#20077) --- packages/data/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/data/README.md b/packages/data/README.md index d0eeba0e08539d..5e53e5c406f5c4 100644 --- a/packages/data/README.md +++ b/packages/data/README.md @@ -234,7 +234,7 @@ registerGenericStore( 'custom-data', createCustomStore() ); ## Comparison with Redux -The data module shares many of the same [core principles](https://redux.js.org/introduction/three-principles) and [API method naming](https://redux.js.org/api-reference) of [Redux](https://redux.js.org/). In fact, it is implemented atop Redux. Where it differs is in establishing a modularization pattern for creating separate but interdependent stores, and in codifying conventions such as selector functions as the primary entry point for data access. +The data module shares many of the same [core principles](https://redux.js.org/introduction/three-principles) and [API method naming](https://redux.js.org/api/api-reference) of [Redux](https://redux.js.org/). In fact, it is implemented atop Redux. Where it differs is in establishing a modularization pattern for creating separate but interdependent stores, and in codifying conventions such as selector functions as the primary entry point for data access. The [higher-order components](#higher-order-components) were created to complement this distinction. The intention with splitting `withSelect` and `withDispatch` — where in React Redux they are combined under `connect` as `mapStateToProps` and `mapDispatchToProps` arguments — is to more accurately reflect that dispatch is not dependent upon a subscription to state changes, and to allow for state-derived values to be used in `withDispatch` (via [higher-order component composition](/packages/compose/README.md)). From 8e93c30b3a6f0d7d0cf8651373d514354e6959b5 Mon Sep 17 00:00:00 2001 From: Ringish Date: Fri, 7 Feb 2020 04:07:08 +0100 Subject: [PATCH 04/15] Add/transform button to buttons (#20063) * Implements transform on buttons * DEV: Makes buttons transform multi block * Improves variable names * Adds some comments & imporoves code readability --- packages/block-library/src/buttons/index.js | 2 ++ .../block-library/src/buttons/transforms.js | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 packages/block-library/src/buttons/transforms.js diff --git a/packages/block-library/src/buttons/index.js b/packages/block-library/src/buttons/index.js index 211c07f4e43ad0..4777968dd1e710 100644 --- a/packages/block-library/src/buttons/index.js +++ b/packages/block-library/src/buttons/index.js @@ -7,6 +7,7 @@ import { button as icon } from '@wordpress/icons'; /** * Internal dependencies */ +import transforms from './transforms'; import edit from './edit'; import metadata from './block.json'; import save from './save'; @@ -26,6 +27,7 @@ export const settings = { align: true, alignWide: false, }, + transforms, edit, save, }; diff --git a/packages/block-library/src/buttons/transforms.js b/packages/block-library/src/buttons/transforms.js new file mode 100644 index 00000000000000..a558b580741d34 --- /dev/null +++ b/packages/block-library/src/buttons/transforms.js @@ -0,0 +1,32 @@ +/** + * WordPress dependencies + */ +import { createBlock } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { name } from './block.json'; + +const transforms = { + from: [ + { + type: 'block', + isMultiBlock: true, + blocks: [ 'core/button' ], + transform: ( buttons ) => + // Creates the buttons block + createBlock( + name, + {}, + // Loop the selected buttons + buttons.map( ( attributes ) => + // Create singular button in the buttons block + createBlock( 'core/button', attributes ) + ) + ), + }, + ], +}; + +export default transforms; From 82c301a0a1fa76056dad79aae6c8485491986ad3 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Fri, 7 Feb 2020 15:46:04 +1100 Subject: [PATCH 05/15] Add .wp-env.json support to wp-env (#20002) * Add .wp-env.json support to wp-env * @wordpress/env: Maintain version at 0.4.0. This is bumped when packages are released * @wordpress/env: Fix copy/paste error in doc comment * @wordpress/env: Output stderr instead of error object --- .wp-env.json | 4 + package-lock.json | 1 + packages/env/CHANGELOG.md | 9 + packages/env/README.md | 86 +++- .../env/lib/build-docker-compose-config.js | 101 ++++ packages/env/lib/cli.js | 25 +- packages/env/lib/config.js | 247 +++++++++ .../env/lib/create-docker-compose-config.js | 76 --- ...ct-context.js => detect-directory-type.js} | 44 +- packages/env/lib/download-source.js | 85 ++++ packages/env/lib/env.js | 473 +++++++++++------- packages/env/lib/resolve-dependencies.js | 50 -- packages/env/package.json | 1 + packages/env/test/cli.js | 18 +- packages/env/test/config.js | 212 ++++++++ 15 files changed, 1061 insertions(+), 371 deletions(-) create mode 100644 .wp-env.json create mode 100644 packages/env/lib/build-docker-compose-config.js create mode 100644 packages/env/lib/config.js delete mode 100644 packages/env/lib/create-docker-compose-config.js rename packages/env/lib/{detect-context.js => detect-directory-type.js} (57%) create mode 100644 packages/env/lib/download-source.js delete mode 100644 packages/env/lib/resolve-dependencies.js create mode 100644 packages/env/test/config.js diff --git a/.wp-env.json b/.wp-env.json new file mode 100644 index 00000000000000..d67cd9642135da --- /dev/null +++ b/.wp-env.json @@ -0,0 +1,4 @@ +{ + "core": "WordPress/WordPress", + "plugins": [ "." ] +} diff --git a/package-lock.json b/package-lock.json index caa337f6af5ef3..d550960e905a44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10496,6 +10496,7 @@ "chalk": "^2.4.2", "copy-dir": "^1.2.0", "docker-compose": "^0.22.2", + "js-yaml": "^3.13.1", "nodegit": "^0.26.2", "ora": "^4.0.2", "terminal-link": "^2.0.0", diff --git a/packages/env/CHANGELOG.md b/packages/env/CHANGELOG.md index b769f45de65ae2..753cab8caca127 100644 --- a/packages/env/CHANGELOG.md +++ b/packages/env/CHANGELOG.md @@ -1,5 +1,14 @@ ## Master +### Breaking Changes + +- `wp-env start` no longer accepts a WordPress branch or tag reference as its argument. Instead, create a `.wp-env.json` file and specify a `"core"` field. +- `wp-env start` will now download WordPress into a hidden directory located in `~/.wp-env`. You may delete your `{projectName}-wordpress` and `{projectName}-tests-wordpress` directories. + +### New Feature + +- A `.wp-env.json` configuration file can now be used to specify the WordPress installation, plugins, and themes to use in the local development environment. + ## 0.4.0 (2020-02-04) ### Bug Fixes diff --git a/packages/env/README.md b/packages/env/README.md index 041ec7fd2947eb..9e4ac9f7b43876 100644 --- a/packages/env/README.md +++ b/packages/env/README.md @@ -138,13 +138,13 @@ $ wp-env start ### `wp-env start [ref]` ```sh -wp-env start [ref] +wp-env start Starts WordPress for development on port 8888 (​http://localhost:8888​) (override with WP_ENV_PORT) and tests on port 8889 (​http://localhost:8889​) -(override with WP_ENV_TESTS_PORT). If the current working directory is a plugin -and/or has e2e-tests with plugins and/or mu-plugins, they will be mounted -appropriately. +(override with WP_ENV_TESTS_PORT). The current working directory must be a +WordPress installation, a plugin, a theme, or contain a .wp-env.json file. + Positionals: ref A `https://github.com/WordPress/WordPress` git repo branch or commit for @@ -171,28 +171,84 @@ Positionals: [string] [choices: "all", "development", "tests"] [default: "tests"] ``` -## Running with multiple plugins and/or themes +## .wp-env.json + +You can customize the WordPress installation, plugins and themes that the development environment will use by specifying a `.wp-env.json` file in the directory that you run `wp-env` from. + +`.wp-env.json` supports three fields: + +| 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. | + +Several types of strings can be passed into these fields: -`wp-env` also supports a configuration file. At the moment, this is only used for loading extra themes and plugins that you may be developing together with your main one. The script will attach the specified theme and plugin directories as volumes on the docker containers so that changes you make to them exist in the WordPress instance. +| Type | Format | Example(s) | +| -- | -- | -- | +| Relative path | `.|~` | `"./a/directory"`, `"../a/directory"`, `"~/a/directory"` | +| Absolute path | `/|:\` | `"/a/directory"`, `"C:\\a\\directory"` | +| GitHub repository | `/[#]` | `"WordPress/WordPress"`, `"WordPress/gutenberg#master"` | -### Example: +Remote sources will be downloaded into a temporary directory located in `~/.wp-env`. + +### Examples + +#### Latest production WordPress + current directory as a plugin + +This is useful for plugin development. -`wp-env.json` ```json { - "themes": [ - "../path/to/theme/dir" - ], + "core": null, + "plugins": [ + "." + ] +} +``` + +#### Latest development WordPress + current directory as a plugin + +This is useful for plugin development when upstream Core changes need to be tested. + +```json +{ + "core": "WordPress/WordPress#master", + "plugins": [ + "." + ] +} +``` + +#### Local `wordpress-develop` + current directory as a plugin + +This is useful for working on plugins and WordPress Core at the same time. + +```json +{ + "core": "../wordpress-develop/build", "plugins": [ - "../path/to/plugin/dir" + "." ] } ``` -### Caveats: +#### A complete testing environment -The file should be located in the same directory from which you run `wp-env` commands for a project. So if you are running `wp-env` in the root directory of a plugin, `wp-env.json` should also be located there. +This is useful for integration testing: that is, testing how old versions of WordPress and different combinations of plugins and themes impact each other. -Each item in the `themes` or `plugins` array should be an absolute or relative path to the root of a different theme or plugin directory. Relative paths will be resolved from the current working directory, which means they will be resolved from the location of the `wp-env.json` file. +```json +{ + "core": "WordPress/WordPress#5.2.0", + "plugins": [ + "WordPress/wp-lazy-loading", + "WordPress/classic-editor", + ], + "themes": [ + "WordPress/theme-experiments" + ] +} +```

Code is Poetry.

diff --git a/packages/env/lib/build-docker-compose-config.js b/packages/env/lib/build-docker-compose-config.js new file mode 100644 index 00000000000000..22a3c92e078efb --- /dev/null +++ b/packages/env/lib/build-docker-compose-config.js @@ -0,0 +1,101 @@ +'use strict'; +/** + * External dependencies + */ +const fs = require( 'fs' ); +const path = require( 'path' ); + +/** + * @typedef {import('./config').Config} Config + */ + +/** + * Creates a docker-compose config object which, when serialized into a + * docker-compose.yml file, tells docker-compose how to run the environment. + * + * @param {Config} config A wp-env config object. + * @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 }`, + + // 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 themeMounts = config.themeSources.map( + ( source ) => + `${ source.path }:/var/www/html/wp-content/themes/${ source.basename }` + ); + + const developmentMounts = [ + `${ + config.coreSource ? config.coreSource.path : 'wordpress' + }:/var/www/html`, + ...pluginMounts, + ...themeMounts, + ]; + + const testsMounts = [ + `${ + config.coreSource ? config.coreSource.testsPath : 'tests-wordpress' + }:/var/www/html`, + ...pluginMounts, + ...themeMounts, + ]; + + return { + version: '3.7', + services: { + mysql: { + image: 'mariadb', + environment: { + MYSQL_ALLOW_EMPTY_PASSWORD: 'yes', + }, + }, + wordpress: { + depends_on: [ 'mysql' ], + image: 'wordpress', + ports: [ '${WP_ENV_PORT:-8888}:80' ], + environment: { + WORDPRESS_DEBUG: '1', + WORDPRESS_DB_NAME: 'wordpress', + }, + volumes: developmentMounts, + }, + 'tests-wordpress': { + depends_on: [ 'mysql' ], + image: 'wordpress', + ports: [ '${WP_ENV_TESTS_PORT:-8889}:80' ], + environment: { + WORDPRESS_DEBUG: '1', + WORDPRESS_DB_NAME: 'tests-wordpress', + }, + volumes: testsMounts, + }, + cli: { + depends_on: [ 'wordpress' ], + image: 'wordpress:cli', + volumes: developmentMounts, + }, + 'tests-cli': { + depends_on: [ 'wordpress' ], + image: 'wordpress:cli', + volumes: testsMounts, + }, + composer: { + image: 'composer', + volumes: [ `${ config.configDirectoryPath }:/app` ], + }, + }, + volumes: { + ...( ! config.coreSource && { wordpress: {} } ), + ...( ! config.coreSource && { 'tests-wordpress': {} } ), + }, + }; +}; diff --git a/packages/env/lib/cli.js b/packages/env/lib/cli.js index 13ce5ccc3f347b..b589e9cce5ecf7 100644 --- a/packages/env/lib/cli.js +++ b/packages/env/lib/cli.js @@ -33,11 +33,13 @@ const withSpinner = ( command ) => ( ...args ) => { ).toFixed( 0 ) }ms)` ); }, - ( err ) => { - spinner.fail( err.message || err.err ); - // eslint-disable-next-line no-console - console.error( `\n\n${ err.out || err.err }\n\n` ); - process.exit( err.exitCode || 1 ); + ( error ) => { + spinner.fail( error.message || error.err ); + if ( ! ( error instanceof env.ValidationError ) ) { + // eslint-disable-next-line no-console + console.error( `\n\n${ error.out || error.err }\n\n` ); + } + process.exit( error.exitCode || 1 ); } ); }; @@ -46,7 +48,7 @@ module.exports = function cli() { yargs.usage( wpPrimary( '$0 ' ) ); yargs.command( - 'start [ref]', + 'start', wpGreen( chalk`Starts WordPress for development on port {bold.underline ${ terminalLink( '8888', @@ -54,16 +56,9 @@ module.exports = function cli() { ) }} (override with WP_ENV_PORT) and tests on port {bold.underline ${ terminalLink( '8889', 'http://localhost:8889' - ) }} (override with WP_ENV_TESTS_PORT). If the current working directory is a plugin and/or has e2e-tests with plugins and/or mu-plugins, they will be mounted appropriately.` + ) }} (override with WP_ENV_TESTS_PORT). The current working directory must be a WordPress installation, a plugin, a theme, or contain a .wp-env.json file.` ), - ( args ) => { - args.positional( 'ref', { - type: 'string', - describe: - 'A `https://github.com/WordPress/WordPress` git repo branch or commit for choosing a specific version.', - default: 'master', - } ); - }, + () => {}, withSpinner( env.start ) ); yargs.command( diff --git a/packages/env/lib/config.js b/packages/env/lib/config.js new file mode 100644 index 00000000000000..61962d6691bb8a --- /dev/null +++ b/packages/env/lib/config.js @@ -0,0 +1,247 @@ +'use strict'; +/** + * External dependencies + */ +const fs = require( 'fs' ).promises; +const path = require( 'path' ); +const os = require( 'os' ); +const crypto = require( 'crypto' ); + +/** + * Internal dependencies + */ +const detectDirectoryType = require( './detect-directory-type' ); + +/** + * Error subtype which indicates that an expected validation erorr occured + * while reading wp-env configuration. + */ +class ValidationError extends Error {} + +/** + * The string at the beginning of a source path that points to a home-relative + * directory. Will be '~/' on unix environments and '~\' on Windows. + */ +const HOME_PATH_PREFIX = `~${ path.sep }`; + +/** + * A wp-env config object. + * + * @typedef Config + * @property {string} name Name of the environment. + * @property {string} configDirectoryPath Path to the .wp-env.json file. + * @property {string} workDirectoryPath Path to the work directory located in ~/.wp-env. + * @property {string} dockerComposeConfigPath Path to the docker-compose.yml file. + * @property {Source|null} coreSource The WordPress installation to load in the environment. + * @property {Source[]} pluginSources Plugins to load in the environment. + * @property {Source[]} themeSources Themes to load in the environment. + */ + +/** + * A WordPress installation, plugin or theme to be loaded into the environment. + * + * @typedef Source + * @property {string} type The source type. Can be 'local' or 'git'. + * @property {string} path The path to the WordPress installation, plugin or theme. + * @property {string} basename Name that identifies the WordPress installation, plugin or theme. + */ + +module.exports = { + ValidationError, + + /** + * Reads and parses the given .wp-env.json file into a wp-env config object. + * + * @param {string} configPath Path to the .wp-env.json file. + * @return {Config} A wp-env config object. + */ + async readConfig( configPath ) { + const configDirectoryPath = path.dirname( configPath ); + + let config = null; + + try { + config = JSON.parse( await fs.readFile( configPath, 'utf8' ) ); + } catch ( error ) { + if ( error.code === 'ENOENT' ) { + // Config file does not exist. Do nothing - it's optional. + } else if ( error instanceof SyntaxError ) { + throw new ValidationError( + `Invalid .wp-env.json: ${ error.message }` + ); + } else { + throw new ValidationError( + `Could not read .wp-env.json: ${ error.message }` + ); + } + } + + if ( config === null ) { + const type = await detectDirectoryType( configDirectoryPath ); + if ( type === 'core' ) { + config = { core: '.' }; + } else if ( type === 'plugin' ) { + config = { plugins: [ '.' ] }; + } else if ( type === 'theme' ) { + config = { themes: [ '.' ] }; + } else { + throw new ValidationError( + `No .wp-env.json file found at '${ configPath }' and could not determine if '${ configDirectoryPath }' is a WordPress installation, a plugin, or a theme.` + ); + } + } + + config = Object.assign( + { + core: null, + plugins: [], + themes: [], + }, + config + ); + + if ( config.core !== null && typeof config.core !== 'string' ) { + throw new ValidationError( + 'Invalid .wp-env.json: "core" must be null or a string.' + ); + } + + if ( + ! Array.isArray( config.plugins ) || + config.plugins.some( ( plugin ) => typeof plugin !== 'string' ) + ) { + throw new ValidationError( + 'Invalid .wp-env.json: "plugins" must be an array of strings.' + ); + } + + if ( + ! Array.isArray( config.themes ) || + config.themes.some( ( theme ) => typeof theme !== 'string' ) + ) { + throw new ValidationError( + 'Invalid .wp-env.json: "themes" must be an array of strings.' + ); + } + + const workDirectoryPath = path.resolve( + os.homedir(), + '.wp-env', + md5( configPath ) + ); + + return { + name: path.basename( configDirectoryPath ), + configDirectoryPath, + workDirectoryPath, + dockerComposeConfigPath: path.resolve( + workDirectoryPath, + 'docker-compose.yml' + ), + coreSource: includeTestsPath( + parseSourceString( config.core, { + workDirectoryPath, + } ) + ), + pluginSources: config.plugins.map( ( sourceString ) => + parseSourceString( sourceString, { + workDirectoryPath, + } ) + ), + themeSources: config.themes.map( ( sourceString ) => + parseSourceString( sourceString, { + workDirectoryPath, + } ) + ), + }; + }, +}; + +/** + * Parses a source string into a source object. + * + * @param {string|null} sourceString The source string. See README.md for documentation on valid source string patterns. + * @param {Object} options + * @param {boolean} options.hasTests Whether or not a `testsPath` is required. Only the 'core' source needs this. + * @param {string} options.workDirectoryPath Path to the work directory located in ~/.wp-env. + * @return {Source|null} A source object. + */ +function parseSourceString( sourceString, { workDirectoryPath } ) { + if ( sourceString === null ) { + return null; + } + + if ( + sourceString.startsWith( '.' ) || + sourceString.startsWith( HOME_PATH_PREFIX ) || + path.isAbsolute( sourceString ) + ) { + let sourcePath; + if ( sourceString.startsWith( HOME_PATH_PREFIX ) ) { + sourcePath = path.resolve( + os.homedir(), + sourceString.slice( HOME_PATH_PREFIX.length ) + ); + } else { + sourcePath = path.resolve( sourceString ); + } + const basename = path.basename( sourcePath ); + return { + type: 'local', + path: sourcePath, + basename, + }; + } + + const gitHubFields = sourceString.match( + /^([\w-]+)\/([\w-]+)(?:#([\w-]+))?$/ + ); + if ( gitHubFields ) { + return { + type: 'git', + url: `https://github.com/${ gitHubFields[ 1 ] }/${ gitHubFields[ 2 ] }.git`, + ref: gitHubFields[ 3 ] || 'master', + path: path.resolve( workDirectoryPath, gitHubFields[ 2 ] ), + basename: gitHubFields[ 2 ], + }; + } + + throw new ValidationError( + `Invalid or unrecognized source: "${ sourceString }."` + ); +} + +/** + * Given a source object, returns a new source object with the testsPath + * property set correctly. Only the 'core' source requires a testsPath. + * + * @param {Source|null} source A source object. + * @return {Source|null} A source object. + */ +function includeTestsPath( source ) { + if ( source === null ) { + return null; + } + + return { + ...source, + testsPath: path.resolve( + source.path, + '..', + 'tests-' + path.basename( source.path ) + ), + }; +} + +/** + * Hashes the given string using the MD5 algorithm. + * + * @param {string} data The string to hash. + * @return {string} An MD5 hash string. + */ +function md5( data ) { + return crypto + .createHash( 'md5' ) + .update( data ) + .digest( 'hex' ); +} diff --git a/packages/env/lib/create-docker-compose-config.js b/packages/env/lib/create-docker-compose-config.js deleted file mode 100644 index 9b5d89995da955..00000000000000 --- a/packages/env/lib/create-docker-compose-config.js +++ /dev/null @@ -1,76 +0,0 @@ -module.exports = function createDockerComposeConfig( - cwdTestsPath, - context, - dependencies -) { - const { path: cwd, pathBasename: cwdName } = context; - - const dependencyMappings = [ ...dependencies, context ] - .map( - ( { path, pathBasename, type } ) => - ` - ${ path }/:/var/www/html/wp-content/${ type }s/${ pathBasename }/\n` - ) - .join( '' ); - const commonVolumes = ` -${ dependencyMappings } - - ${ cwd }${ cwdTestsPath }/e2e-tests/mu-plugins/:/var/www/html/wp-content/mu-plugins/ - - ${ cwd }${ cwdTestsPath }/e2e-tests/plugins/:/var/www/html/wp-content/plugins/${ cwdName }-test-plugins/`; - const volumes = ` - - ${ cwd }/../${ cwdName }-wordpress/:/var/www/html/${ commonVolumes }`; - const testsVolumes = ` - - ${ cwd }/../${ cwdName }-tests-wordpress/:/var/www/html/${ commonVolumes }`; - return `version: '2.1' -volumes: - tests-wordpress-phpunit: -services: - mysql: - environment: - MYSQL_ROOT_PASSWORD: password - image: mariadb - wordpress: - depends_on: - - mysql - environment: - WORDPRESS_DEBUG: 1 - WORDPRESS_DB_NAME: wordpress - WORDPRESS_DB_PASSWORD: password - image: wordpress - ports: - - \${WP_ENV_PORT:-8888}:80 - volumes:${ volumes } - wordpress-cli: - depends_on: - - wordpress - image: wordpress:cli - volumes:${ volumes } - tests-wordpress: - depends_on: - - mysql - environment: - WORDPRESS_DEBUG: 1 - WORDPRESS_DB_NAME: tests-wordpress - WORDPRESS_DB_PASSWORD: password - image: wordpress - ports: - - \${WP_ENV_TESTS_PORT:-8889}:80 - volumes:${ testsVolumes } - tests-wordpress-cli: - depends_on: - - tests-wordpress - image: wordpress:cli - volumes:${ testsVolumes } - tests-wordpress-phpunit: - depends_on: - - mysql - environment: - PHPUNIT_DB_HOST: mysql - image: chriszarate/wordpress-phpunit - volumes: - - ${ cwd }/:/app/ - - tests-wordpress-phpunit/:/tmp/ - composer: - image: composer - volumes: - - ${ cwd }/:/app/ -`; -}; diff --git a/packages/env/lib/detect-context.js b/packages/env/lib/detect-directory-type.js similarity index 57% rename from packages/env/lib/detect-context.js rename to packages/env/lib/detect-directory-type.js index b962287bddc238..b685685c76c0cf 100644 --- a/packages/env/lib/detect-context.js +++ b/packages/env/lib/detect-directory-type.js @@ -10,40 +10,41 @@ const path = require( 'path' ); /** * Promisified dependencies */ +const exists = util.promisify( fs.exists ); const readDir = util.promisify( fs.readdir ); const finished = util.promisify( stream.finished ); /** - * @typedef Context - * @type {Object} - * @property {string} type - * @property {string} path - * @property {string} pathBasename - */ - -/** - * Detects the context of a given path. - * - * @param {string} [directoryPath=process.cwd()] The directory to detect. Should point to a directory, defaulting to the current working directory. + * Detects whether the given directory is a WordPress installation, a plugin or a theme. * - * @return {Context} The context of the directory. If a theme or plugin, the type property will contain 'theme' or 'plugin'. + * @param {string} directoryPath The directory to detect. + * @return {string|null} 'core' if the directory is a WordPress installation, 'plugin' if it is a plugin, 'theme' if it is a theme, or null if we can't tell. */ -module.exports = async function detectContext( directoryPath = process.cwd() ) { - const context = {}; +module.exports = async function detectDirectoryType( directoryPath ) { + // If we have a `wp-includes/version.php` file, then this is a Core install. + if ( + await exists( + path.resolve( directoryPath, 'wp-includes', 'version.php' ) + ) + ) { + return 'core'; + } + + let result = null; // Use absolute paths to files so that we can properly read // dependencies not in the current working directory. - const absPath = path.resolve( directoryPath ); + const absolutePath = path.resolve( directoryPath ); // Race multiple file read streams against each other until // a plugin or theme header is found. - const files = ( await readDir( absPath ) ) + const files = ( await readDir( absolutePath ) ) .filter( ( file ) => path.extname( file ) === '.php' || path.basename( file ) === 'style.css' ) - .map( ( fileName ) => path.join( absPath, fileName ) ); + .map( ( fileName ) => path.join( absolutePath, fileName ) ); const streams = []; for ( const file of files ) { @@ -52,11 +53,10 @@ module.exports = async function detectContext( directoryPath = process.cwd() ) { const [ , type ] = text.match( /(Plugin|Theme) Name: .*[\r\n]/ ) || []; if ( type ) { - context.type = type.toLowerCase(); - context.path = absPath; - context.pathBasename = path.basename( absPath ); + result = type.toLowerCase(); - // Stop the creation of new streams by mutating the iterated array. We can't `break`, because we are inside a function. + // Stop the creation of new streams by mutating the iterated + // array. We can't `break`, because we are inside a function. files.splice( 0 ); fileStream.destroy(); streams.forEach( ( otherFileStream ) => @@ -72,5 +72,5 @@ module.exports = async function detectContext( directoryPath = process.cwd() ) { ) ); - return context; + return result; }; diff --git a/packages/env/lib/download-source.js b/packages/env/lib/download-source.js new file mode 100644 index 00000000000000..507db9db5aa697 --- /dev/null +++ b/packages/env/lib/download-source.js @@ -0,0 +1,85 @@ +'use strict'; +/** + * External dependencies + */ +const NodeGit = require( 'nodegit' ); + +/** + * @typedef {import('./config').Source} Source + */ + +/** + * Downloads the given source if necessary. The specific action taken depends + * on the source type. + * + * @param {Source} source The source to download. + * @param {Object} options + * @param {Function} options.onProgress A function called with download progress. Will be invoked with one argument: a number that ranges from 0 to 1 which indicates current download progress for this source. + */ +module.exports = async function downloadSource( source, options ) { + if ( source.type === 'git' ) { + await downloadGitSource( source, options ); + } +}; + +/** + * Clones the git repository at `source.url` into `source.path`. If the + * repository already exists, it is updated instead. + * + * @param {Source} source The source to download. + * @param {Object} options + * @param {Function} options.onProgress A function called with download progress. Will be invoked with one argument: a number that ranges from 0 to 1 which indicates current download progress for this source. + */ +async function downloadGitSource( source, { onProgress } ) { + onProgress( 0 ); + + const gitFetchOptions = { + fetchOpts: { + callbacks: { + transferProgress( progress ) { + // Fetches are finished when all objects are received and indexed, + // so received objects plus indexed objects should equal twice + // the total number of objects when done. + onProgress( + ( progress.receivedObjects() + + progress.indexedObjects() ) / + ( progress.totalObjects() * 2 ) + ); + }, + }, + }, + }; + + // Clone or get the repo. + const repository = await NodeGit.Clone( + source.url, + source.path, + gitFetchOptions + ) + // Repo already exists, get it. + .catch( () => NodeGit.Repository.open( source.path ) ); + + // Checkout the specified ref. + const remote = await repository.getRemote( 'origin' ); + await remote.fetch( source.ref, gitFetchOptions.fetchOpts ); + await remote.disconnect(); + try { + await repository.checkoutRef( + await repository + .getReference( 'FETCH_HEAD' ) + // Sometimes git doesn't update FETCH_HEAD for things + // like tags so we try another method here. + .catch( + repository.getReference.bind( repository, source.ref ) + ), + { + checkoutStrategy: NodeGit.Checkout.STRATEGY.FORCE, + } + ); + } catch ( error ) { + // Some commit refs need to be set as detached. + await repository.setHeadDetached( source.ref ); + } + + onProgress( 1 ); +} diff --git a/packages/env/lib/env.js b/packages/env/lib/env.js index 0610e90510308f..94421544d2ff90 100644 --- a/packages/env/lib/env.js +++ b/packages/env/lib/env.js @@ -4,236 +4,189 @@ */ const util = require( 'util' ); const path = require( 'path' ); -const fs = require( 'fs' ); +const fs = require( 'fs' ).promises; const dockerCompose = require( 'docker-compose' ); -const NodeGit = require( 'nodegit' ); +const yaml = require( 'js-yaml' ); + +/** + * Promisified dependencies + */ +const copyDir = util.promisify( require( 'copy-dir' ) ); +const sleep = util.promisify( setTimeout ); /** * Internal dependencies */ -const createDockerComposeConfig = require( './create-docker-compose-config' ); -const detectContext = require( './detect-context' ); -const resolveDependencies = require( './resolve-dependencies' ); +const { ValidationError, readConfig } = require( './config' ); +const downloadSource = require( './download-source' ); +const buildDockerComposeConfig = require( './build-docker-compose-config' ); /** - * Promisified dependencies + * @typedef {import('./config').Config} Config */ -const copyDir = util.promisify( require( 'copy-dir' ) ); -const wait = util.promisify( setTimeout ); - -// Config Variables -const cwd = process.cwd(); -const cwdName = path.basename( cwd ); -const cwdTestsPath = fs.existsSync( './packages' ) ? '/packages' : ''; -const dockerComposeOptions = { - config: path.join( __dirname, 'docker-compose.yml' ), -}; -const hasConfigFile = fs.existsSync( dockerComposeOptions.config ); - -// WP CLI Utils -const wpCliRun = ( command, isTests = false ) => - dockerCompose.run( - `${ isTests ? 'tests-' : '' }wordpress-cli`, - command, - dockerComposeOptions - ); -const setupSite = ( isTests = false ) => - wpCliRun( - `wp core install --url=localhost:${ - isTests - ? process.env.WP_ENV_TESTS_PORT || 8889 - : process.env.WP_ENV_PORT || 8888 - } --title=${ cwdName } --admin_user=admin --admin_password=password --admin_email=admin@wordpress.org`, - isTests - ); -const activateContext = ( { type, pathBasename }, isTests = false ) => - wpCliRun( `wp ${ type } activate ${ pathBasename }`, isTests ); -const resetDatabase = ( isTests = false ) => - wpCliRun( 'wp db reset --yes', isTests ); module.exports = { - async start( { ref, spinner = {} } ) { - const context = await detectContext(); - const dependencies = await resolveDependencies(); - - spinner.text = `Downloading WordPress@${ ref } 0/100%.`; - const gitFetchOptions = { - fetchOpts: { - callbacks: { - transferProgress( progress ) { - // Fetches are finished when all objects are received and indexed, - // so received objects plus indexed objects should equal twice - // the total number of objects when done. - const percent = ( - ( ( progress.receivedObjects() + - progress.indexedObjects() ) / - ( progress.totalObjects() * 2 ) ) * - 100 - ).toFixed( 0 ); - spinner.text = `Downloading WordPress@${ ref } ${ percent }/100%.`; - }, - }, - }, + /** + * Starts the development server. + * + * @param {Object} options + * @param {Object} options.spinner A CLI spinner which indicates progress. + */ + async start( { spinner } ) { + const config = await initConfig(); + + spinner.text = 'Downloading WordPress.'; + + const progresses = {}; + const getProgressSetter = ( id ) => ( progress ) => { + progresses[ id ] = progress; + spinner.text = + 'Downloading WordPress.\n' + + Object.entries( progresses ) + .map( + ( [ key, value ] ) => + ` - ${ key }: ${ ( value * 100 ).toFixed( + 0 + ) }/100%` + ) + .join( '\n' ); }; - // Clone or get the repo. - const repoPath = `../${ cwdName }-wordpress/`; - const repo = await NodeGit.Clone( - 'https://github.com/WordPress/WordPress.git', - repoPath, - gitFetchOptions - ) - // Repo already exists, get it. - .catch( () => NodeGit.Repository.open( repoPath ) ); - - // Checkout the specified ref. - const remote = await repo.getRemote( 'origin' ); - await remote.fetch( ref, gitFetchOptions.fetchOpts ); - await remote.disconnect(); - try { - await repo.checkoutRef( - await repo - .getReference( 'FETCH_HEAD' ) - // Sometimes git doesn't update FETCH_HEAD for things - // like tags so we try another method here. - .catch( repo.getReference.bind( repo, ref ) ), - { - checkoutStrategy: NodeGit.Checkout.STRATEGY.FORCE, + await Promise.all( [ + // Preemptively start the database while we wait for sources to download. + dockerCompose.upOne( 'mysql', { + config: config.dockerComposeConfigPath, + } ), + + ( async () => { + if ( config.coreSource ) { + await downloadSource( config.coreSource, { + onProgress: getProgressSetter( 'core' ), + } ); + await copyCoreFiles( + config.coreSource.path, + config.coreSource.testsPath + ); } - ); - } catch ( err ) { - // Some commit refs need to be set as detached. - await repo.setHeadDetached( ref ); - } + } )(), - // Duplicate repo for the tests container. - let stashed = true; // Stash to avoid copying config changes. - try { - await NodeGit.Stash.save( - repo, - await NodeGit.Signature.default( repo ), - null, - NodeGit.Stash.FLAGS.INCLUDE_UNTRACKED - ); - } catch ( err ) { - stashed = false; - } - await copyDir( repoPath, `../${ cwdName }-tests-wordpress/`, { - filter: ( stat, filepath ) => - stat !== 'symbolicLink' && - ( stat !== 'directory' || - ( filepath !== `${ repoPath }.git` && - ! filepath.endsWith( 'node_modules' ) ) ), - } ); - if ( stashed ) { - try { - await NodeGit.Stash.pop( repo, 0 ); - } catch ( err ) {} - } - spinner.text = `Downloading WordPress@${ ref } 100/100%.`; + ...config.pluginSources.map( ( source ) => + downloadSource( source, { + onProgress: getProgressSetter( source.basename ), + } ) + ), - spinner.text = `Starting WordPress@${ ref }.`; - fs.writeFileSync( - dockerComposeOptions.config, - createDockerComposeConfig( cwdTestsPath, context, dependencies ) - ); + ...config.themeSources.map( ( source ) => + downloadSource( source, { + onProgress: getProgressSetter( source.basename ), + } ) + ), + ] ); - // These will bring up the database container, - // because it's a dependency. - await dockerCompose.upMany( - [ 'wordpress', 'tests-wordpress' ], - dockerComposeOptions - ); + spinner.text = 'Starting WordPress.'; - const retryableSiteSetup = async () => { - try { - await Promise.all( [ setupSite(), setupSite( true ) ] ); - } catch ( err ) { - await wait( 5000 ); - throw err; - } - }; - // Try a few times, in case - // the database wasn't ready. - await retryableSiteSetup() - .catch( retryableSiteSetup ) - .catch( retryableSiteSetup ); + await dockerCompose.upMany( [ 'wordpress', 'tests-wordpress' ], { + config: config.dockerComposeConfigPath, + } ); + try { + await checkDatabaseConnection( config ); + } catch ( error ) { + // Wait 30 seconds for MySQL to accept connections. + await retry( () => checkDatabaseConnection( config ), { + times: 30, + delay: 1000, + } ); + + // It takes 3-4 seconds for MySQL to be ready after it starts accepting connections. + await sleep( 4000 ); + } + + // Retry WordPress installation in case MySQL *still* wasn't ready. await Promise.all( [ - activateContext( context ), - activateContext( context, true ), - ...dependencies.map( activateContext ), + retry( () => configureWordPress( 'development', config ), { + times: 2, + } ), + retry( () => configureWordPress( 'tests', config ), { times: 2 } ), ] ); - // Remove dangling containers and finish. - await dockerCompose.rm( dockerComposeOptions ); - spinner.text = `Started WordPress@${ ref }.`; + spinner.text = 'WordPress started.'; }, - async stop( { spinner = {} } ) { + /** + * Stops the development server. + * + * @param {Object} options + * @param {Object} options.spinner A CLI spinner which indicates progress. + */ + async stop( { spinner } ) { + const { dockerComposeConfigPath } = await initConfig(); + spinner.text = 'Stopping WordPress.'; - await dockerCompose.stop( dockerComposeOptions ); + + await dockerCompose.down( { config: dockerComposeConfigPath } ); + spinner.text = 'Stopped WordPress.'; }, + /** + * Wipes the development server's database, the tests server's database, or both. + * + * @param {Object} options + * @param {string} options.environment The environment to clean. Either 'development', 'tests', or 'all'. + * @param {Object} options.spinner A CLI spinner which indicates progress. + */ async clean( { environment, spinner } ) { - const context = await detectContext(); - const dependencies = await resolveDependencies(); - const activateDependencies = () => - Promise.all( dependencies.map( activateContext ) ); + const config = await initConfig(); const description = `${ environment } environment${ environment === 'all' ? 's' : '' }`; spinner.text = `Cleaning ${ description }.`; - // Parallelize task sequences for each environment. const tasks = []; + if ( environment === 'all' || environment === 'development' ) { tasks.push( - resetDatabase() - .then( setupSite ) - .then( activateContext.bind( null, context ) ) - .then( activateDependencies ) + resetDatabase( 'development', config ) + .then( () => configureWordPress( 'development', config ) ) .catch( () => {} ) ); } + if ( environment === 'all' || environment === 'tests' ) { tasks.push( - resetDatabase( true ) - .then( setupSite.bind( null, true ) ) - .then( activateContext.bind( null, context, true ) ) + resetDatabase( 'tests', config ) + .then( () => configureWordPress( 'tests', config ) ) .catch( () => {} ) ); } + await Promise.all( tasks ); - // Remove dangling containers and finish. - await dockerCompose.rm( dockerComposeOptions ); spinner.text = `Cleaned ${ description }.`; }, + /** + * Runs an arbitrary command on the given Docker container. + * + * @param {Object} options + * @param {Object} options.container The Docker container to run the command on. + * @param {Object} options.command The command to run. + * @param {Object} options.spinner A CLI spinner which indicates progress. + */ async run( { container, command, spinner } ) { + const config = await initConfig(); + command = command.join( ' ' ); - spinner.text = `Running \`${ command }\` in "${ container }".`; - - // Generate config file if we don't have one. - if ( ! hasConfigFile ) { - fs.writeFileSync( - dockerComposeOptions.config, - createDockerComposeConfig( - cwdTestsPath, - await detectContext(), - await resolveDependencies() - ) - ); - } - const result = await dockerCompose.run( - container, - command, - dockerComposeOptions - ); + spinner.text = `Running \`${ command }\` in '${ container }'.`; + + const result = await dockerCompose.run( container, command, { + config: config.dockerComposeConfigPath, + commandOptions: [ '--rm' ], + } ); + if ( result.out ) { // eslint-disable-next-line no-console console.log( @@ -247,8 +200,168 @@ module.exports = { throw result.err; } - // Remove dangling containers and finish. - await dockerCompose.rm( dockerComposeOptions ); - spinner.text = `Ran \`${ command }\` in "${ container }".`; + spinner.text = `Ran \`${ command }\` in '${ container }'.`; }, + + ValidationError, }; + +/** + * Initializes the local environment so that Docker commands can be run. Reads + * ./.wp-env.json, creates ~/.wp-env, and creates ~/.wp-env/docker-compose.yml. + * + * @return {Config} The-env config object. + */ +async function initConfig() { + const configPath = path.resolve( '.wp-env.json' ); + const config = await readConfig( configPath ); + + await fs.mkdir( config.workDirectoryPath, { recursive: true } ); + + await fs.writeFile( + config.dockerComposeConfigPath, + yaml.dump( buildDockerComposeConfig( config ) ) + ); + + return config; +} + +/** + * Copies a WordPress installation, taking care to ignore large directories + * (.git, node_modules) and configuration files (wp-config.php). + * + * @param {string} fromPath Path to the WordPress directory to copy. + * @param {string} toPath Destination path. + */ +async function copyCoreFiles( fromPath, toPath ) { + await copyDir( fromPath, toPath, { + filter( stat, filepath, filename ) { + if ( stat === 'symbolicLink' ) { + return false; + } + if ( stat === 'directory' && filename === '.git' ) { + return false; + } + if ( stat === 'directory' && filename === 'node_modules' ) { + return false; + } + if ( stat === 'file' && filename === 'wp-config.php' ) { + return false; + } + return true; + }, + } ); +} + +/** + * Performs the given action again and again until it does not throw an error. + * + * @param {Function} action The action to perform. + * @param {Object} options + * @param {number} options.times How many times to try before giving up. + * @param {number} [options.delay=5000] How long, in milliseconds, to wait between each try. + */ +async function retry( action, { times, delay = 5000 } ) { + let tries = 0; + while ( true ) { + try { + return await action(); + } catch ( error ) { + if ( ++tries >= times ) { + throw error; + } + await sleep( delay ); + } + } +} + +/** + * Checks a WordPress database connection. An error is thrown if the test is + * unsuccessful. + * + * @param {Config} config The wp-env config object. + */ +async function checkDatabaseConnection( { dockerComposeConfigPath } ) { + await dockerCompose.run( 'cli', 'wp db check', { + config: dockerComposeConfigPath, + commandOptions: [ '--rm' ], + } ); +} + +/** + * Configures WordPress for the given environment by installing WordPress, + * activating all plugins, and activating the first theme. These steps are + * performed sequentially so as to not overload the WordPress instance. + * + * @param {string} environment The environment to configure. Either 'development' or 'tests'. + * @param {Config} config The wp-env config object. + */ +async function configureWordPress( environment, config ) { + const options = { + config: config.dockerComposeConfigPath, + commandOptions: [ '--rm' ], + }; + + // Install WordPress. + await dockerCompose.run( + environment === 'development' ? 'cli' : 'tests-cli', + `wp core install + --url=localhost:${ + environment === 'development' + ? process.env.WP_ENV_PORT || '8888' + : process.env.WP_ENV_TESTS_PORT || '8889' + } + --title='${ config.name }' + --admin_user=admin + --admin_password=password + --admin_email=wordpress@example.com + --skip-email`, + options + ); + + // Activate all plugins. + for ( const pluginSource of config.pluginSources ) { + await dockerCompose.run( + environment === 'development' ? 'cli' : 'tests-cli', + `wp plugin activate ${ pluginSource.basename }`, + options + ); + } + + // Activate the first theme. + const [ themeSource ] = config.themeSources; + if ( themeSource ) { + await dockerCompose.run( + environment === 'development' ? 'cli' : 'tests-cli', + `wp theme activate ${ themeSource.basename }`, + options + ); + } +} + +/** + * Resets the development server's database, the tests server's database, or both. + * + * @param {string} environment The environment to clean. Either 'development', 'tests', or 'all'. + * @param {Config} config The wp-env config object. + */ +async function resetDatabase( environment, { dockerComposeConfigPath } ) { + const options = { + config: dockerComposeConfigPath, + commandOptions: [ '--rm' ], + }; + + const tasks = []; + + if ( environment === 'all' || environment === 'development' ) { + tasks.push( dockerCompose.run( 'cli', 'wp db reset --yes', options ) ); + } + + if ( environment === 'all' || environment === 'tests' ) { + tasks.push( + dockerCompose.run( 'tests-cli', 'wp db reset --yes', options ) + ); + } + + await Promise.all( tasks ); +} diff --git a/packages/env/lib/resolve-dependencies.js b/packages/env/lib/resolve-dependencies.js deleted file mode 100644 index b812f6a6213041..00000000000000 --- a/packages/env/lib/resolve-dependencies.js +++ /dev/null @@ -1,50 +0,0 @@ -'use strict'; - -/** - * External dependencies - */ -const util = require( 'util' ); -const fs = require( 'fs' ); - -/** - * Internal dependencies - */ -const detectContext = require( './detect-context' ); - -/** - * Promisified dependencies - */ -const readFile = util.promisify( fs.readFile ); - -/** - * Returns an array of dependencies to be mounted in the Docker image. - * - * Reads from the wp-env.json file in the current directory and uses detect - * context to make sure the specified dependencies exist and are plugins - * and/or themes. - * - * @return {Array} An array of dependencies in the context format. - */ -module.exports = async function resolveDependencies() { - let envFile; - try { - envFile = await readFile( './wp-env.json' ); - } catch ( error ) { - return []; - } - - const { themes, plugins } = JSON.parse( envFile ); - - const dependencyResolvers = []; - if ( Array.isArray( themes ) ) { - dependencyResolvers.push( ...themes.map( detectContext ) ); - } - - if ( Array.isArray( plugins ) ) { - dependencyResolvers.push( ...plugins.map( detectContext ) ); - } - - // Return all dependencies which have been detected to be a plugin or a theme. - const dependencies = await Promise.all( dependencyResolvers ); - return dependencies.filter( ( { type } ) => !! type ); -}; diff --git a/packages/env/package.json b/packages/env/package.json index 64b8bd6bdeabfb..663a0711abf471 100644 --- a/packages/env/package.json +++ b/packages/env/package.json @@ -35,6 +35,7 @@ "chalk": "^2.4.2", "copy-dir": "^1.2.0", "docker-compose": "^0.22.2", + "js-yaml": "^3.13.1", "nodegit": "^0.26.2", "ora": "^4.0.2", "terminal-link": "^2.0.0", diff --git a/packages/env/test/cli.js b/packages/env/test/cli.js index 8eaaf4b5eb1633..ef19cc816dd0b3 100644 --- a/packages/env/test/cli.js +++ b/packages/env/test/cli.js @@ -17,21 +17,15 @@ jest.mock( '../lib/env', () => ( { start: jest.fn( Promise.resolve.bind( Promise ) ), stop: jest.fn( Promise.resolve.bind( Promise ) ), clean: jest.fn( Promise.resolve.bind( Promise ) ), + ValidationError: jest.requireActual( '../lib/env' ).ValidationError, } ) ); describe( 'env cli', () => { beforeEach( jest.clearAllMocks ); - it( 'parses start commands for the default ref.', () => { + it( 'parses start commands.', () => { cli().parse( [ 'start' ] ); - const { ref, spinner } = env.start.mock.calls[ 0 ][ 0 ]; - expect( ref ).toBe( 'master' ); - expect( spinner.text ).toBe( '' ); - } ); - it( 'parses start commands for an explicit ref.', () => { - cli().parse( [ 'start', 'explicit' ] ); - const { ref, spinner } = env.start.mock.calls[ 0 ][ 0 ]; - expect( ref ).toBe( 'explicit' ); + const { spinner } = env.start.mock.calls[ 0 ][ 0 ]; expect( spinner.text ).toBe( '' ); } ); @@ -103,9 +97,7 @@ describe( 'env cli', () => { await env.start.mock.results[ 0 ].value.catch( () => {} ); expect( spinner.fail ).toHaveBeenCalledWith( 'failure message' ); - expect( console.error ).toHaveBeenCalledWith( - '\n\nfailure message\n\n' - ); + expect( console.error ).toHaveBeenCalled(); expect( process.exit ).toHaveBeenCalledWith( 2 ); console.error = consoleError; process.exit = processExit; @@ -124,7 +116,7 @@ describe( 'env cli', () => { await env.start.mock.results[ 0 ].value.catch( () => {} ); expect( spinner.fail ).toHaveBeenCalledWith( 'failure error' ); - expect( console.error ).toHaveBeenCalledWith( '\n\nfailure error\n\n' ); + expect( console.error ).toHaveBeenCalled(); expect( process.exit ).toHaveBeenCalledWith( 1 ); console.error = consoleError; process.exit = processExit; diff --git a/packages/env/test/config.js b/packages/env/test/config.js new file mode 100644 index 00000000000000..5428ddc88ec446 --- /dev/null +++ b/packages/env/test/config.js @@ -0,0 +1,212 @@ +'use strict'; +/** + * External dependencies + */ +const { readFile } = require( 'fs' ).promises; + +/** + * Internal dependencies + */ +const { readConfig, ValidationError } = require( '../lib/config' ); +const detectDirectoryType = require( '../lib/detect-directory-type' ); + +jest.mock( 'fs', () => ( { + promises: { + readFile: jest.fn(), + }, +} ) ); + +jest.mock( '../lib/detect-directory-type', () => jest.fn() ); + +describe( 'readConfig', () => { + beforeEach( () => { + jest.clearAllMocks(); + } ); + + it( 'should throw a validation error if config is invalid JSON', async () => { + readFile.mockImplementation( () => Promise.resolve( '{' ) ); + expect.assertions( 2 ); + try { + await readConfig( '.wp-env.json' ); + } catch ( error ) { + expect( error ).toBeInstanceOf( ValidationError ); + expect( error.message ).toContain( 'Invalid .wp-env.json' ); + } + } ); + + it( 'should throw a validation error if config cannot be read', async () => { + readFile.mockImplementation( () => + Promise.reject( { message: 'Uh oh!' } ) + ); + expect.assertions( 2 ); + try { + await readConfig( '.wp-env.json' ); + } catch ( error ) { + expect( error ).toBeInstanceOf( ValidationError ); + expect( error.message ).toContain( 'Could not read .wp-env.json' ); + } + } ); + + it( 'should infer a core config when ran from a core directory', async () => { + readFile.mockImplementation( () => + Promise.reject( { code: 'ENOENT' } ) + ); + detectDirectoryType.mockImplementation( () => 'core' ); + const config = await readConfig( '.wp-env.json' ); + expect( config.coreSource ).not.toBeNull(); + expect( config.pluginSources ).toHaveLength( 0 ); + expect( config.themeSources ).toHaveLength( 0 ); + } ); + + it( 'should infer a plugin config when ran from a plugin directory', async () => { + readFile.mockImplementation( () => + Promise.reject( { code: 'ENOENT' } ) + ); + detectDirectoryType.mockImplementation( () => 'plugin' ); + const config = await readConfig( '.wp-env.json' ); + expect( config.coreSource ).toBeNull(); + expect( config.pluginSources ).toHaveLength( 1 ); + expect( config.themeSources ).toHaveLength( 0 ); + } ); + + it( 'should infer a theme config when ran from a theme directory', async () => { + readFile.mockImplementation( () => + Promise.reject( { code: 'ENOENT' } ) + ); + detectDirectoryType.mockImplementation( () => 'theme' ); + const config = await readConfig( '.wp-env.json' ); + expect( config.coreSource ).toBeNull(); + expect( config.pluginSources ).toHaveLength( 0 ); + expect( config.themeSources ).toHaveLength( 1 ); + } ); + + it( "should throw a validation error if 'core' is not a string", async () => { + readFile.mockImplementation( () => + Promise.resolve( JSON.stringify( { core: 123 } ) ) + ); + expect.assertions( 2 ); + try { + await readConfig( '.wp-env.json' ); + } catch ( error ) { + expect( error ).toBeInstanceOf( ValidationError ); + expect( error.message ).toContain( 'must be null or a string' ); + } + } ); + + it( "should throw a validation error if 'plugins' is not an array of strings", async () => { + readFile.mockImplementation( () => + Promise.resolve( JSON.stringify( { plugins: [ 'test', 123 ] } ) ) + ); + expect.assertions( 2 ); + try { + await readConfig( '.wp-env.json' ); + } catch ( error ) { + expect( error ).toBeInstanceOf( ValidationError ); + expect( error.message ).toContain( 'must be an array of strings' ); + } + } ); + + it( "should throw a validation error if 'themes' is not an array of strings", async () => { + readFile.mockImplementation( () => + Promise.resolve( JSON.stringify( { themes: [ 'test', 123 ] } ) ) + ); + expect.assertions( 2 ); + try { + await readConfig( '.wp-env.json' ); + } catch ( error ) { + expect( error ).toBeInstanceOf( ValidationError ); + expect( error.message ).toContain( 'must be an array of strings' ); + } + } ); + + it( 'should parse local sources', async () => { + readFile.mockImplementation( () => + Promise.resolve( + JSON.stringify( { + plugins: [ './relative', '../parent', '~/home' ], + } ) + ) + ); + const config = await readConfig( '.wp-env.json' ); + expect( config ).toMatchObject( { + pluginSources: [ + { + type: 'local', + path: expect.stringMatching( /^\/.*relative$/ ), + basename: 'relative', + }, + { + type: 'local', + path: expect.stringMatching( /^\/.*parent$/ ), + basename: 'parent', + }, + { + type: 'local', + path: expect.stringMatching( /^\/.*home$/ ), + basename: 'home', + }, + ], + } ); + } ); + + it( "should set testsPath on the 'core' source", async () => { + readFile.mockImplementation( () => + Promise.resolve( JSON.stringify( { core: './relative' } ) ) + ); + const config = await readConfig( '.wp-env.json' ); + expect( config ).toMatchObject( { + coreSource: { + type: 'local', + path: expect.stringMatching( /^\/.*relative$/ ), + testsPath: expect.stringMatching( /^\/.*tests-relative$/ ), + }, + } ); + } ); + + it( 'should parse GitHub sources', async () => { + readFile.mockImplementation( () => + Promise.resolve( + JSON.stringify( { + plugins: [ + 'WordPress/gutenberg', + 'WordPress/gutenberg#master', + ], + } ) + ) + ); + const config = await readConfig( '.wp-env.json' ); + expect( config ).toMatchObject( { + pluginSources: [ + { + type: 'git', + url: 'https://github.com/WordPress/gutenberg.git', + ref: 'master', + path: expect.stringMatching( /^\/.*gutenberg$/ ), + basename: 'gutenberg', + }, + { + type: 'git', + url: 'https://github.com/WordPress/gutenberg.git', + ref: 'master', + path: expect.stringMatching( /^\/.*gutenberg$/ ), + basename: 'gutenberg', + }, + ], + } ); + } ); + + it( 'should throw a validaton error if there is an unknown source', async () => { + readFile.mockImplementation( () => + Promise.resolve( JSON.stringify( { plugins: [ 'invalid' ] } ) ) + ); + expect.assertions( 2 ); + try { + await readConfig( '.wp-env.json' ); + } catch ( error ) { + expect( error ).toBeInstanceOf( ValidationError ); + expect( error.message ).toContain( + 'Invalid or unrecognized source' + ); + } + } ); +} ); From c605373f3e845da983193d2fa649d9a3cf777de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= Date: Fri, 7 Feb 2020 07:32:25 +0100 Subject: [PATCH 06/15] Scripts: Ensure the default Prettier config is used with `lint-js` when needed (#20071) * Scripts: Ensure the default Prettier config is used with `lint-js` when necessary * Apply suggestions from code review Co-Authored-By: Enrique Piqueras Co-authored-by: Enrique Piqueras --- packages/scripts/CHANGELOG.md | 4 ++++ packages/scripts/config/.eslintrc.js | 22 +++++++++++++++++++++- packages/scripts/scripts/format-js.js | 14 ++------------ packages/scripts/utils/config.js | 11 +++++++++++ packages/scripts/utils/index.js | 8 +++++++- 5 files changed, 45 insertions(+), 14 deletions(-) diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index 9d89a3bca8194f..832d66261997c0 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -1,5 +1,9 @@ ## Master +### Bug Fixes + +- Ensure the default Prettier config is used in the `lint-js` script when no Prettier config is found in the project ([#20071](https://github.com/WordPress/gutenberg/pull/20071)). + ## 7.0.0 (2020-02-04) ### Breaking Changes diff --git a/packages/scripts/config/.eslintrc.js b/packages/scripts/config/.eslintrc.js index 98c2e716c390c8..b3bb9bce2d2d32 100644 --- a/packages/scripts/config/.eslintrc.js +++ b/packages/scripts/config/.eslintrc.js @@ -1,4 +1,10 @@ -module.exports = { +/** + * Internal dependencies + */ +const defaultPrettierConfig = require( './.prettierrc' ); +const { hasPrettierConfig } = require( '../utils' ); + +const eslintConfig = { root: true, extends: [ 'plugin:@wordpress/eslint-plugin/recommended', @@ -10,3 +16,17 @@ module.exports = { 'plugin:@wordpress/eslint-plugin/test-unit', ], }; + +if ( ! hasPrettierConfig() ) { + eslintConfig.rules = { + 'prettier/prettier': [ + 'error', + defaultPrettierConfig, + { + usePrettierrc: false, + }, + ], + }; +} + +module.exports = eslintConfig; diff --git a/packages/scripts/scripts/format-js.js b/packages/scripts/scripts/format-js.js index 867f882e2c589a..5210d114b26a75 100644 --- a/packages/scripts/scripts/format-js.js +++ b/packages/scripts/scripts/format-js.js @@ -21,7 +21,7 @@ const { getArgFromCLI, getFileArgsFromCLI, hasArgInCLI, - hasPackageProp, + hasPrettierConfig, hasProjectFile, } = require( '../utils' ); @@ -72,18 +72,8 @@ if ( ! checkResult.success ) { // needed for config, otherwise pass in args to default config in packages // See: https://prettier.io/docs/en/configuration.html let configArgs = []; -const hasProjectPrettierConfig = - hasProjectFile( '.prettierrc.js' ) || - hasProjectFile( '.prettierrc.json' ) || - hasProjectFile( '.prettierrc.toml' ) || - hasProjectFile( '.prettierrc.yaml' ) || - hasProjectFile( '.prettierrc.yml' ) || - hasProjectFile( 'prettier.config.js' ) || - hasProjectFile( '.prettierrc' ) || - hasPackageProp( 'prettier' ); - // TODO: once setup, use @wordpress/prettier-config -if ( ! hasProjectPrettierConfig ) { +if ( ! hasPrettierConfig() ) { configArgs = [ '--config', fromConfigRoot( '.prettierrc.js' ) ]; } diff --git a/packages/scripts/utils/config.js b/packages/scripts/utils/config.js index 2524f7d17168aa..a4dc41c1803800 100644 --- a/packages/scripts/utils/config.js +++ b/packages/scripts/utils/config.js @@ -31,6 +31,16 @@ const hasJestConfig = () => hasProjectFile( 'jest.config.json' ) || hasPackageProp( 'jest' ); +const hasPrettierConfig = () => + hasProjectFile( '.prettierrc.js' ) || + hasProjectFile( '.prettierrc.json' ) || + hasProjectFile( '.prettierrc.toml' ) || + hasProjectFile( '.prettierrc.yaml' ) || + hasProjectFile( '.prettierrc.yml' ) || + hasProjectFile( 'prettier.config.js' ) || + hasProjectFile( '.prettierrc' ) || + hasPackageProp( 'prettier' ); + const hasWebpackConfig = () => hasArgInCLI( '--config' ) || hasProjectFile( 'webpack.config.js' ) || @@ -98,4 +108,5 @@ module.exports = { getWebpackArgs, hasBabelConfig, hasJestConfig, + hasPrettierConfig, }; diff --git a/packages/scripts/utils/index.js b/packages/scripts/utils/index.js index 0ddf62d395b90e..0629a0e21bcab7 100644 --- a/packages/scripts/utils/index.js +++ b/packages/scripts/utils/index.js @@ -9,7 +9,12 @@ const { hasFileArgInCLI, spawnScript, } = require( './cli' ); -const { getWebpackArgs, hasBabelConfig, hasJestConfig } = require( './config' ); +const { + getWebpackArgs, + hasBabelConfig, + hasJestConfig, + hasPrettierConfig, +} = require( './config' ); const { buildWordPress, downloadWordPressZip, @@ -33,6 +38,7 @@ module.exports = { hasFileArgInCLI, hasJestConfig, hasPackageProp, + hasPrettierConfig, hasProjectFile, downloadWordPressZip, mergeYAMLConfigs, From bf3042fa7ad3875995af8f29beaecae118c00d3e Mon Sep 17 00:00:00 2001 From: Miguel Fonseca Date: Fri, 7 Feb 2020 07:25:17 +0000 Subject: [PATCH 07/15] Social Link: Fix server-side rendering for legacy format (#20074) Fixes a regression introduced in #19887. While changing the logic in `register_block_core_social_link` to consume the block type's `block.json` and switch to the `$service` attribute name, I broke the handling of the legacy format for social links. Old format: New format: This fixes the handling of legacy social links by providing the adequate attribute default for each `$service`. --- packages/block-library/src/social-link/index.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/block-library/src/social-link/index.php b/packages/block-library/src/social-link/index.php index 605e49fce2f11b..7fae205fb47323 100644 --- a/packages/block-library/src/social-link/index.php +++ b/packages/block-library/src/social-link/index.php @@ -81,6 +81,18 @@ function register_block_core_social_link() { array_merge( $metadata, array( + 'attributes' => array( + 'url' => array( + 'type' => 'string', + ), + 'service' => array( + 'type' => 'string', + 'default' => $site, + ), + 'label' => array( + 'type' => 'string', + ), + ), 'render_callback' => 'render_core_social_link', ) ) From 31e7219ee9d27710c9f7ba2cdd6d628b1e4ae28d Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 7 Feb 2020 09:27:30 +0100 Subject: [PATCH 08/15] Move more format library icons to the icons package (#20072) --- package-lock.json | 1 + .../components/media-replace-flow/index.js | 3 +- .../src/components/url-input/button.js | 3 +- .../url-popover/image-url-input-ui.js | 3 +- packages/block-library/src/button/edit.js | 3 +- .../block-library/src/button/edit.native.js | 5 +- .../block-library/src/image/edit.native.js | 4 +- .../block-library/src/navigation-link/edit.js | 4 +- .../components/src/button/stories/index.js | 9 +- .../src/toolbar-group/stories/index.js | 17 ++-- .../components/src/toolbar/stories/index.js | 27 +++-- .../src/components/post-permalink/index.js | 3 +- packages/format-library/package.json | 1 + packages/format-library/src/bold/index.js | 3 +- packages/format-library/src/code/index.js | 3 +- packages/format-library/src/italic/index.js | 3 +- packages/format-library/src/link/index.js | 5 +- .../format-library/src/link/index.native.js | 3 +- .../format-library/src/link/modal.native.js | 3 +- .../format-library/src/strikethrough/index.js | 3 +- packages/icons/src/index.js | 5 + packages/icons/src/library/format-bold.js | 12 +++ packages/icons/src/library/format-italic.js | 12 +++ .../icons/src/library/format-strikethrough.js | 12 +++ packages/icons/src/library/link-off.js | 12 +++ packages/icons/src/library/link.js | 12 +++ storybook/test/__snapshots__/index.js.snap | 98 ++++++++----------- 27 files changed, 179 insertions(+), 90 deletions(-) create mode 100644 packages/icons/src/library/format-bold.js create mode 100644 packages/icons/src/library/format-italic.js create mode 100644 packages/icons/src/library/format-strikethrough.js create mode 100644 packages/icons/src/library/link-off.js create mode 100644 packages/icons/src/library/link.js diff --git a/package-lock.json b/package-lock.json index d550960e905a44..b3a7902a9beaaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10536,6 +10536,7 @@ "@wordpress/element": "file:packages/element", "@wordpress/html-entities": "file:packages/html-entities", "@wordpress/i18n": "file:packages/i18n", + "@wordpress/icons": "file:packages/icons", "@wordpress/keycodes": "file:packages/keycodes", "@wordpress/rich-text": "file:packages/rich-text", "@wordpress/url": "file:packages/url", diff --git a/packages/block-editor/src/components/media-replace-flow/index.js b/packages/block-editor/src/components/media-replace-flow/index.js index 73723a4484f909..ccfc9a09e2c797 100644 --- a/packages/block-editor/src/components/media-replace-flow/index.js +++ b/packages/block-editor/src/components/media-replace-flow/index.js @@ -16,6 +16,7 @@ import { import { LEFT, RIGHT, UP, DOWN, BACKSPACE, ENTER } from '@wordpress/keycodes'; import { useSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; +import { link } from '@wordpress/icons'; /** * Internal dependencies @@ -170,7 +171,7 @@ const MediaReplaceFlow = ( { { onSelectURL && ( setShowURLInput( ! showURLInput ) } diff --git a/packages/block-editor/src/components/url-input/button.js b/packages/block-editor/src/components/url-input/button.js index e262dce201b3ed..d6a67b0e837ff0 100644 --- a/packages/block-editor/src/components/url-input/button.js +++ b/packages/block-editor/src/components/url-input/button.js @@ -4,6 +4,7 @@ import { __ } from '@wordpress/i18n'; import { Component } from '@wordpress/element'; import { Button } from '@wordpress/components'; +import { link } from '@wordpress/icons'; /** * Internal dependencies @@ -37,7 +38,7 @@ class URLInputButton extends Component { return (
-
@@ -156,11 +161,11 @@ export const buttons = () => { - diff --git a/packages/components/src/color-picker/inputs.js b/packages/components/src/color-picker/inputs.js index 7e8302833aea9e..3f46d0b3e58f8d 100644 --- a/packages/components/src/color-picker/inputs.js +++ b/packages/components/src/color-picker/inputs.js @@ -11,6 +11,7 @@ import { __ } from '@wordpress/i18n'; import { Component } from '@wordpress/element'; import { DOWN, ENTER, UP } from '@wordpress/keycodes'; import { pure } from '@wordpress/compose'; +import { chevronDown } from '@wordpress/icons'; /** * Internal dependencies @@ -289,7 +290,7 @@ export class Inputs extends Component {
diff --git a/packages/components/src/color-picker/test/__snapshots__/index.js.snap b/packages/components/src/color-picker/test/__snapshots__/index.js.snap index 602cb30406427f..3f58e17213bf99 100644 --- a/packages/components/src/color-picker/test/__snapshots__/index.js.snap +++ b/packages/components/src/color-picker/test/__snapshots__/index.js.snap @@ -156,16 +156,15 @@ exports[`ColorPicker should commit changes to all views on blur 1`] = ` > @@ -331,16 +330,15 @@ exports[`ColorPicker should commit changes to all views on keyDown = DOWN 1`] = > @@ -506,16 +504,15 @@ exports[`ColorPicker should commit changes to all views on keyDown = ENTER 1`] = > @@ -681,16 +678,15 @@ exports[`ColorPicker should commit changes to all views on keyDown = UP 1`] = ` > @@ -856,16 +852,15 @@ exports[`ColorPicker should only update input view for draft changes 1`] = ` > @@ -1031,16 +1026,15 @@ exports[`ColorPicker should render color picker 1`] = ` > diff --git a/packages/components/src/custom-select-control/index.js b/packages/components/src/custom-select-control/index.js index 19d3abbed8b122..94f63d6c8d41f5 100644 --- a/packages/components/src/custom-select-control/index.js +++ b/packages/components/src/custom-select-control/index.js @@ -7,11 +7,11 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { Icon, check } from '@wordpress/icons'; +import { Icon, check, chevronDown } from '@wordpress/icons'; /** * Internal dependencies */ -import { Button, Dashicon } from '../'; +import { Button } from '../'; const itemToString = ( item ) => item && item.name; // This is needed so that in Windows, where @@ -121,8 +121,8 @@ export default function CustomSelectControl( { } ) } > { itemToString( selectedItem ) } - diff --git a/packages/components/src/toolbar/stories/index.js b/packages/components/src/toolbar/stories/index.js index fda6e28152555d..9a822836e2ade8 100644 --- a/packages/components/src/toolbar/stories/index.js +++ b/packages/components/src/toolbar/stories/index.js @@ -2,11 +2,16 @@ * WordPress dependencies */ import { + alignCenter, + alignLeft, + alignRight, + code, formatBold, formatItalic, formatStrikethrough, link, - code, + more, + paragraph, } from '@wordpress/icons'; /** @@ -41,27 +46,27 @@ export const _default = () => { id="options-toolbar" > - + { ( toggleProps ) => ( { diff --git a/packages/editor/src/components/reusable-blocks-buttons/reusable-block-delete-button.js b/packages/editor/src/components/reusable-blocks-buttons/reusable-block-delete-button.js index df3bdfa2b5f6b9..b1e5fb3cd6ef73 100644 --- a/packages/editor/src/components/reusable-blocks-buttons/reusable-block-delete-button.js +++ b/packages/editor/src/components/reusable-blocks-buttons/reusable-block-delete-button.js @@ -11,6 +11,7 @@ import { MenuItem } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { isReusableBlock } from '@wordpress/blocks'; import { withSelect, withDispatch } from '@wordpress/data'; +import { close } from '@wordpress/icons'; export function ReusableBlockDeleteButton( { isVisible, @@ -23,7 +24,7 @@ export function ReusableBlockDeleteButton( { return ( onDelete() } > diff --git a/packages/editor/src/components/reusable-blocks-buttons/test/__snapshots__/reusable-block-delete-button.js.snap b/packages/editor/src/components/reusable-blocks-buttons/test/__snapshots__/reusable-block-delete-button.js.snap index c15c65b6c63a5d..352d038e49121d 100644 --- a/packages/editor/src/components/reusable-blocks-buttons/test/__snapshots__/reusable-block-delete-button.js.snap +++ b/packages/editor/src/components/reusable-blocks-buttons/test/__snapshots__/reusable-block-delete-button.js.snap @@ -3,7 +3,16 @@ exports[`ReusableBlockDeleteButton matches the snapshot 1`] = ` + + + } onClick={[Function]} > Remove from Reusable blocks diff --git a/storybook/test/__snapshots__/index.js.snap b/storybook/test/__snapshots__/index.js.snap index 53918805e7b9ed..c1e09abbe535f5 100644 --- a/storybook/test/__snapshots__/index.js.snap +++ b/storybook/test/__snapshots__/index.js.snap @@ -316,16 +316,15 @@ exports[`Storyshots Components/Button Buttons 1`] = ` > @@ -335,16 +334,15 @@ exports[`Storyshots Components/Button Buttons 1`] = ` > @@ -354,16 +352,15 @@ exports[`Storyshots Components/Button Buttons 1`] = ` > @@ -373,16 +370,15 @@ exports[`Storyshots Components/Button Buttons 1`] = ` > @@ -392,16 +388,15 @@ exports[`Storyshots Components/Button Buttons 1`] = ` > Icon & Text @@ -443,16 +438,15 @@ exports[`Storyshots Components/Button Buttons 1`] = ` > @@ -462,16 +456,15 @@ exports[`Storyshots Components/Button Buttons 1`] = ` > @@ -481,16 +474,15 @@ exports[`Storyshots Components/Button Buttons 1`] = ` > @@ -500,16 +492,15 @@ exports[`Storyshots Components/Button Buttons 1`] = ` > @@ -519,16 +510,15 @@ exports[`Storyshots Components/Button Buttons 1`] = ` > Icon & Text @@ -669,7 +659,7 @@ exports[`Storyshots Components/Button Grouped Icons 1`] = ` @@ -2336,16 +2325,15 @@ exports[`Storyshots Components/ColorPicker Alpha Enabled 1`] = ` > @@ -2511,16 +2499,15 @@ exports[`Storyshots Components/ColorPicker Default 1`] = ` > @@ -2553,16 +2540,16 @@ exports[`Storyshots Components/CustomSelectControl Default 1`] = ` > @@ -3092,16 +3079,16 @@ exports[`Storyshots Components/FontSizePicker Default 1`] = ` Normal @@ -3180,16 +3167,16 @@ exports[`Storyshots Components/FontSizePicker With Slider 1`] = ` Normal @@ -3312,16 +3299,16 @@ exports[`Storyshots Components/FontSizePicker Without Custom Sizes 1`] = ` Normal @@ -5127,16 +5114,15 @@ exports[`Storyshots Components/Toolbar Default 1`] = ` > @@ -5165,12 +5151,11 @@ exports[`Storyshots Components/Toolbar Default 1`] = ` >
- + { // translators: %s: Humanized date of last update e.g: "2 months ago". sprintf( __( 'Updated %s' ), humanizedUpdated ) }
diff --git a/packages/block-editor/src/components/alignment-toolbar/test/__snapshots__/index.js.snap b/packages/block-editor/src/components/alignment-toolbar/test/__snapshots__/index.js.snap index 8ca05b1361631e..bfccac02fa4864 100644 --- a/packages/block-editor/src/components/alignment-toolbar/test/__snapshots__/index.js.snap +++ b/packages/block-editor/src/components/alignment-toolbar/test/__snapshots__/index.js.snap @@ -6,7 +6,14 @@ exports[`AlignmentToolbar should allow custom alignment controls to be specified Array [ Object { "align": "custom-left", - "icon": "editor-alignleft", + "icon": + + , "isActive": false, "onClick": [Function], "role": "menuitemradio", @@ -14,7 +21,14 @@ exports[`AlignmentToolbar should allow custom alignment controls to be specified }, Object { "align": "custom-right", - "icon": "editor-aligncenter", + "icon": + + , "isActive": true, "onClick": [Function], "role": "menuitemradio", @@ -22,7 +36,16 @@ exports[`AlignmentToolbar should allow custom alignment controls to be specified }, ] } - icon="editor-aligncenter" + icon={ + + + + } isCollapsed={true} label="Change text alignment" /> diff --git a/packages/block-editor/src/components/alignment-toolbar/test/index.js b/packages/block-editor/src/components/alignment-toolbar/test/index.js index 3ad4397b84da0f..05ac7e67dcda7a 100644 --- a/packages/block-editor/src/components/alignment-toolbar/test/index.js +++ b/packages/block-editor/src/components/alignment-toolbar/test/index.js @@ -3,6 +3,11 @@ */ import { shallow } from 'enzyme'; +/** + * WordPress dependencies + */ +import { alignLeft, alignCenter } from '@wordpress/icons'; + /** * Internal dependencies */ @@ -53,12 +58,12 @@ describe( 'AlignmentToolbar', () => { onChange={ onChangeSpy } alignmentControls={ [ { - icon: 'editor-alignleft', + icon: alignLeft, title: 'My custom left', align: 'custom-left', }, { - icon: 'editor-aligncenter', + icon: alignCenter, title: 'My custom right', align: 'custom-right', }, diff --git a/packages/block-editor/src/components/block-controls/test/__snapshots__/index.js.snap b/packages/block-editor/src/components/block-controls/test/__snapshots__/index.js.snap index ff2040497b12a7..af976de16f268a 100644 --- a/packages/block-editor/src/components/block-controls/test/__snapshots__/index.js.snap +++ b/packages/block-editor/src/components/block-controls/test/__snapshots__/index.js.snap @@ -20,17 +20,38 @@ exports[`BlockControls should render a dynamic toolbar of controls 1`] = ` Array [ Object { "align": "left", - "icon": "editor-alignleft", + "icon": + + , "title": "Align left", }, Object { "align": "center", - "icon": "editor-aligncenter", + "icon": + + , "title": "Align center", }, Object { "align": "right", - "icon": "editor-alignright", + "icon": + + , "title": "Align right", }, ] diff --git a/packages/block-editor/src/components/block-controls/test/index.js b/packages/block-editor/src/components/block-controls/test/index.js index f970dc030cadbe..3ab1fec37b3f1e 100644 --- a/packages/block-editor/src/components/block-controls/test/index.js +++ b/packages/block-editor/src/components/block-controls/test/index.js @@ -3,6 +3,11 @@ */ import { shallow } from 'enzyme'; +/** + * WordPress dependencies + */ +import { alignCenter, alignLeft, alignRight } from '@wordpress/icons'; + /** * Internal dependencies */ @@ -12,17 +17,17 @@ import BlockEdit from '../../block-edit'; describe( 'BlockControls', () => { const controls = [ { - icon: 'editor-alignleft', + icon: alignLeft, title: 'Align left', align: 'left', }, { - icon: 'editor-aligncenter', + icon: alignCenter, title: 'Align center', align: 'center', }, { - icon: 'editor-alignright', + icon: alignRight, title: 'Align right', align: 'right', }, diff --git a/packages/block-editor/src/components/block-mover/index.native.js b/packages/block-editor/src/components/block-mover/index.native.js index 27bf29650e9a92..b572e21ee79dd8 100644 --- a/packages/block-editor/src/components/block-mover/index.native.js +++ b/packages/block-editor/src/components/block-mover/index.native.js @@ -10,6 +10,7 @@ import { ToolbarButton } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { withSelect, withDispatch } from '@wordpress/data'; import { withInstanceId, compose } from '@wordpress/compose'; +import { arrowUp, arrowDown } from '@wordpress/icons'; const BlockMover = ( { isFirst, @@ -39,7 +40,7 @@ const BlockMover = ( { } isDisabled={ isFirst } onClick={ onMoveUp } - icon="arrow-up-alt" + icon={ arrowUp } extraProps={ { hint: __( 'Double tap to move the block up' ) } } /> @@ -58,7 +59,7 @@ const BlockMover = ( { } isDisabled={ isLast } onClick={ onMoveDown } - icon="arrow-down-alt" + icon={ arrowDown } extraProps={ { hint: __( 'Double tap to move the block down' ), } } diff --git a/packages/block-editor/src/components/block-settings/button.native.js b/packages/block-editor/src/components/block-settings/button.native.js index 5225e8bb988c44..9fa1a037494294 100644 --- a/packages/block-editor/src/components/block-settings/button.native.js +++ b/packages/block-editor/src/components/block-settings/button.native.js @@ -4,13 +4,14 @@ import { createSlotFill, ToolbarButton } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { withDispatch } from '@wordpress/data'; +import { cog } from '@wordpress/icons'; const { Fill, Slot } = createSlotFill( 'SettingsToolbarButton' ); const SettingsButton = ( { openGeneralSidebar } ) => ( ); diff --git a/packages/block-editor/src/components/media-edit/index.native.js b/packages/block-editor/src/components/media-edit/index.native.js index fd10c10e2ef5b1..e4d86d0e1d0c9f 100644 --- a/packages/block-editor/src/components/media-edit/index.native.js +++ b/packages/block-editor/src/components/media-edit/index.native.js @@ -12,6 +12,7 @@ import { */ import { __ } from '@wordpress/i18n'; import { Picker } from '@wordpress/components'; +import { update } from '@wordpress/icons'; export const MEDIA_TYPE_IMAGE = 'image'; @@ -30,7 +31,7 @@ const replaceOption = { value: mediaSources.deviceLibrary, label: __( 'Replace' ), types: [ MEDIA_TYPE_IMAGE ], - icon: 'update', + icon: update, }; const options = [ editOption, replaceOption ]; diff --git a/packages/block-editor/src/components/media-placeholder/index.js b/packages/block-editor/src/components/media-placeholder/index.js index 1874c431a83d02..aa3225fb63602f 100644 --- a/packages/block-editor/src/components/media-placeholder/index.js +++ b/packages/block-editor/src/components/media-placeholder/index.js @@ -19,6 +19,7 @@ import { Component } from '@wordpress/element'; import { compose } from '@wordpress/compose'; import { withSelect } from '@wordpress/data'; import deprecated from '@wordpress/deprecated'; +import { keyboardReturn } from '@wordpress/icons'; /** * Internal dependencies @@ -43,7 +44,7 @@ const InsertFromURLPopover = ( { src, onChange, onSubmit, onClose } ) => ( /> ); @@ -170,7 +159,7 @@ describe( 'Button', () => { it( 'should force showing the tooltip even if icon and children defined', () => { const iconButton = shallow( - ); diff --git a/packages/components/src/draggable/README.md b/packages/components/src/draggable/README.md index f76c983a8d999e..28bdee89faf23e 100644 --- a/packages/components/src/draggable/README.md +++ b/packages/components/src/draggable/README.md @@ -12,56 +12,53 @@ The component accepts the following props: The HTML id of the element to clone on drag -- Type: `string` -- Required: Yes +- Type: `string` +- Required: Yes ### transferData Arbitrary data object attached to the drag and drop event. -- Type: `Object` -- Required: Yes +- Type: `Object` +- Required: Yes ### onDragStart A function to be called when dragging starts. -- Type: `Function` -- Required: No -- Default: `noop` +- Type: `Function` +- Required: No +- Default: `noop` ### onDragEnd A function to be called when dragging ends. -- Type: `Function` -- Required: No -- Default: `noop` +- Type: `Function` +- Required: No +- Default: `noop` ## Usage ```jsx -import { Dashicon, Draggable, Panel, PanelBody } from '@wordpress/components'; +import { Draggable, Panel, PanelBody } from '@wordpress/components'; +import { Icon, more } from '@wordpress/icons'; const MyDraggable = () => (
- + - - { - ( { onDraggableStart, onDraggableEnd } ) => ( -
- -
- ) - } + + { ( { onDraggableStart, onDraggableEnd } ) => ( +
+ +
+ ) }
@@ -72,29 +69,29 @@ const MyDraggable = () => ( In case you want to call your own `dragstart` / `dragend` event handlers as well, you can pass them to `Draggable` and it'll take care of calling them after their own: ```jsx -import { Dashicon, Draggable, Panel, PanelBody } from '@wordpress/components'; +import { Draggable, Panel, PanelBody } from '@wordpress/components'; +import { Icon, more } from '@wordpress/icons'; const MyDraggable = ( { onDragStart, onDragEnd } ) => (
- + - { - ( { onDraggableStart, onDraggableEnd } ) => ( -
- -
- ) - } + { ( { onDraggableStart, onDraggableEnd } ) => ( +
+ +
+ ) }
diff --git a/packages/components/src/draggable/stories/index.js b/packages/components/src/draggable/stories/index.js index 816870dbc63a69..e124529cf62034 100644 --- a/packages/components/src/draggable/stories/index.js +++ b/packages/components/src/draggable/stories/index.js @@ -2,12 +2,12 @@ * WordPress dependencies */ import { useState } from '@wordpress/element'; +import { Icon, more } from '@wordpress/icons'; /** * Internal dependencies */ import Draggable from '../'; -import Dashicon from '../../dashicon'; export default { title: 'Components/Draggable', component: Draggable }; @@ -56,7 +56,7 @@ const DraggalbeExample = () => { onDragEnd={ handleOnDragEnd } draggable > - + ); } } diff --git a/packages/components/src/dropdown-menu/README.md b/packages/components/src/dropdown-menu/README.md index a8c0da0ae5d293..3790412a69df28 100644 --- a/packages/components/src/dropdown-menu/README.md +++ b/packages/components/src/dropdown-menu/README.md @@ -60,11 +60,17 @@ Render a Dropdown Menu with a set of controls: ```jsx import { DropdownMenu } from '@wordpress/components'; -import { arrowLeft, arrowRight, arrowUp, arrowDown } from '@wordpress/icons'; +import { + more, + arrowLeft, + arrowRight, + arrowUp, + arrowDown, +} from '@wordpress/icons'; const MyDropdownMenu = () => ( ( - + { ( { onClose } ) => ( diff --git a/packages/components/src/dropdown/stories/index.js b/packages/components/src/dropdown/stories/index.js index c005e719951d52..e95387634d8a63 100644 --- a/packages/components/src/dropdown/stories/index.js +++ b/packages/components/src/dropdown/stories/index.js @@ -1,7 +1,13 @@ /** * WordPress dependencies */ -import { arrowLeft, arrowRight, arrowUp, arrowDown } from '@wordpress/icons'; +import { + more, + arrowLeft, + arrowRight, + arrowUp, + arrowDown, +} from '@wordpress/icons'; /** * Internal dependencies @@ -20,7 +26,7 @@ const DropdownAndDropdownMenuExample = () => {

This is a DropdownMenu component:

{ position="bottom right" renderToggle={ ( { isOpen, onToggle } ) => (
@@ -2657,16 +2656,15 @@ Array [ > @@ -2693,16 +2691,15 @@ Array [ > From a8f6bdabddc35c00294400be4ee4e0593c3b6e2a Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Fri, 7 Feb 2020 13:36:35 +0000 Subject: [PATCH 15/15] Remove: Alignment options from button nested inside buttons (#19824) --- packages/block-editor/src/hooks/align.js | 19 +++++++++++++---- packages/block-editor/src/hooks/index.js | 4 +++- packages/block-editor/src/index.js | 3 ++- packages/block-library/src/buttons/edit.js | 24 +++++++++++++++------- 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/packages/block-editor/src/hooks/align.js b/packages/block-editor/src/hooks/align.js index cfded941725ecc..8ff4b5232c9f65 100644 --- a/packages/block-editor/src/hooks/align.js +++ b/packages/block-editor/src/hooks/align.js @@ -7,6 +7,7 @@ import { assign, get, has, includes, without } from 'lodash'; /** * WordPress dependencies */ +import { createContext, useContext } from '@wordpress/element'; import { createHigherOrderComponent } from '@wordpress/compose'; import { addFilter } from '@wordpress/hooks'; import { @@ -99,6 +100,13 @@ export function addAttribute( settings ) { return settings; } +const AlignmentHookSettings = createContext( {} ); + +/** + * Allows to pass additional settings to the alignment hook. + */ +export const AlignmentHookSettingsProvider = AlignmentHookSettings.Provider; + /** * Override the default edit UI to include new toolbar controls for block * alignment, if block defines support. @@ -108,14 +116,17 @@ export function addAttribute( settings ) { */ export const withToolbarControls = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { + const { isEmbedButton } = useContext( AlignmentHookSettings ); const { name: blockName } = props; // Compute valid alignments without taking into account, // if the theme supports wide alignments or not. // BlockAlignmentToolbar takes into account the theme support. - const validAlignments = getValidAlignments( - getBlockSupport( blockName, 'align' ), - hasBlockSupport( blockName, 'alignWide', true ) - ); + const validAlignments = isEmbedButton + ? [] + : getValidAlignments( + getBlockSupport( blockName, 'align' ), + hasBlockSupport( blockName, 'alignWide', true ) + ); const updateAlignment = ( nextAlign ) => { if ( ! nextAlign ) { diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index 7bd9f390390c4d..ab3ae2756ce70c 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -1,7 +1,9 @@ /** * Internal dependencies */ -import './align'; +import { AlignmentHookSettingsProvider } from './align'; import './anchor'; import './custom-class-name'; import './generated-class-name'; + +export { AlignmentHookSettingsProvider }; diff --git a/packages/block-editor/src/index.js b/packages/block-editor/src/index.js index 85bf3895156723..9e9e2a3c7a15b7 100644 --- a/packages/block-editor/src/index.js +++ b/packages/block-editor/src/index.js @@ -9,7 +9,8 @@ import '@wordpress/keyboard-shortcuts'; /** * Internal dependencies */ -import './hooks'; +import { AlignmentHookSettingsProvider as __experimentalAlignmentHookSettingsProvider } from './hooks'; +export { __experimentalAlignmentHookSettingsProvider }; export * from './components'; export * from './utils'; export { storeConfig } from './store'; diff --git a/packages/block-library/src/buttons/edit.js b/packages/block-library/src/buttons/edit.js index 390bd687b09e88..15590ca49e713b 100644 --- a/packages/block-library/src/buttons/edit.js +++ b/packages/block-library/src/buttons/edit.js @@ -1,7 +1,10 @@ /** * WordPress dependencies */ -import { InnerBlocks } from '@wordpress/block-editor'; +import { + __experimentalAlignmentHookSettingsProvider as AlignmentHookSettingsProvider, + InnerBlocks, +} from '@wordpress/block-editor'; /** * Internal dependencies @@ -14,15 +17,22 @@ const UI_PARTS = { hasSelectedUI: false, }; +// Inside buttons block alignment options are not supported. +const alignmentHooksSetting = { + isEmbedButton: true, +}; + function ButtonsEdit( { className } ) { return (
- + + +
); }