diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index ce120ddf0aad57..d426bc4a2a6012 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -18,12 +18,12 @@
# Editor
/packages/annotations @atimmer @ellatrix
/packages/autop @aduth
-/packages/block-editor @youknowriad @talldan @ellatrix
+/packages/block-editor @youknowriad @ellatrix
/packages/block-serialization-spec-parser @dmsnell
/packages/block-serialization-default-parser @dmsnell
/packages/blocks @youknowriad @ellatrix
-/packages/edit-post @talldan
-/packages/editor @talldan
+/packages/edit-post
+/packages/editor
/packages/list-reusable-blocks @youknowriad @noisysocks
/packages/shortcode @aduth
@@ -53,12 +53,12 @@
/packages/scripts @youknowriad @gziolo @ntwb @nerrad @ajitbohra
# UI Components
-/packages/components @youknowriad @ajitbohra @jaymanpandya @jorgefilipecosta @talldan @chrisvanpatten
-/packages/compose @youknowriad @ajitbohra @jaymanpandya @jorgefilipecosta @talldan
-/packages/element @youknowriad @ajitbohra @jaymanpandya @jorgefilipecosta @talldan
-/packages/notices @ajitbohra @jaymanpandya @jorgefilipecosta @talldan
-/packages/nux @ajitbohra @jaymanpandya @jorgefilipecosta @talldan @noisysocks
-/packages/viewport @youknowriad @ajitbohra @jaymanpandya @jorgefilipecosta @talldan
+/packages/components @youknowriad @ajitbohra @jaymanpandya @jorgefilipecosta @chrisvanpatten
+/packages/compose @youknowriad @ajitbohra @jaymanpandya @jorgefilipecosta
+/packages/element @youknowriad @ajitbohra @jaymanpandya @jorgefilipecosta
+/packages/notices @ajitbohra @jaymanpandya @jorgefilipecosta
+/packages/nux @ajitbohra @jaymanpandya @jorgefilipecosta @noisysocks
+/packages/viewport @youknowriad @ajitbohra @jaymanpandya @jorgefilipecosta
# Utilities
/packages/a11y @youknowriad @aduth
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/docs/contributors/coding-guidelines.md b/docs/contributors/coding-guidelines.md
index 24256a1914ebf0..a8f0698fa4a5c2 100644
--- a/docs/contributors/coding-guidelines.md
+++ b/docs/contributors/coding-guidelines.md
@@ -10,8 +10,8 @@ To avoid class name collisions, class names **must** adhere to the following gui
All class names assigned to an element must be prefixed with the name of the package, followed by a dash and the name of the directory in which the component resides. Any descendent of the component's root element must append a dash-delimited descriptor, separated from the base by two consecutive underscores `__`.
-* Root element: `package-directory`
-* Child elements: `package-directory__descriptor-foo-bar`
+- Root element: `package-directory`
+- Child elements: `package-directory__descriptor-foo-bar`
The root element is considered to be the highest ancestor element returned by the default export in the `index.js`. Notably, if your folder contains multiple files, each with their own default exported component, only the element rendered by that of `index.js` can be considered the root. All others should be treated as descendents.
@@ -23,12 +23,10 @@ Consider the following component located at `packages/components/src/notice/inde
export default function Notice( { children, onRemove } ) {
return (
;
}
```
@@ -135,8 +129,8 @@ export { __experimentalDoExcitingExperimentalAction } from './api';
export { __unstableDoTerribleAwfulAction } from './api';
```
-- An **experimental API** is one which is planned for eventual public availability, but is subject to further experimentation, testing, and discussion.
-- An **unstable API** is one which serves as a means to an end. It is not desired to ever be converted into a public API.
+- An **experimental API** is one which is planned for eventual public availability, but is subject to further experimentation, testing, and discussion.
+- An **unstable API** is one which serves as a means to an end. It is not desired to ever be converted into a public API.
In both cases, the API should be made stable or removed at the earliest opportunity.
@@ -168,7 +162,7 @@ const object = {
### Strings
-String literals should be declared with single-quotes *unless* the string itself contains a single-quote that would need to be escaped–in that case: use a double-quote. If the string contains a single-quote *and* a double-quote, you can use ES6 template strings to avoid escaping the quotes.
+String literals should be declared with single-quotes _unless_ the string itself contains a single-quote that would need to be escaped–in that case: use a double-quote. If the string contains a single-quote _and_ a double-quote, you can use ES6 template strings to avoid escaping the quotes.
**Note:** The single-quote character (`'`) should never be used in place of an apostrophe (`’`) for words like `it’s` or `haven’t` in user-facing strings. For test code it's still encouraged to use a real apostrophe.
@@ -176,12 +170,12 @@ In general, avoid backslash-escaping quotes:
```js
// Bad:
-const name = "Matt";
+const name = 'Matt';
// Good:
const name = 'Matt';
// Bad:
-const pet = 'Matt\'s dog';
+const pet = "Matt's dog";
// Also bad (not using an apostrophe):
const pet = "Matt's dog";
// Good:
@@ -207,8 +201,8 @@ Gutenberg follows the [WordPress JavaScript Documentation Standards](https://mak
For additional guidance, consult the following resources:
-- [JSDoc Official Documentation](https://jsdoc.app/index.html)
-- [TypeScript Supported JSDoc](https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html#supported-jsdoc)
+- [JSDoc Official Documentation](https://jsdoc.app/index.html)
+- [TypeScript Supported JSDoc](https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html#supported-jsdoc)
### Custom Types
@@ -236,7 +230,7 @@ Custom types can also be used to describe a set of predefined options. While the
```js
/**
* Named breakpoint sizes.
- *
+ *
* @typedef {'huge'|'wide'|'large'|'medium'|'small'|'mobile'} WPBreakpoint
*/
```
@@ -311,13 +305,13 @@ Similar to the "Custom Types" advice concerning type unions and with literal val
```js
/**
* Named breakpoint sizes.
- *
+ *
* @typedef {"huge"|"wide"|"large"|"medium"|"small"|"mobile"} WPBreakpoint
*/
/**
* Hash of breakpoint names with pixel width at which it becomes effective.
- *
+ *
* @type {Object}
*/
const BREAKPOINTS = { huge: 1440 /* , ... */ };
@@ -331,9 +325,9 @@ You can express a nullable type using a leading `?`. Use the nullable form of a
/**
* Returns a configuration value for a given key, if exists. Returns null if
* there is no configured value.
- *
+ *
* @param {string} key Configuration key to retrieve.
- *
+ *
* @return {?*} Configuration value, if exists.
*/
function getConfigurationValue( key ) {
@@ -372,9 +366,9 @@ If a function has multiple code paths where some (but not all) conditions result
```js
/**
* Returns a configuration value for a given key, if exists.
- *
+ *
* @param {string} key Configuration key to retrieve.
- *
+ *
* @return {*|void} Configuration value, if exists.
*/
function getConfigurationValue( key ) {
@@ -401,7 +395,7 @@ Because the documentation generated using the `@wordpress/docgen` tool will incl
When documenting an example, use the markdown \`\`\` code block to demarcate the beginning and end of the code sample. An example can span multiple lines.
-```js
+````js
/**
* Given the name of a registered store, returns an object of the store's
* selectors. The selector functions are been pre-bound to pass the current
@@ -418,7 +412,7 @@ When documenting an example, use the markdown \`\`\` code block to
* @return {Object} Object containing the store's
* selectors.
*/
-```
+````
### Documenting `@wordpress/element` (React) Components
@@ -426,7 +420,7 @@ When possible, all components should be implemented as [function components](htt
Documenting a function component should be treated the same as any other function. The primary caveat in documenting a component is being aware that the function typically accepts only a single argument (the "props"), which may include many property members. Use the [dot syntax for parameter properties](https://jsdoc.app/tags-param.html#parameters-with-properties) to document individual prop types.
-```js
+````js
/**
* Renders the block's configured title as a string, or empty if the title
* cannot be determined.
@@ -442,15 +436,15 @@ Documenting a function component should be treated the same as any other functio
*
* @return {?string} Block title.
*/
-```
+````
For class components, there is no recommendation for documenting the props of the component. Gutenberg does not use or endorse the [`propTypes` static class member](https://reactjs.org/docs/typechecking-with-proptypes.html).
## PHP
We use
-[`phpcs` (PHP\_CodeSniffer)](https://github.com/squizlabs/PHP_CodeSniffer) with the [WordPress Coding Standards ruleset](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards) to run a lot of automated checks against all PHP code in this project. This ensures that we are consistent with WordPress PHP coding standards.
+[`phpcs` (PHP_CodeSniffer)](https://github.com/squizlabs/PHP_CodeSniffer) with the [WordPress Coding Standards ruleset](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards) to run a lot of automated checks against all PHP code in this project. This ensures that we are consistent with WordPress PHP coding standards.
The easiest way to use PHPCS is [local environment](/docs/contributors/getting-started.md#local-environment). Once that's installed, you can check your PHP by running `npm run lint-php`.
-If you prefer to install PHPCS locally, you should use `composer`. [Install `composer`](https://getcomposer.org/download/) on your computer, then run `composer install`. This will install `phpcs` and `WordPress-Coding-Standards` which you can then run via `composer lint`.
+If you prefer to install PHPCS locally, you should use `composer`. [Install `composer`](https://getcomposer.org/download/) on your computer, then run `composer install`. This will install `phpcs` and `WordPress-Coding-Standards` which you can then run via `composer lint`.
diff --git a/docs/designers-developers/developers/block-api/block-registration.md b/docs/designers-developers/developers/block-api/block-registration.md
index f69cc31ee0a984..e31115ea459b87 100644
--- a/docs/designers-developers/developers/block-api/block-registration.md
+++ b/docs/designers-developers/developers/block-api/block-registration.md
@@ -2,13 +2,13 @@
## `registerBlockType`
-* **Type:** `Function`
+- **Type:** `Function`
Every block starts by registering a new block type definition. To register, you use the `registerBlockType` function from the [`wp-blocks` package](/packages/blocks/README.md#registerBlockType). The function takes two arguments, a block `name` and a block configuration object.
### Block Name
-* **Type:** `String`
+- **Type:** `String`
The name for a block is a unique string that identifies a block. Names have to be structured as `namespace/block-name`, where namespace is the name of your plugin or theme.
@@ -17,50 +17,50 @@ The name for a block is a unique string that identifies a block. Names have to b
registerBlockType( 'my-plugin/book', {} );
```
-*Note:* A block name can only contain lowercase alphanumeric characters and dashes, and must begin with a letter.
+_Note:_ A block name can only contain lowercase alphanumeric characters and dashes, and must begin with a letter.
-*Note:* This name is used on the comment delimiters as ``. Those blocks provided by core don't include a namespace when serialized.
+_Note:_ This name is used on the comment delimiters as ``. Those blocks provided by core don't include a namespace when serialized.
### Block Configuration
-* **Type:** `Object` [ `{ key: value }` ]
+- **Type:** `Object` [ `{ key: value }` ]
A block requires a few properties to be specified before it can be registered successfully. These are defined through a configuration object, which includes the following:
#### title
-* **Type:** `String`
+- **Type:** `String`
This is the display title for your block, which can be translated with our translation functions. The block inserter will show this name.
```js
// Our data object
-title: __( 'Book' )
+title: __( 'Book' );
```
#### description (optional)
-* **Type:** `String`
+- **Type:** `String`
This is a short description for your block, which can be translated with our translation functions. This will be shown in the Block Tab in the Settings Sidebar.
```js
-description: __( 'Block showing a Book card.' )
+description: __( 'Block showing a Book card.' );
```
#### category
-* **Type:** `String` [ common | formatting | layout | widgets | embed ]
+- **Type:** `String` [ common | formatting | layout | widgets | embed ]
Blocks are grouped into categories to help users browse and discover them.
The core provided categories are:
-* common
-* formatting
-* layout
-* widgets
-* embed
+- common
+- formatting
+- layout
+- widgets
+- embed
```js
// Assigning to the 'widgets' category
@@ -71,7 +71,7 @@ Plugins and Themes can also register [custom block categories](/docs/designers-d
#### icon (optional)
-* **Type:** `String` | `Object`
+- **Type:** `String` | `Object`
An icon property should be specified to make it easier to identify a block. These can be any of [WordPress' Dashicons](https://developer.wordpress.org/resource/dashicons/), or a custom `svg` element.
@@ -95,14 +95,14 @@ icon: {
background: '#7e70af',
// Specifying a color for the icon (optional: if not set, a readable color will be automatically defined)
foreground: '#fff',
- // Specifying a dashicon for the block
- src: 'book-alt',
+ // Specifying an icon for the block
+ src: ,
} ,
```
#### keywords (optional)
-* **Type:** `Array`
+- **Type:** `Array`
Sometimes a block could have aliases that help users discover it while searching. For example, an `image` block could also want to be discovered by `photo`. You can do so by providing an array of terms (which can be translated).
@@ -114,7 +114,7 @@ keywords: [ __( 'image' ), __( 'photo' ), __( 'pics' ) ],
#### styles (optional)
-* **Type:** `Array`
+- **Type:** `Array`
Block styles can be used to provide alternative styles to block. It works by adding a class name to the block’s wrapper. Using CSS, a theme developer can target the class name for the style variation if it is selected.
@@ -142,7 +142,7 @@ Plugins and Themes can also register [custom block style](/docs/designers-develo
#### attributes (optional)
-* **Type:** `Object`
+- **Type:** `Object`
Attributes provide the structured data needs of a block. They can exist in different forms when they are serialized, but they are declared together under a common interface.
@@ -166,11 +166,11 @@ attributes: {
},
```
-* **See: [Attributes](/docs/designers-developers/developers/block-api/block-attributes.md).**
+- **See: [Attributes](/docs/designers-developers/developers/block-api/block-attributes.md).**
#### example (optional)
-* **Type:** `Object`
+- **Type:** `Object`
Example provides structured example data for the block. This data is used to construct a preview for the block to be shown in the Inspector Help Panel when the user mouses over the block.
@@ -190,7 +190,7 @@ If `example` is not defined, the preview will not be shown. So even if no-attrib
#### transforms (optional)
-* **Type:** `Array`
+- **Type:** `Array`
Transforms provide rules for what a block can be transformed from and what it can be transformed to. A block can be transformed from another block, a shortcode, a regular expression, a file or a raw DOM node.
@@ -198,6 +198,7 @@ For example, a Paragraph block can be transformed into a Heading block. This use
{% codetabs %}
{% ES5 %}
+
```js
transforms: {
from: [
@@ -213,7 +214,9 @@ transforms: {
]
},
```
+
{% ESNext %}
+
```js
transforms: {
from: [
@@ -229,12 +232,14 @@ transforms: {
]
},
```
+
{% end %}
An existing shortcode can be transformed into its block counterpart.
{% codetabs %}
{% ES5 %}
+
```js
transforms: {
from: [
@@ -263,7 +268,9 @@ transforms: {
]
},
```
+
{% ESNext %}
+
```js
transforms: {
from: [
@@ -292,12 +299,14 @@ transforms: {
},
```
+
{% end %}
A block can also be transformed into another block type. For example, a Heading block can be transformed into a Paragraph block.
{% codetabs %}
{% ES5 %}
+
```js
transforms: {
to: [
@@ -313,7 +322,9 @@ transforms: {
],
},
```
+
{% ESNext %}
+
```js
transforms: {
to: [
@@ -329,12 +340,14 @@ transforms: {
],
},
```
+
{% end %}
In addition to accepting an array of known block types, the `blocks` option also accepts a "wildcard" (`"*"`). This allows for transformations which apply to _all_ block types (eg: all blocks can transform into `core/group`):
{% codetabs %}
{% ES5 %}
+
```js
transforms: {
from: [
@@ -348,7 +361,9 @@ transforms: {
],
},
```
+
{% ESNext %}
+
```js
transforms: {
from: [
@@ -362,13 +377,14 @@ transforms: {
],
},
```
-{% end %}
+{% end %}
A block with InnerBlocks can also be transformed from and to another block with InnerBlocks.
{% codetabs %}
{% ES5 %}
+
```js
transforms: {
to: [
@@ -382,7 +398,9 @@ transforms: {
],
},
```
+
{% ESNext %}
+
```js
transforms: {
to: [
@@ -396,12 +414,14 @@ transforms: {
],
},
```
+
{% end %}
An optional `isMatch` function can be specified on a transform object. This provides an opportunity to perform additional checks on whether a transform should be possible. Returning `false` from this function will prevent the transform from being displayed as an option to the user.
{% codetabs %}
{% ES5 %}
+
```js
transforms: {
to: [
@@ -420,7 +440,9 @@ transforms: {
],
},
```
+
{% ESNext %}
+
```js
transforms: {
to: [
@@ -437,25 +459,29 @@ transforms: {
],
},
```
+
{% end %}
In the case of shortcode transforms, `isMatch` receives shortcode attributes per the [Shortcode API](https://codex.wordpress.org/Shortcode_API):
{% codetabs %}
{% ES5 %}
+
```js
isMatch: function( attributes ) {
return attributes.named.id === 'my-id';
},
```
+
{% ESNext %}
+
```js
isMatch( { named: { id } } ) {
return id === 'my-id';
},
```
-{% end %}
+{% end %}
To control the priority with which a transform is applied, define a `priority` numeric property on your transform object, where a lower value will take precedence over higher values. This behaves much like a [WordPress hook](https://codex.wordpress.org/Plugin_API#Hook_to_WordPress). Like hooks, the default priority is `10` when not otherwise set.
@@ -463,12 +489,13 @@ A file can be dropped into the editor and converted into a block with a matching
{% codetabs %}
{% ES5 %}
+
```js
transforms: {
from: [
{
type: 'files',
- isMatch: function ( files ) {
+ isMatch: function( files ) {
return files.length === 1;
},
// We define a lower priority (higher number) than the default of 10. This
@@ -486,10 +513,12 @@ transforms: {
} );
},
},
- ]
+ ];
}
```
+
{% ESNext %}
+
```js
transforms: {
from: [
@@ -511,52 +540,56 @@ transforms: {
} );
},
},
- ]
+ ];
}
```
+
{% end %}
A prefix transform is a transform that will be applied if the user prefixes some text in e.g. the Paragraph block with a given pattern and a trailing space.
{% codetabs %}
{% ES5 %}
+
```js
transforms: {
- from: [
- {
- type: 'prefix',
- prefix: '?',
- transform: function( content ) {
- return createBlock( 'my-plugin/question', {
- content,
- } );
- },
- },
- ]
+ from: [
+ {
+ type: 'prefix',
+ prefix: '?',
+ transform: function( content ) {
+ return createBlock( 'my-plugin/question', {
+ content,
+ } );
+ },
+ },
+ ];
}
```
+
{% ESNext %}
+
```js
transforms: {
- from: [
- {
- type: 'prefix',
- prefix: '?',
- transform( content ) {
- return createBlock( 'my-plugin/question', {
- content,
- } );
- },
- },
- ]
+ from: [
+ {
+ type: 'prefix',
+ prefix: '?',
+ transform( content ) {
+ return createBlock( 'my-plugin/question', {
+ content,
+ } );
+ },
+ },
+ ];
}
```
-{% end %}
+{% end %}
#### parent (optional)
-* **Type:** `Array`
+- **Type:** `Array`
Blocks are able to be inserted into blocks that use [`InnerBlocks`](https://github.com/WordPress/gutenberg/blob/master/packages/block-editor/src/components/inner-blocks/README.md) as nested content. Sometimes it is useful to restrict a block so that it is only available as a nested block. For example, you might want to allow an 'Add to Cart' block to only be available within a 'Product' block.
@@ -569,13 +602,13 @@ parent: [ 'core/columns' ],
#### supports (optional)
-*Some [block supports](#supports-optional) — for example, `anchor` or `className` — apply their attributes by adding additional props on the element returned by `save`. This will work automatically for default HTML tag elements (`div`, etc). However, if the return value of your `save` is a custom component element, you will need to ensure that your custom component handles these props in order for the attributes to be persisted.*
+_Some [block supports](#supports-optional) — for example, `anchor` or `className` — apply their attributes by adding additional props on the element returned by `save`. This will work automatically for default HTML tag elements (`div`, etc). However, if the return value of your `save` is a custom component element, you will need to ensure that your custom component handles these props in order for the attributes to be persisted._
-* **Type:** `Object`
+- **Type:** `Object`
Optional block extended support features. The following options are supported:
-- `align` (default `false`): This property adds block controls which allow to change block's alignment. _Important: It doesn't work with dynamic blocks yet._
+- `align` (default `false`): This property adds block controls which allow to change block's alignment. _Important: It doesn't work with dynamic blocks yet._
```js
// Add the support for block's alignment (left, center, right, wide, full).
@@ -583,9 +616,11 @@ align: true,
// Pick which alignment options to display.
align: [ 'left', 'right', 'full' ],
```
+
When supports align is used the block attributes definition is extended to include an align attribute with a string type.
By default, no alignment is assigned to the block.
The block can apply a default alignment by specifying its own align attribute with a default e.g.:
+
```
attributes: {
...
@@ -597,57 +632,57 @@ attributes: {
}
```
-- `alignWide` (default `true`): This property allows to enable [wide alignment](/docs/designers-developers/developers/themes/theme-support.md#wide-alignment) for your theme. To disable this behavior for a single block, set this flag to `false`.
+- `alignWide` (default `true`): This property allows to enable [wide alignment](/docs/designers-developers/developers/themes/theme-support.md#wide-alignment) for your theme. To disable this behavior for a single block, set this flag to `false`.
```js
// Remove the support for wide alignment.
alignWide: false,
```
-- `anchor` (default `false`): Anchors let you link directly to a specific block on a page. This property adds a field to define an id for the block and a button to copy the direct link.
+- `anchor` (default `false`): Anchors let you link directly to a specific block on a page. This property adds a field to define an id for the block and a button to copy the direct link.
```js
// Add the support for an anchor link.
anchor: true,
```
-- `customClassName` (default `true`): This property adds a field to define a custom className for the block's wrapper.
+- `customClassName` (default `true`): This property adds a field to define a custom className for the block's wrapper.
```js
// Remove the support for the custom className.
customClassName: false,
```
-- `className` (default `true`): By default, the class `.wp-block-your-block-name` is added to the root element of your saved markup. This helps having a consistent mechanism for styling blocks that themes and plugins can rely on. If for whatever reason a class is not desired on the markup, this functionality can be disabled.
+- `className` (default `true`): By default, the class `.wp-block-your-block-name` is added to the root element of your saved markup. This helps having a consistent mechanism for styling blocks that themes and plugins can rely on. If for whatever reason a class is not desired on the markup, this functionality can be disabled.
```js
// Remove the support for the generated className.
className: false,
```
-- `html` (default `true`): By default, a block's markup can be edited individually. To disable this behavior, set `html` to `false`.
+- `html` (default `true`): By default, a block's markup can be edited individually. To disable this behavior, set `html` to `false`.
```js
// Remove support for an HTML mode.
html: false,
```
-- `inserter` (default `true`): By default, all blocks will appear in the inserter. To hide a block so that it can only be inserted programmatically, set `inserter` to `false`.
+- `inserter` (default `true`): By default, all blocks will appear in the inserter. To hide a block so that it can only be inserted programmatically, set `inserter` to `false`.
```js
// Hide this block from the inserter.
inserter: false,
```
-- `multiple` (default `true`): A non-multiple block can be inserted into each post, one time only. For example, the built-in 'More' block cannot be inserted again if it already exists in the post being edited. A non-multiple block's icon is automatically dimmed (unclickable) to prevent multiple instances.
+- `multiple` (default `true`): A non-multiple block can be inserted into each post, one time only. For example, the built-in 'More' block cannot be inserted again if it already exists in the post being edited. A non-multiple block's icon is automatically dimmed (unclickable) to prevent multiple instances.
```js
// Use the block just once per post
multiple: false,
```
-- `reusable` (default `true`): A block may want to disable the ability of being converted into a reusable block.
-By default all blocks can be converted to a reusable block. If supports reusable is set to false, the option to convert the block into a reusable block will not appear.
+- `reusable` (default `true`): A block may want to disable the ability of being converted into a reusable block.
+ By default all blocks can be converted to a reusable block. If supports reusable is set to false, the option to convert the block into a reusable block will not appear.
```js
// Don't allow the block to be converted into a reusable block.
@@ -658,7 +693,7 @@ reusable: false,
## `registerBlockCollection`
-* **Type:** `Function`
+- **Type:** `Function`
Blocks can be added to collections, grouping together all blocks from the same origin
@@ -666,7 +701,7 @@ Blocks can be added to collections, grouping together all blocks from the same o
### Namespace
-* **Type:** `String`
+- **Type:** `String`
This should match the namespace declared in the block name; the name of your plugin or theme.
@@ -674,13 +709,13 @@ This should match the namespace declared in the block name; the name of your plu
#### Title
-* **Type:** `String`
+- **Type:** `String`
This will display in the block inserter section, which will list all blocks in this collection.
#### Icon
-* **Type:** `Object`
+- **Type:** `Object`
(Optional) An icon to display alongside the title in the block inserter.
diff --git a/docs/designers-developers/developers/data/data-core-blocks.md b/docs/designers-developers/developers/data/data-core-blocks.md
index 50a9e8891231bc..933c432cd93260 100644
--- a/docs/designers-developers/developers/data/data-core-blocks.md
+++ b/docs/designers-developers/developers/data/data-core-blocks.md
@@ -59,6 +59,20 @@ _Returns_
- `Array`: Block Types.
+# **getBlockVariations**
+
+Returns block variations by block name.
+
+_Parameters_
+
+- _state_ `Object`: Data state.
+- _blockName_ `string`: Block type name.
+- _scope_ `[WPBlockVariationScope]`: Block variation scope name.
+
+_Returns_
+
+- `(Array|void)`: Block variations.
+
# **getCategories**
Returns all the available categories.
@@ -108,6 +122,23 @@ _Returns_
- `?string`: Default block name.
+# **getDefaultBlockVariation**
+
+Returns the default block variation for the given block type.
+When there are multiple variations annotated as the default one,
+the last added item is picked. This simplifies registering overrides.
+When there is no default variation set, it returns the first item.
+
+_Parameters_
+
+- _state_ `Object`: Data state.
+- _blockName_ `string`: Block type name.
+- _scope_ `[WPBlockVariationScope]`: Block variation scope name.
+
+_Returns_
+
+- `?WPBlockVariation`: The default block variation.
+
# **getFreeformFallbackBlockName**
Returns the name of the block for handling non-block content.
@@ -246,6 +277,19 @@ _Returns_
- `Object`: Action object.
+# **addBlockVariations**
+
+Returns an action object used in signalling that new block variations have been added.
+
+_Parameters_
+
+- _blockName_ `string`: Block name.
+- _variations_ `(WPBlockVariation|Array)`: Block variations.
+
+_Returns_
+
+- `Object`: Action object.
+
# **removeBlockCollection**
Returns an action object used to remove block collections
@@ -283,6 +327,19 @@ _Returns_
- `Object`: Action object.
+# **removeBlockVariations**
+
+Returns an action object used in signalling that block variations have been removed.
+
+_Parameters_
+
+- _blockName_ `string`: Block name.
+- _variationNames_ `(string|Array)`: Block variation names.
+
+_Returns_
+
+- `Object`: Action object.
+
# **setCategories**
Returns an action object used to set block categories.
diff --git a/docs/designers-developers/developers/slotfills/plugin-more-menu-item.md b/docs/designers-developers/developers/slotfills/plugin-more-menu-item.md
index 23efd6a021685c..5625d33f03f6e8 100644
--- a/docs/designers-developers/developers/slotfills/plugin-more-menu-item.md
+++ b/docs/designers-developers/developers/slotfills/plugin-more-menu-item.md
@@ -5,13 +5,16 @@ This slot will add a new item to the More Tools & Options section.
## Example
```js
-const { registerPlugin } = wp.plugins;
-const { PluginMoreMenuItem } = wp.editPost;
+import { registerPlugin } from '@wordpress/plugins';
+import { PluginMoreMenuItem } from '@wordpress/edit-post';
+import { image } from '@wordpress/icons';
const MyButtonMoreMenuItemTest = () => (
{ alert( 'Button Clicked' ) } }
+ icon={ image }
+ onClick={ () => {
+ alert( 'Button Clicked' );
+ } }
>
More Menu Item
@@ -23,4 +26,3 @@ registerPlugin( 'more-menu-item-test', { render: MyButtonMoreMenuItemTest } );
## Location
![Location](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/assets/plugin-more-menu-item.png?raw=true)
-
diff --git a/docs/designers-developers/developers/slotfills/plugin-sidebar-more-menu-item.md b/docs/designers-developers/developers/slotfills/plugin-sidebar-more-menu-item.md
index 5bd43819501e74..40d1e0e22bab54 100644
--- a/docs/designers-developers/developers/slotfills/plugin-sidebar-more-menu-item.md
+++ b/docs/designers-developers/developers/slotfills/plugin-sidebar-more-menu-item.md
@@ -3,30 +3,30 @@
This slot allows the creation of a `` with a menu item that when clicked will expand the sidebar to the appropriate Plugin section.
This is done by setting the `target` on `` to match the `name` on the ``
-
## Example
```js
-const { registerPlugin } = wp.plugins;
-
-const {
+import { registerPlugin } from '@wordpress/plugins';
+import {
PluginSidebar,
PluginSidebarMoreMenuItem
-} = wp.editPost;
+} from '@wordpress/edit-post';
+import { image } from '@wordpress/icons';
const { Fragment } = wp.element;
+const myIcon =
const PluginSidebarMoreMenuItemTest = () => (
Expanded Sidebar - More item
Content of the sidebar
diff --git a/docs/designers-developers/developers/slotfills/plugin-sidebar.md b/docs/designers-developers/developers/slotfills/plugin-sidebar.md
index 66121d8c8941cb..8af1641f6ea1fd 100644
--- a/docs/designers-developers/developers/slotfills/plugin-sidebar.md
+++ b/docs/designers-developers/developers/slotfills/plugin-sidebar.md
@@ -6,22 +6,22 @@ Using this slot will add an icon to the bar that, when clicked, will open a side
## Example
```js
-const { registerPlugin } = wp.plugins;
-const { PluginSidebar } = wp.editPost;
+import { registerPlugin } from '@wordpress/plugins';
+import { PluginSidebar } from '@wordpress/edit-post';
+import { image } from '@wordpress/icons';
const PluginSidebarTest = () => {
- return(
+ return (
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/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 = () => (
+ ) }
@@ -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 } ) => (
+ ) }
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/drop-zone/index.js b/packages/components/src/drop-zone/index.js
index 716b609cdbe63b..de2318d4d7c847 100644
--- a/packages/components/src/drop-zone/index.js
+++ b/packages/components/src/drop-zone/index.js
@@ -8,11 +8,11 @@ import classnames from 'classnames';
*/
import { __ } from '@wordpress/i18n';
import { useContext, useEffect, useState, useRef } from '@wordpress/element';
+import { upload, Icon } from '@wordpress/icons';
/**
* Internal dependencies
*/
-import Dashicon from '../dashicon';
import { DropZoneConsumer, Context } from './provider';
export function useDropZone( {
@@ -84,8 +84,8 @@ function DropZoneComponent( {
if ( isDraggingOverElement ) {
children = (
-
diff --git a/packages/components/src/drop-zone/style.scss b/packages/components/src/drop-zone/style.scss
index 414918b3a44a54..a4307cbc6e9e0c 100644
--- a/packages/components/src/drop-zone/style.scss
+++ b/packages/components/src/drop-zone/style.scss
@@ -50,6 +50,7 @@
.components-drop-zone__content-icon {
margin: 0 auto;
line-height: 0;
+ fill: currentColor;
}
diff --git a/packages/components/src/dropdown-menu/README.md b/packages/components/src/dropdown-menu/README.md
index f996795dc7eccc..3790412a69df28 100644
--- a/packages/components/src/dropdown-menu/README.md
+++ b/packages/components/src/dropdown-menu/README.md
@@ -1,6 +1,6 @@
# DropdownMenu
-The DropdownMenu displays a list of actions (each contained in a MenuItem, MenuItemsChoice, or MenuGroup) in a compact way. It appears in a Popover after the user has interacted with an element (a button or icon) or when they perform a specific action.
+The DropdownMenu displays a list of actions (each contained in a MenuItem, MenuItemsChoice, or MenuGroup) in a compact way. It appears in a Popover after the user has interacted with an element (a button or icon) or when they perform a specific action.
![An expanded DropdownMenu, containing a list of MenuItems.](https://wordpress.org/gutenberg/files/2019/01/DropdownMenuExample.png)
@@ -25,8 +25,8 @@ The DropdownMenu displays a list of actions (each contained in a MenuItem, MenuI
Use a DropdownMenu when you want users to:
-- Choose an action or change a setting from a list, AND
-- Only see the available choices contextually.
+- Choose an action or change a setting from a list, AND
+- Only see the available choices contextually.
If you need to display all the available options at all times, consider using a Toolbar instead.
@@ -60,31 +60,38 @@ Render a Dropdown Menu with a set of controls:
```jsx
import { DropdownMenu } from '@wordpress/components';
+import {
+ more,
+ arrowLeft,
+ arrowRight,
+ arrowUp,
+ arrowDown,
+} from '@wordpress/icons';
const MyDropdownMenu = () => (
console.log( 'up' )
+ icon: arrowUp,
+ onClick: () => console.log( 'up' ),
},
{
title: 'Right',
- icon: 'arrow-right-alt',
- onClick: () => console.log( 'right' )
+ icon: arrowRight,
+ onClick: () => console.log( 'right' ),
},
{
title: 'Down',
- icon: 'arrow-down-alt',
- onClick: () => console.log( 'down' )
+ icon: arrowDown,
+ onClick: () => console.log( 'down' ),
},
{
title: 'Left',
- icon: 'arrow-left-alt',
- onClick: () => console.log( 'left' )
+ icon: arrowLeft,
+ onClick: () => console.log( 'left' ),
},
] }
/>
@@ -96,33 +103,22 @@ Alternatively, specify a `children` function which returns elements valid for us
```jsx
import { Fragment } from '@wordpress/element';
import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components';
+import { more, arrowUp, arrowDown, trash } from '@wordpress/icons';
const MyDropdownMenu = () => (
-
+
{ ( { onClose } ) => (
-
+
Move Up
-
+
Move Down
-
+
Remove
@@ -140,9 +136,9 @@ The component accepts the following props:
The [Dashicon](https://developer.wordpress.org/resource/dashicons/) icon slug to be shown in the collapsed menu button.
-- Type: `String|null`
-- Required: No
-- Default: `"menu"`
+- Type: `String|null`
+- Required: No
+- Default: `"menu"`
See also: [https://developer.wordpress.org/resource/dashicons/](https://developer.wordpress.org/resource/dashicons/)
@@ -150,9 +146,9 @@ See also: [https://developer.wordpress.org/resource/dashicons/](https://develope
Whether to display an arrow indicator next to the icon.
-- Type: `Boolean`
-- Required: No
-- Default: `false`
+- Type: `Boolean`
+- Required: No
+- Default: `false`
For backward compatibility, when `icon` is explicitly set to `null` then the arrow indicator will be displayed even when this flag is set to `false`.
@@ -160,8 +156,8 @@ For backward compatibility, when `icon` is explicitly set to `null` then the arr
A human-readable label to present as accessibility text on the focused collapsed menu button.
-- Type: `String`
-- Required: Yes
+- Type: `String`
+- Required: Yes
#### controls
@@ -171,8 +167,8 @@ Each object should include an `icon` [Dashicon](https://developer.wordpress.org/
A valid DropdownMenu must specify one or the other of a `controls` or `children` prop.
-- Type: `Array`
-- Required: No
+- Type: `Array`
+- Required: No
#### children
@@ -180,8 +176,8 @@ A [function render prop](https://reactjs.org/docs/render-props.html#using-props-
A valid DropdownMenu must specify one or the other of a `controls` or `children` prop.
-- Type: `Function`
-- Required: No
+- Type: `Function`
+- Required: No
See also: [https://developer.wordpress.org/resource/dashicons/](https://developer.wordpress.org/resource/dashicons/)
@@ -189,29 +185,29 @@ See also: [https://developer.wordpress.org/resource/dashicons/](https://develope
A class name to apply to the dropdown menu's toggle element wrapper.
-- Type: `String`
-- Required: No
+- Type: `String`
+- Required: No
#### popoverProps
-
+
Properties of `popoverProps` object will be passed as props to the nested `Popover` component.
-Use this object to modify props available for the `Popover` component that are not already exposed in the `DropdownMenu` component, e.g.: the direction in which the popover should open relative to its parent node set with `position` prop.
-
- - Type: `Object`
- - Required: No
-
+Use this object to modify props available for the `Popover` component that are not already exposed in the `DropdownMenu` component, e.g.: the direction in which the popover should open relative to its parent node set with `position` prop.
+
+- Type: `Object`
+- Required: No
+
#### toggleProps
-
+
Properties of `toggleProps` object will be passed as props to the nested `Button` component in the `renderToggle` implementation of the `Dropdown` component used internally.
-Use this object to modify props available for the `Button` component that are not already exposed in the `DropdownMenu` component, e.g.: the tooltip text displayed on hover set with `tooltip` prop.
-
- - Type: `Object`
- - Required: No
-
+Use this object to modify props available for the `Button` component that are not already exposed in the `DropdownMenu` component, e.g.: the tooltip text displayed on hover set with `tooltip` prop.
+
+- Type: `Object`
+- Required: No
+
#### menuProps
-
+
Properties of `menuProps` object will be passed as props to the nested `NavigableMenu` component in the `renderContent` implementation of the `Dropdown` component used internally.
-Use this object to modify props available for the `NavigableMenu` component that are not already exposed in the `DropdownMenu` component, e.g.: the orientation of the menu set with `orientation` prop.
-
- - Type: `Object`
- - Required: No
+Use this object to modify props available for the `NavigableMenu` component that are not already exposed in the `DropdownMenu` component, e.g.: the orientation of the menu set with `orientation` prop.
+
+- Type: `Object`
+- Required: No
diff --git a/packages/components/src/dropdown-menu/test/index.js b/packages/components/src/dropdown-menu/test/index.js
index 6486af19dcb1ee..86d6242ad75309 100644
--- a/packages/components/src/dropdown-menu/test/index.js
+++ b/packages/components/src/dropdown-menu/test/index.js
@@ -7,6 +7,7 @@ import { shallow, mount } from 'enzyme';
* WordPress dependencies
*/
import { DOWN } from '@wordpress/keycodes';
+import { arrowLeft, arrowRight, arrowUp, arrowDown } from '@wordpress/icons';
/**
* Internal dependencies
@@ -22,22 +23,22 @@ describe( 'DropdownMenu', () => {
controls = [
{
title: 'Up',
- icon: 'arrow-up-alt',
+ icon: arrowUp,
onClick: jest.fn(),
},
{
title: 'Right',
- icon: 'arrow-right-alt',
+ icon: arrowRight,
onClick: jest.fn(),
},
{
title: 'Down',
- icon: 'arrow-down-alt',
+ icon: arrowDown,
onClick: jest.fn(),
},
{
title: 'Left',
- icon: 'arrow-left-alt',
+ icon: arrowLeft,
onClick: jest.fn(),
},
];
diff --git a/packages/components/src/dropdown/stories/index.js b/packages/components/src/dropdown/stories/index.js
index c6fd55e4e1f438..e95387634d8a63 100644
--- a/packages/components/src/dropdown/stories/index.js
+++ b/packages/components/src/dropdown/stories/index.js
@@ -1,3 +1,14 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ more,
+ arrowLeft,
+ arrowRight,
+ arrowUp,
+ arrowDown,
+} from '@wordpress/icons';
+
/**
* Internal dependencies
*/
@@ -15,24 +26,24 @@ const DropdownAndDropdownMenuExample = () => {
This is a DropdownMenu component:
@@ -45,7 +56,7 @@ const DropdownAndDropdownMenuExample = () => {
position="bottom right"
renderToggle={ ( { isOpen, onToggle } ) => (
{
) }
renderContent={ () => (
- Up
- Down
- Left
- Right
+ Up
+ Down
+ Left
+ Right
) }
/>
diff --git a/packages/components/src/icon/README.md b/packages/components/src/icon/README.md
index 9fbf69609ccbcb..4566ed9831a5df 100644
--- a/packages/components/src/icon/README.md
+++ b/packages/components/src/icon/README.md
@@ -9,9 +9,7 @@ Allows you to render a raw icon without any initial styling or wrappers.
```jsx
import { Icon } from '@wordpress/components';
-const MyIcon = () => (
-
-);
+const MyIcon = () => ;
```
#### With a function
@@ -20,7 +18,13 @@ const MyIcon = () => (
import { Icon } from '@wordpress/components';
const MyIcon = () => (
- } />
+ (
+
+ ) }
+ />
);
```
@@ -30,9 +34,7 @@ const MyIcon = () => (
import { MyIconComponent } from '../my-icon-component';
import { Icon } from '@wordpress/components';
-const MyIcon = () => (
-
-);
+const MyIcon = () => ;
```
#### With an SVG
@@ -41,7 +43,13 @@ const MyIcon = () => (
import { Icon } from '@wordpress/components';
const MyIcon = () => (
- } />
+
+
+
+ }
+ />
);
```
@@ -50,9 +58,7 @@ const MyIcon = () => (
```jsx
import { Icon } from '@wordpress/components';
-const MyIcon = () => (
-
-);
+const MyIcon = () => ;
```
## Props
@@ -63,14 +69,14 @@ The component accepts the following props. Any additional props are passed throu
The icon to render. Supported values are: Dashicons (specified as strings), functions, WPComponent instances and `null`.
-- Type: `String|Function|WPComponent|null`
-- Required: No
-- Default: `null`
+- Type: `String|Function|WPComponent|null`
+- Required: No
+- Default: `null`
### size
The size (width and height) of the icon.
-- Type: `Number`
-- Required: No
-- Default: `20` when a Dashicon is rendered, `24` for all other icons.
+- Type: `Number`
+- Required: No
+- Default: `20` when a Dashicon is rendered, `24` for all other icons.
diff --git a/packages/components/src/menu-item/test/__snapshots__/index.js.snap b/packages/components/src/menu-item/test/__snapshots__/index.js.snap
index 34171596d61d7f..bdd62deacfda1a 100644
--- a/packages/components/src/menu-item/test/__snapshots__/index.js.snap
+++ b/packages/components/src/menu-item/test/__snapshots__/index.js.snap
@@ -4,7 +4,19 @@ exports[`MenuItem should match snapshot when all props provided 1`] = `
+
+
+ }
onClick={[Function]}
role="menuitemcheckbox"
>
@@ -40,7 +52,19 @@ exports[`MenuItem should match snapshot when info is provided 1`] = `
exports[`MenuItem should match snapshot when isSelected and role are optionally provided 1`] = `
+
+
+ }
onClick={[Function]}
role="menuitem"
>
diff --git a/packages/components/src/menu-item/test/index.js b/packages/components/src/menu-item/test/index.js
index 3146a95c2c163d..3d1a66013f5cd9 100644
--- a/packages/components/src/menu-item/test/index.js
+++ b/packages/components/src/menu-item/test/index.js
@@ -4,6 +4,11 @@
import { shallow } from 'enzyme';
import { noop } from 'lodash';
+/**
+ * WordPress dependencies
+ */
+import { more } from '@wordpress/icons';
+
/**
* Internal dependencies
*/
@@ -20,7 +25,7 @@ describe( 'MenuItem', () => {
const wrapper = shallow(
{
const wrapper = shallow(
diff --git a/packages/components/src/mobile/bottom-sheet/stepper-cell/stepper.android.js b/packages/components/src/mobile/bottom-sheet/stepper-cell/stepper.android.js
index 8215550b4f4a55..ca7f90eb845df0 100644
--- a/packages/components/src/mobile/bottom-sheet/stepper-cell/stepper.android.js
+++ b/packages/components/src/mobile/bottom-sheet/stepper-cell/stepper.android.js
@@ -6,7 +6,7 @@ import { Text, TouchableOpacity, View } from 'react-native';
/**
* WordPress dependencies
*/
-import { Dashicon } from '@wordpress/components';
+import { Icon, chevronDown, chevronUp } from '@wordpress/icons';
import { withPreferredColorScheme } from '@wordpress/compose';
/**
@@ -47,8 +47,8 @@ function Stepper( {
isMinValue ? { opacity: 0.4 } : null,
] }
>
-
@@ -63,8 +63,8 @@ function Stepper( {
isMaxValue ? { opacity: 0.4 } : null,
] }
>
-
diff --git a/packages/components/src/mobile/stepper-control/README.md b/packages/components/src/mobile/stepper-control/README.md
index c80ee305fcb767..736378dfba1a4a 100644
--- a/packages/components/src/mobile/stepper-control/README.md
+++ b/packages/components/src/mobile/stepper-control/README.md
@@ -1,5 +1,4 @@
-StepperControl
-===================
+# StepperControl
`StepperControl` shows a stepper control to change a value wrapped in a `StepperCell` component.
@@ -9,61 +8,62 @@ Usage example
```jsx
import { StepperControl } from '@wordpress/components';
+import { more } from '@wordpress/icons';
function Stepper( { onChangeValue, value } ) {
return (
-
+
);
}
```
## Props
-### maxValue
+### maxValue
Maximum value of the stepper.
-- Type: `Number`
-- Required: Yes
-- Platform: Mobile
+- Type: `Number`
+- Required: Yes
+- Platform: Mobile
-### minValue
+### minValue
Minimum value of the stepper.
-- Type: `Number`
-- Required: Yes
-- Platform: Mobile
+- Type: `Number`
+- Required: Yes
+- Platform: Mobile
-### step
+### step
Step increment value.
-- Type: `Number`
-- Required: No
-- Platform: Mobile
+- Type: `Number`
+- Required: No
+- Platform: Mobile
-### value
+### value
Current value of the stepper.
-- Type: `Number`
-- Required: Yes
-- Platform: Mobile
+- Type: `Number`
+- Required: Yes
+- Platform: Mobile
### onChangeValue
Callback called when the value has changed
-- Type: `Function`
-- Required: Yes
-- Platform: Mobile
+- Type: `Function`
+- Required: Yes
+- Platform: Mobile
-The argument of the callback is the updated value as a `Number`.
\ No newline at end of file
+The argument of the callback is the updated value as a `Number`.
diff --git a/packages/components/src/panel/README.md b/packages/components/src/panel/README.md
index c736ed88b9fa40..8573230f14d35f 100644
--- a/packages/components/src/panel/README.md
+++ b/packages/components/src/panel/README.md
@@ -1,5 +1,6 @@
# Panel
- Panels expand and collapse multiple sections of content.
+
+Panels expand and collapse multiple sections of content.
![](https://make.wordpress.org/design/files/2019/03/panel.png)
@@ -29,15 +30,15 @@ Panels show and hide details of list items by expanding and collapsing list cont
Use Panels when it’s helpful to:
-- See an overview of multiple, related sections of content.
-- Show and hide those sections as needed.
-- Hide information that is lower priority that users don’t need to see all the time.
-- View more than one section at a time.
+- See an overview of multiple, related sections of content.
+- Show and hide those sections as needed.
+- Hide information that is lower priority that users don’t need to see all the time.
+- View more than one section at a time.
Consider an alternative component when:
-- There’s crucial information or error messages that require immediate action.
-- You need to quickly switch between only a few sections (consider using Tabs instead).
+- There’s crucial information or error messages that require immediate action.
+- You need to quickly switch between only a few sections (consider using Tabs instead).
### Behavior
@@ -59,17 +60,12 @@ The `Panel` creates a container with a header that can take collapsible `PanelBo
```jsx
import { Panel, PanelBody, PanelRow } from '@wordpress/components';
-
+import { more } from '@wordpress/icons';
+
const MyPanel = () => (
-
-
- My Panel Inputs and Labels
-
+
+ My Panel Inputs and Labels
);
@@ -85,20 +81,21 @@ const MyPanel = () => (
The class that will be added with `components-panel`. If no `className` is passed only `components-panel__body` and `is-opened` is used.
-- Type: `String`
-- Required: No
+- Type: `String`
+- Required: No
###### header
Title of the `Panel`. Text will be rendered inside an `
` tag.
-- Type: `String`
-- Required: No
+- Type: `String`
+- Required: No
---
+
#### PanelBody
-The `PanelBody` creates a collapsible container that can be toggled open or closed.
+The `PanelBody` creates a collapsible container that can be toggled open or closed.
##### Props
@@ -106,47 +103,47 @@ The `PanelBody` creates a collapsible container that can be toggled open or clos
Title of the `PanelBody`. This shows even when it is closed.
-- Type: `String`
-- Required: No
-
+- Type: `String`
+- Required: No
###### opened
If opened is true then the `Panel` will remain open regardless of the `initialOpen` prop and the panel will be prevented from being closed.
-- Type: `Boolean`
-- Required: No
+- Type: `Boolean`
+- Required: No
###### className
The class that will be added with `components-panel__body`, if the panel is currently open, the `is-opened` class will also be passed to the classes of the wrapper div. If no `className` is passed then only `components-panel__body` and `is-opened` is used.
-- Type: `String`
-- Required: No
+- Type: `String`
+- Required: No
###### icon
An icon to be shown next to the `PanelBody` title.
-- Type: `String`
-- Required: No
+- Type: `String`
+- Required: No
###### onToggle
A function that is called when the user clicks on the `PanelBody` title after the open state is changed.
-- Type: `function`
-- Required: No
+- Type: `function`
+- Required: No
###### initialOpen
Whether or not the panel will start open.
-- Type: `Boolean`
-- Required: No
-- Default: true
+- Type: `Boolean`
+- Required: No
+- Default: true
---
+
#### PanelRow
The is a generic container for panel content. Default styles add a top margin and arrange items in a flex row.
@@ -155,10 +152,11 @@ The is a generic container for panel content. Default styles add a top margin an
###### className
-The class that will be added with `components-panel__row`. to the classes of the wrapper div. If no `className` is passed only `components-panel__row` is used.
+The class that will be added with `components-panel__row`. to the classes of the wrapper div. If no `className` is passed only `components-panel__row` is used.
+
+- Type: `String`
+- Required: No
-- Type: `String`
-- Required: No
---
#### PanelHeader
@@ -171,8 +169,9 @@ This is a simple container for a header component. This is used by the `Panel` c
The text that will be rendered as the title of the `Panel`. Will be rendered in an `
` tag.
-- Type: `String`
-- Required: No
+- Type: `String`
+- Required: No
## Related components
-- To divide related sections of content accessed by a horizontal menu, use `TabPanel`
+
+- To divide related sections of content accessed by a horizontal menu, use `TabPanel`
diff --git a/packages/components/src/placeholder/README.md b/packages/components/src/placeholder/README.md
index 9e5bf053417d0d..759883cf358353 100644
--- a/packages/components/src/placeholder/README.md
+++ b/packages/components/src/placeholder/README.md
@@ -1,22 +1,19 @@
# Placeholder
## Usage
+
```jsx
import { Placeholder } from '@wordpress/components';
+import { more } from '@wordpress/icons';
-const MyPlaceholder = () => (
-
-);
+const MyPlaceholder = () => ;
```
### Props
-Name | Type | Default | Description
---- | --- | --- | ---
-`icon` | `string, WPElement` | `undefined` | If provided, renders an icon next to the label.
-`label` | `string` | `undefined` | Renders a label for the placeholder.
-`instructions` | `string` | `undefined` | Renders instruction text below label.
-`isColumnLayout` | `bool` | `false` | Changes placeholder children layout from flex-row to flex-column.
+| Name | Type | Default | Description |
+| ---------------- | ------------------- | ----------- | ----------------------------------------------------------------- |
+| `icon` | `string, WPElement` | `undefined` | If provided, renders an icon next to the label. |
+| `label` | `string` | `undefined` | Renders a label for the placeholder. |
+| `instructions` | `string` | `undefined` | Renders instruction text below label. |
+| `isColumnLayout` | `bool` | `false` | Changes placeholder children layout from flex-row to flex-column. |
diff --git a/packages/components/src/placeholder/test/index.js b/packages/components/src/placeholder/test/index.js
index bf14ba0f00ec49..cf14e596469c9c 100644
--- a/packages/components/src/placeholder/test/index.js
+++ b/packages/components/src/placeholder/test/index.js
@@ -4,6 +4,11 @@
import { shallow } from 'enzyme';
import useResizeAware from 'react-resize-aware';
+/**
+ * WordPress dependencies
+ */
+import { more } from '@wordpress/icons';
+
/**
* Internal dependencies
*/
@@ -44,7 +49,7 @@ describe( 'Placeholder', () => {
} );
it( 'should render an Icon in the label section', () => {
- const placeholder = shallow( );
+ const placeholder = shallow( );
const placeholderLabel = placeholder.find(
'.components-placeholder__label'
);
diff --git a/packages/components/src/toolbar-group/stories/index.js b/packages/components/src/toolbar-group/stories/index.js
index 9802f9ed53f334..c24447be9dfab4 100644
--- a/packages/components/src/toolbar-group/stories/index.js
+++ b/packages/components/src/toolbar-group/stories/index.js
@@ -1,3 +1,8 @@
+/**
+ * WordPress dependencies
+ */
+import { formatBold, formatItalic, link } from '@wordpress/icons';
+
/**
* Internal dependencies
*/
@@ -8,9 +13,9 @@ export default { title: 'Components/ToolbarGroup', component: ToolbarGroup };
export const _default = () => {
return (
-
-
-
+
+
+
);
};
@@ -19,9 +24,9 @@ export const withControlsProp = () => {
return (
);
diff --git a/packages/components/src/toolbar/stories/index.js b/packages/components/src/toolbar/stories/index.js
index fe56ac39f7881a..9a822836e2ade8 100644
--- a/packages/components/src/toolbar/stories/index.js
+++ b/packages/components/src/toolbar/stories/index.js
@@ -1,3 +1,19 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ alignCenter,
+ alignLeft,
+ alignRight,
+ code,
+ formatBold,
+ formatItalic,
+ formatStrikethrough,
+ link,
+ more,
+ paragraph,
+} from '@wordpress/icons';
+
/**
* Internal dependencies
*/
@@ -30,27 +46,27 @@ export const _default = () => {
id="options-toolbar"
>
-
+
{ ( toggleProps ) => (
{
Text
-
-
-
+
+
+ , title: 'Inline image' },
{
- icon: 'editor-strikethrough',
+ icon: formatStrikethrough,
title: 'Strikethrough',
},
] }
@@ -80,17 +96,17 @@ export const _default = () => {
@@ -104,9 +120,9 @@ export const withoutGroup = () => {
__experimentalAccessibilityLabel="Options"
id="options-toolbar-without-group"
>
-
-
-
+
+
+
);
};
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)).
diff --git a/packages/e2e-tests/specs/editor/various/adding-blocks.test.js b/packages/e2e-tests/specs/editor/various/adding-blocks.test.js
index 2434e541990914..ed5244c50c189a 100644
--- a/packages/e2e-tests/specs/editor/various/adding-blocks.test.js
+++ b/packages/e2e-tests/specs/editor/various/adding-blocks.test.js
@@ -42,7 +42,7 @@ describe( 'adding blocks', () => {
// Click below editor to focus last field (block appender)
await clickAtBottom(
- await page.$( '.edit-post-editor-regions__content' )
+ await page.$( '.block-editor-editor-skeleton__content' )
);
expect( await page.$( '[data-type="core/paragraph"]' ) ).not.toBeNull();
await page.keyboard.type( 'Paragraph block' );
diff --git a/packages/e2e-tests/specs/editor/various/keyboard-navigable-blocks.test.js b/packages/e2e-tests/specs/editor/various/keyboard-navigable-blocks.test.js
index a32474aad3b5ab..815528f8c0e7ba 100644
--- a/packages/e2e-tests/specs/editor/various/keyboard-navigable-blocks.test.js
+++ b/packages/e2e-tests/specs/editor/various/keyboard-navigable-blocks.test.js
@@ -152,7 +152,7 @@ describe( 'Order of block keyboard navigation', () => {
await page.evaluate( () => {
document.querySelector( '.edit-post-visual-editor' ).focus();
document
- .querySelector( '.edit-post-editor-regions__sidebar' )
+ .querySelector( '.block-editor-editor-skeleton__sidebar' )
.focus();
} );
diff --git a/packages/edit-post/README.md b/packages/edit-post/README.md
index 070d4e743da467..641bb89209f185 100644
--- a/packages/edit-post/README.md
+++ b/packages/edit-post/README.md
@@ -166,6 +166,7 @@ _Usage_
// Using ES5 syntax
var __ = wp.i18n.__;
var PluginMoreMenuItem = wp.editPost.PluginMoreMenuItem;
+var moreIcon = wp.element.createElement( 'svg' ); //... svg element.
function onButtonClick() {
alert( 'Button clicked.' );
@@ -175,18 +176,19 @@ function MyButtonMoreMenuItem() {
return wp.element.createElement(
PluginMoreMenuItem,
{
- icon: 'smiley',
- onClick: onButtonClick
+ icon: moreIcon,
+ onClick: onButtonClick,
},
__( 'My button title' )
- )
+ );
}
```
```jsx
// Using ESNext syntax
-const { __ } = wp.i18n;
-const { PluginMoreMenuItem } = wp.editPost;
+import { __ } from '@wordpress/i18n';
+import { PluginMoreMenuItem } from '@wordpress/edit-post';
+import { more } from '@wordpress/icons';
function onButtonClick() {
alert( 'Button clicked.' );
@@ -194,7 +196,7 @@ function onButtonClick() {
const MyButtonMoreMenuItem = () => (
{ __( 'My button title' ) }
@@ -388,6 +390,7 @@ var __ = wp.i18n.__;
var el = wp.element.createElement;
var PanelBody = wp.components.PanelBody;
var PluginSidebar = wp.editPost.PluginSidebar;
+var moreIcon = wp.element.createElement( 'svg' ); //... svg element.
function MyPluginSidebar() {
return el(
@@ -395,7 +398,7 @@ function MyPluginSidebar() {
{
name: 'my-sidebar',
title: 'My sidebar title',
- icon: 'smiley',
+ icon: moreIcon,
},
el(
PanelBody,
@@ -408,15 +411,16 @@ function MyPluginSidebar() {
```jsx
// Using ESNext syntax
-const { __ } = wp.i18n;
-const { PanelBody } = wp.components;
-const { PluginSidebar } = wp.editPost;
+import { __ } from '@wordpress/i18n';
+import { PanelBody } from '@wordpress/components';
+import { PluginSidebar } from '@wordpress/edit-post';
+import { more } from '@wordpress/icons';
const MyPluginSidebar = () => (
{ __( 'My sidebar content' ) }
@@ -450,13 +454,14 @@ _Usage_
// Using ES5 syntax
var __ = wp.i18n.__;
var PluginSidebarMoreMenuItem = wp.editPost.PluginSidebarMoreMenuItem;
+var moreIcon = wp.element.createElement( 'svg' ); //... svg element.
function MySidebarMoreMenuItem() {
return wp.element.createElement(
PluginSidebarMoreMenuItem,
{
target: 'my-sidebar',
- icon: 'smiley',
+ icon: moreIcon,
},
__( 'My sidebar title' )
)
@@ -465,13 +470,14 @@ function MySidebarMoreMenuItem() {
```jsx
// Using ESNext syntax
-const { __ } = wp.i18n;
-const { PluginSidebarMoreMenuItem } = wp.editPost;
+import { __ } from '@wordpress/i18n';
+import { PluginSidebarMoreMenuItem } from '@wordpress/edit-post';
+import { more } from '@wordpress/icons';
const MySidebarMoreMenuItem = () => (
{ __( 'My sidebar title' ) }
diff --git a/packages/edit-post/src/components/header/fullscreen-mode-close/index.js b/packages/edit-post/src/components/header/fullscreen-mode-close/index.js
index cd91c8308000c0..d1fe2ffd3cab39 100644
--- a/packages/edit-post/src/components/header/fullscreen-mode-close/index.js
+++ b/packages/edit-post/src/components/header/fullscreen-mode-close/index.js
@@ -10,6 +10,7 @@ import { withSelect } from '@wordpress/data';
import { Button, Toolbar } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
+import { chevronLeft } from '@wordpress/icons';
function FullscreenModeClose( { isActive, postType } ) {
if ( ! isActive || ! postType ) {
@@ -19,7 +20,7 @@ function FullscreenModeClose( { isActive, postType } ) {
return (
(
* // Using ES5 syntax
* var __ = wp.i18n.__;
* var PluginMoreMenuItem = wp.editPost.PluginMoreMenuItem;
+ * var moreIcon = wp.element.createElement( 'svg' ); //... svg element.
*
* function onButtonClick() {
* alert( 'Button clicked.' );
@@ -50,19 +51,20 @@ const PluginMoreMenuItem = ( { onClick = noop, ...props } ) => (
* return wp.element.createElement(
* PluginMoreMenuItem,
* {
- * icon: 'smiley',
- * onClick: onButtonClick
+ * icon: moreIcon,
+ * onClick: onButtonClick,
* },
* __( 'My button title' )
- * )
+ * );
* }
* ```
*
* @example
* ```jsx
* // Using ESNext syntax
- * const { __ } = wp.i18n;
- * const { PluginSidebarMoreMenuItem } = wp.editPost;
+ * import { __ } from '@wordpress/i18n';
+ * import { PluginSidebarMoreMenuItem } from '@wordpress/edit-post';
+ * import { more } from '@wordpress/icons';
*
* const MySidebarMoreMenuItem = () => (
*
* { __( 'My sidebar title' ) }
*
diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js
index 57bc10c9c077d9..fcd2e693e99e96 100644
--- a/packages/edit-post/src/components/layout/index.js
+++ b/packages/edit-post/src/components/layout/index.js
@@ -17,6 +17,7 @@ import {
import { useSelect, useDispatch } from '@wordpress/data';
import {
BlockBreadcrumb,
+ __experimentalEditorSkeleton as EditorSkeleton,
__experimentalPageTemplatePicker,
__experimentalUsePageTemplatePickerVisible,
} from '@wordpress/block-editor';
@@ -39,7 +40,6 @@ import EditPostKeyboardShortcuts from '../keyboard-shortcuts';
import KeyboardShortcutHelpModal from '../keyboard-shortcut-help-modal';
import ManageBlocksModal from '../manage-blocks-modal';
import OptionsModal from '../options-modal';
-import EditorRegions from '../editor-regions';
import FullscreenMode from '../fullscreen-mode';
import BrowserURL from '../browser-url';
import Header from '../header';
@@ -124,7 +124,7 @@ function Layout() {
- }
sidebar={
diff --git a/packages/edit-post/src/components/sidebar/plugin-sidebar/index.js b/packages/edit-post/src/components/sidebar/plugin-sidebar/index.js
index 179463f8aeeac2..01ddbbca79a43f 100644
--- a/packages/edit-post/src/components/sidebar/plugin-sidebar/index.js
+++ b/packages/edit-post/src/components/sidebar/plugin-sidebar/index.js
@@ -90,6 +90,7 @@ function PluginSidebar( props ) {
* var el = wp.element.createElement;
* var PanelBody = wp.components.PanelBody;
* var PluginSidebar = wp.editPost.PluginSidebar;
+ * var moreIcon = wp.element.createElement( 'svg' ); //... svg element.
*
* function MyPluginSidebar() {
* return el(
@@ -97,7 +98,7 @@ function PluginSidebar( props ) {
* {
* name: 'my-sidebar',
* title: 'My sidebar title',
- * icon: 'smiley',
+ * icon: moreIcon,
* },
* el(
* PanelBody,
@@ -111,15 +112,16 @@ function PluginSidebar( props ) {
* @example
ESNext
* ```jsx
* // Using ESNext syntax
- * const { __ } = wp.i18n;
- * const { PanelBody } = wp.components;
- * const { PluginSidebar } = wp.editPost;
+ * import { __ } from '@wordpress/i18n';
+ * import { PanelBody } from '@wordpress/components';
+ * import { PluginSidebar } from '@wordpress/edit-post';
+ * import { more } from '@wordpress/icons';
*
* const MyPluginSidebar = () => (
*
*
* { __( 'My sidebar content' ) }
diff --git a/packages/edit-post/src/components/visual-editor/block-inspector-button.js b/packages/edit-post/src/components/visual-editor/block-inspector-button.js
index 977e3f55d2de9a..d566300730e324 100644
--- a/packages/edit-post/src/components/visual-editor/block-inspector-button.js
+++ b/packages/edit-post/src/components/visual-editor/block-inspector-button.js
@@ -9,6 +9,7 @@ import { noop } from 'lodash';
import { __ } from '@wordpress/i18n';
import { MenuItem, withSpokenMessages } from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data';
+import { cog } from '@wordpress/icons';
export function BlockInspectorButton( {
onClick = noop,
@@ -57,7 +58,7 @@ export function BlockInspectorButton( {
onClick();
}
} }
- icon="admin-generic"
+ icon={ cog }
shortcut={ shortcut }
>
{ ! small && label }
diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js
index 1f10cb7a36ba6d..086553ee738b61 100644
--- a/packages/edit-post/src/index.js
+++ b/packages/edit-post/src/index.js
@@ -113,14 +113,14 @@ export function initializeEditor(
// Without this hack the browser scrolls the mobile toolbar off-screen.
// Once supported in Safari we can replace this in favor of preventScroll.
// For details see issue #18632 and PR #18686
- // Specifically, we scroll `edit-post-editor-regions__body` to enable a fixed top toolbar.
+ // Specifically, we scroll `block-editor-editor-skeleton__body` to enable a fixed top toolbar.
// But Mobile Safari forces the `html` element to scroll upwards, hiding the toolbar.
const isIphone = window.navigator.userAgent.indexOf( 'iPhone' ) !== -1;
if ( isIphone ) {
window.addEventListener( 'scroll', function( event ) {
const editorScrollContainer = document.getElementsByClassName(
- 'edit-post-editor-regions__body'
+ 'block-editor-editor-skeleton__body'
)[ 0 ];
if ( event.target === document ) {
// Scroll element into view by scrolling the editor container by the same amount
diff --git a/packages/edit-post/src/style.scss b/packages/edit-post/src/style.scss
index 43b69c59da22cc..babb793d4ac516 100644
--- a/packages/edit-post/src/style.scss
+++ b/packages/edit-post/src/style.scss
@@ -1,6 +1,5 @@
$footer-height: $icon-button-size-small;
-@import "./components/editor-regions/style.scss";
@import "./components/fullscreen-mode/style.scss";
@import "./components/header/style.scss";
@import "./components/header/fullscreen-mode-close/style.scss";
diff --git a/packages/editor/src/components/editor-history/redo.js b/packages/editor/src/components/editor-history/redo.js
index ea5ad889f6d646..688c259201e00d 100644
--- a/packages/editor/src/components/editor-history/redo.js
+++ b/packages/editor/src/components/editor-history/redo.js
@@ -6,11 +6,12 @@ import { Button } from '@wordpress/components';
import { withSelect, withDispatch } from '@wordpress/data';
import { compose } from '@wordpress/compose';
import { displayShortcut } from '@wordpress/keycodes';
+import { redo as redoIcon } from '@wordpress/icons';
function EditorHistoryRedo( { hasRedo, redo } ) {
return (
this.setState( { isCopied: true } ) }
aria-disabled={ isCopied }
- icon="admin-links"
+ icon={ linkIcon }
/>
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/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"
+ ]
+}
+```
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'
+ );
+ }
+ } );
+} );
diff --git a/packages/format-library/package.json b/packages/format-library/package.json
index 49c7b488a0b0dc..a970f595c36ea2 100644
--- a/packages/format-library/package.json
+++ b/packages/format-library/package.json
@@ -28,6 +28,7 @@
"@wordpress/element": "file:../element",
"@wordpress/html-entities": "file:../html-entities",
"@wordpress/i18n": "file:../i18n",
+ "@wordpress/icons": "file:../icons",
"@wordpress/keycodes": "file:../keycodes",
"@wordpress/rich-text": "file:../rich-text",
"@wordpress/url": "file:../url",
diff --git a/packages/format-library/src/bold/index.js b/packages/format-library/src/bold/index.js
index 37d920490a8c9a..8ad18c2c90e54d 100644
--- a/packages/format-library/src/bold/index.js
+++ b/packages/format-library/src/bold/index.js
@@ -8,6 +8,7 @@ import {
RichTextShortcut,
__unstableRichTextInputEvent,
} from '@wordpress/block-editor';
+import { formatBold } from '@wordpress/icons';
const name = 'core/bold';
const title = __( 'Bold' );
@@ -36,7 +37,7 @@ export const bold = {
/>
diff --git a/packages/format-library/src/italic/index.js b/packages/format-library/src/italic/index.js
index 54ebc758a88eb3..51b1793bc4980f 100644
--- a/packages/format-library/src/italic/index.js
+++ b/packages/format-library/src/italic/index.js
@@ -8,6 +8,7 @@ import {
RichTextShortcut,
__unstableRichTextInputEvent,
} from '@wordpress/block-editor';
+import { formatItalic } from '@wordpress/icons';
const name = 'core/italic';
const title = __( 'Italic' );
@@ -36,7 +37,7 @@ export const italic = {
/>
+
+
+);
+
+export default arrowDown;
diff --git a/packages/icons/src/library/arrow-left.js b/packages/icons/src/library/arrow-left.js
new file mode 100644
index 00000000000000..859468b10cdf9d
--- /dev/null
+++ b/packages/icons/src/library/arrow-left.js
@@ -0,0 +1,12 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const arrowLeft = (
+
+);
+
+export default arrowLeft;
diff --git a/packages/icons/src/library/arrow-right.js b/packages/icons/src/library/arrow-right.js
new file mode 100644
index 00000000000000..a1f22767863385
--- /dev/null
+++ b/packages/icons/src/library/arrow-right.js
@@ -0,0 +1,12 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const arrowRight = (
+
+);
+
+export default arrowRight;
diff --git a/packages/icons/src/library/arrow-up.js b/packages/icons/src/library/arrow-up.js
new file mode 100644
index 00000000000000..26ba4dc1c7e34f
--- /dev/null
+++ b/packages/icons/src/library/arrow-up.js
@@ -0,0 +1,12 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const arrowUp = (
+
+);
+
+export default arrowUp;
diff --git a/packages/icons/src/library/chevron-left.js b/packages/icons/src/library/chevron-left.js
new file mode 100644
index 00000000000000..a2d6121afe1c25
--- /dev/null
+++ b/packages/icons/src/library/chevron-left.js
@@ -0,0 +1,12 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const chevronLeft = (
+
+);
+
+export default chevronLeft;
diff --git a/packages/icons/src/library/chevron-right.js b/packages/icons/src/library/chevron-right.js
new file mode 100644
index 00000000000000..bbfdedf904cffc
--- /dev/null
+++ b/packages/icons/src/library/chevron-right.js
@@ -0,0 +1,12 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const chevronRight = (
+
+);
+
+export default chevronRight;
diff --git a/packages/icons/src/library/cog.js b/packages/icons/src/library/cog.js
new file mode 100644
index 00000000000000..9417b61dc043b8
--- /dev/null
+++ b/packages/icons/src/library/cog.js
@@ -0,0 +1,12 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const cog = (
+
+);
+
+export default cog;
diff --git a/packages/icons/src/library/format-bold.js b/packages/icons/src/library/format-bold.js
new file mode 100644
index 00000000000000..61be513b67ef7c
--- /dev/null
+++ b/packages/icons/src/library/format-bold.js
@@ -0,0 +1,12 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const formatBold = (
+
+);
+
+export default formatBold;
diff --git a/packages/icons/src/library/format-italic.js b/packages/icons/src/library/format-italic.js
new file mode 100644
index 00000000000000..eb60895996bbde
--- /dev/null
+++ b/packages/icons/src/library/format-italic.js
@@ -0,0 +1,12 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const formatItalic = (
+
+);
+
+export default formatItalic;
diff --git a/packages/icons/src/library/format-strikethrough.js b/packages/icons/src/library/format-strikethrough.js
new file mode 100644
index 00000000000000..454e09325e333c
--- /dev/null
+++ b/packages/icons/src/library/format-strikethrough.js
@@ -0,0 +1,12 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const formatStrikethrough = (
+
+);
+
+export default formatStrikethrough;
diff --git a/packages/icons/src/library/keyboard-return.js b/packages/icons/src/library/keyboard-return.js
new file mode 100644
index 00000000000000..4855d708131fb9
--- /dev/null
+++ b/packages/icons/src/library/keyboard-return.js
@@ -0,0 +1,12 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const keyboardReturn = (
+
+);
+
+export default keyboardReturn;
diff --git a/packages/icons/src/library/link-off.js b/packages/icons/src/library/link-off.js
new file mode 100644
index 00000000000000..ff3d81da3854be
--- /dev/null
+++ b/packages/icons/src/library/link-off.js
@@ -0,0 +1,12 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const linkOff = (
+
+);
+
+export default linkOff;
diff --git a/packages/icons/src/library/link.js b/packages/icons/src/library/link.js
new file mode 100644
index 00000000000000..ec3aed86833d32
--- /dev/null
+++ b/packages/icons/src/library/link.js
@@ -0,0 +1,12 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const link = (
+
+);
+
+export default link;
diff --git a/packages/icons/src/library/pencil.js b/packages/icons/src/library/pencil.js
new file mode 100644
index 00000000000000..d29965a5f1e63b
--- /dev/null
+++ b/packages/icons/src/library/pencil.js
@@ -0,0 +1,12 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const pencil = (
+
+);
+
+export default pencil;
diff --git a/packages/icons/src/library/redo.js b/packages/icons/src/library/redo.js
new file mode 100644
index 00000000000000..d3275878ff4fd8
--- /dev/null
+++ b/packages/icons/src/library/redo.js
@@ -0,0 +1,12 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const redo = (
+
+);
+
+export default redo;
diff --git a/packages/icons/src/library/undo.js b/packages/icons/src/library/undo.js
new file mode 100644
index 00000000000000..e1122960ba584c
--- /dev/null
+++ b/packages/icons/src/library/undo.js
@@ -0,0 +1,12 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const undo = (
+
+);
+
+export default undo;
diff --git a/packages/icons/src/library/update.js b/packages/icons/src/library/update.js
new file mode 100644
index 00000000000000..d751fb970ea551
--- /dev/null
+++ b/packages/icons/src/library/update.js
@@ -0,0 +1,12 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const update = (
+
+);
+
+export default update;
diff --git a/packages/icons/src/library/upload.js b/packages/icons/src/library/upload.js
new file mode 100644
index 00000000000000..30d05e3f109910
--- /dev/null
+++ b/packages/icons/src/library/upload.js
@@ -0,0 +1,12 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const upload = (
+
+);
+
+export default upload;
diff --git a/packages/plugins/README.md b/packages/plugins/README.md
index 9bf049e1bc6abc..5bc811d5c6565a 100644
--- a/packages/plugins/README.md
+++ b/packages/plugins/README.md
@@ -86,6 +86,7 @@ var Fragment = wp.element.Fragment;
var PluginSidebar = wp.editPost.PluginSidebar;
var PluginSidebarMoreMenuItem = wp.editPost.PluginSidebarMoreMenuItem;
var registerPlugin = wp.plugins.registerPlugin;
+var moreIcon = wp.element.createElement( 'svg' ); //... svg element.
function Component() {
return el(
@@ -109,15 +110,16 @@ function Component() {
);
}
registerPlugin( 'plugin-name', {
- icon: 'smiley',
+ icon: moreIcon,
render: Component,
} );
```
```js
// Using ESNext syntax
-const { PluginSidebar, PluginSidebarMoreMenuItem } = wp.editPost;
-const { registerPlugin } = wp.plugins;
+import { PluginSidebar, PluginSidebarMoreMenuItem } from '@wordpress/edit-post';
+import { registerPlugin } from '@wordpress/plugins';
+import { more } from '@wordpress/icons';
const Component = () => (
<>
@@ -136,7 +138,7 @@ const Component = () => (
);
registerPlugin( 'plugin-name', {
- icon: 'smiley',
+ icon: more,
render: Component,
} );
```
diff --git a/packages/plugins/src/api/index.js b/packages/plugins/src/api/index.js
index 30d51258df07e8..970cd978593c12 100644
--- a/packages/plugins/src/api/index.js
+++ b/packages/plugins/src/api/index.js
@@ -48,6 +48,7 @@ const plugins = {};
* var PluginSidebar = wp.editPost.PluginSidebar;
* var PluginSidebarMoreMenuItem = wp.editPost.PluginSidebarMoreMenuItem;
* var registerPlugin = wp.plugins.registerPlugin;
+ * var moreIcon = wp.element.createElement( 'svg' ); //... svg element.
*
* function Component() {
* return el(
@@ -71,7 +72,7 @@ const plugins = {};
* );
* }
* registerPlugin( 'plugin-name', {
- * icon: 'smiley',
+ * icon: moreIcon,
* render: Component,
* } );
* ```
@@ -79,8 +80,9 @@ const plugins = {};
* @example