diff --git a/.gitignore b/.gitignore
index 96f49190bf8b5..fc31278c325de 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,6 @@ playground/dist
# Report generated from jest-junit
test/native/junit.xml
+
+# Local overrides
+.wp-env.override.json
diff --git a/.travis.yml b/.travis.yml
index 122858672c93e..593dde89c7b9b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -24,7 +24,6 @@ branches:
env:
global:
- - PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
- WP_DEVELOP_DIR: ./wordpress
- LOCAL_SCRIPT_DEBUG: false
- INSTALL_COMPOSER: false
@@ -162,49 +161,49 @@ jobs:
- npm run test-php && npm run test-unit-php-multisite
- name: E2E tests (Admin) (1/4)
- env: FORCE_REDUCED_MOTION=true PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=
+ env: FORCE_REDUCED_MOTION=true
script:
- $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --listTests > ~/.jest-e2e-tests
- $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --cacheDirectory="$HOME/.jest-cache" --runTestsByPath $( awk 'NR % 4 == 0' < ~/.jest-e2e-tests )
- name: E2E tests (Admin) (2/4)
- env: FORCE_REDUCED_MOTION=true PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=
+ env: FORCE_REDUCED_MOTION=true
script:
- $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --listTests > ~/.jest-e2e-tests
- $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --cacheDirectory="$HOME/.jest-cache" --runTestsByPath $( awk 'NR % 4 == 1' < ~/.jest-e2e-tests )
- name: E2E tests (Admin) (3/4)
- env: FORCE_REDUCED_MOTION=true PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=
+ env: FORCE_REDUCED_MOTION=true
script:
- $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --listTests > ~/.jest-e2e-tests
- $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --cacheDirectory="$HOME/.jest-cache" --runTestsByPath $( awk 'NR % 4 == 2' < ~/.jest-e2e-tests )
- name: E2E tests (Admin) (4/4)
- env: FORCE_REDUCED_MOTION=true PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=
+ env: FORCE_REDUCED_MOTION=true
script:
- $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --listTests > ~/.jest-e2e-tests
- $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --cacheDirectory="$HOME/.jest-cache" --runTestsByPath $( awk 'NR % 4 == 3' < ~/.jest-e2e-tests )
- name: E2E tests (Author) (1/4)
- env: E2E_ROLE=author FORCE_REDUCED_MOTION=true PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=
+ env: E2E_ROLE=author FORCE_REDUCED_MOTION=true
script:
- $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --listTests > ~/.jest-e2e-tests
- $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --cacheDirectory="$HOME/.jest-cache" --runTestsByPath $( awk 'NR % 4 == 0' < ~/.jest-e2e-tests )
- name: E2E tests (Author) (2/4)
- env: E2E_ROLE=author FORCE_REDUCED_MOTION=true PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=
+ env: E2E_ROLE=author FORCE_REDUCED_MOTION=true
script:
- $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --listTests > ~/.jest-e2e-tests
- $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --cacheDirectory="$HOME/.jest-cache" --runTestsByPath $( awk 'NR % 4 == 1' < ~/.jest-e2e-tests )
- name: E2E tests (Author) (3/4)
- env: E2E_ROLE=author FORCE_REDUCED_MOTION=true PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=
+ env: E2E_ROLE=author FORCE_REDUCED_MOTION=true
script:
- $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --listTests > ~/.jest-e2e-tests
- $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --cacheDirectory="$HOME/.jest-cache" --runTestsByPath $( awk 'NR % 4 == 2' < ~/.jest-e2e-tests )
- name: E2E tests (Author) (4/4)
- env: E2E_ROLE=author FORCE_REDUCED_MOTION=true PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=
+ env: E2E_ROLE=author FORCE_REDUCED_MOTION=true
script:
- $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --listTests > ~/.jest-e2e-tests
- $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --cacheDirectory="$HOME/.jest-cache" --runTestsByPath $( awk 'NR % 4 == 3' < ~/.jest-e2e-tests )
diff --git a/package-lock.json b/package-lock.json
index 0ef8da4b1ea83..e0b0534df68e0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10498,11 +10498,245 @@
"chalk": "^2.4.2",
"copy-dir": "^1.2.0",
"docker-compose": "^0.22.2",
+ "extract-zip": "^1.6.7",
+ "inquirer": "^7.0.4",
"js-yaml": "^3.13.1",
"nodegit": "^0.26.2",
"ora": "^4.0.2",
+ "request": "^2.88.2",
+ "request-progress": "^3.0.0",
+ "rimraf": "^3.0.2",
"terminal-link": "^2.0.0",
"yargs": "^14.0.0"
+ },
+ "dependencies": {
+ "ansi-escapes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz",
+ "integrity": "sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==",
+ "dev": true,
+ "requires": {
+ "type-fest": "^0.8.1"
+ }
+ },
+ "ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "dev": true
+ },
+ "cli-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "dev": true,
+ "requires": {
+ "restore-cursor": "^3.1.0"
+ }
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "figures": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
+ "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
+ "dev": true,
+ "requires": {
+ "escape-string-regexp": "^1.0.5"
+ }
+ },
+ "glob": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "inquirer": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz",
+ "integrity": "sha512-Bu5Td5+j11sCkqfqmUTiwv+tWisMtP0L7Q8WrqA2C/BbBhy1YTdFrvjjlrKq8oagA/tLQBski2Gcx/Sqyi2qSQ==",
+ "dev": true,
+ "requires": {
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^2.4.2",
+ "cli-cursor": "^3.1.0",
+ "cli-width": "^2.0.0",
+ "external-editor": "^3.0.3",
+ "figures": "^3.0.0",
+ "lodash": "^4.17.15",
+ "mute-stream": "0.0.8",
+ "run-async": "^2.2.0",
+ "rxjs": "^6.5.3",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^5.1.0",
+ "through": "^2.3.6"
+ }
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true
+ },
+ "mime-db": {
+ "version": "1.43.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz",
+ "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==",
+ "dev": true
+ },
+ "mime-types": {
+ "version": "2.1.26",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz",
+ "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==",
+ "dev": true,
+ "requires": {
+ "mime-db": "1.43.0"
+ }
+ },
+ "mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true
+ },
+ "mute-stream": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
+ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
+ "dev": true
+ },
+ "onetime": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz",
+ "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==",
+ "dev": true,
+ "requires": {
+ "mimic-fn": "^2.1.0"
+ }
+ },
+ "request": {
+ "version": "2.88.2",
+ "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
+ "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
+ "dev": true,
+ "requires": {
+ "aws-sign2": "~0.7.0",
+ "aws4": "^1.8.0",
+ "caseless": "~0.12.0",
+ "combined-stream": "~1.0.6",
+ "extend": "~3.0.2",
+ "forever-agent": "~0.6.1",
+ "form-data": "~2.3.2",
+ "har-validator": "~5.1.3",
+ "http-signature": "~1.2.0",
+ "is-typedarray": "~1.0.0",
+ "isstream": "~0.1.2",
+ "json-stringify-safe": "~5.0.1",
+ "mime-types": "~2.1.19",
+ "oauth-sign": "~0.9.0",
+ "performance-now": "^2.1.0",
+ "qs": "~6.5.2",
+ "safe-buffer": "^5.1.2",
+ "tough-cookie": "~2.5.0",
+ "tunnel-agent": "^0.6.0",
+ "uuid": "^3.3.2"
+ }
+ },
+ "restore-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+ "dev": true,
+ "requires": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "rxjs": {
+ "version": "6.5.4",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz",
+ "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.9.0"
+ }
+ },
+ "string-width": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+ "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "dependencies": {
+ "strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.0"
+ }
+ }
+ }
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true
+ }
+ }
+ },
+ "tough-cookie": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
+ "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
+ "dev": true,
+ "requires": {
+ "psl": "^1.1.28",
+ "punycode": "^2.1.1"
+ }
+ },
+ "type-fest": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+ "dev": true
+ }
}
},
"@wordpress/escape-html": {
@@ -34789,6 +35023,15 @@
}
}
},
+ "request-progress": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz",
+ "integrity": "sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=",
+ "dev": true,
+ "requires": {
+ "throttleit": "^1.0.0"
+ }
+ },
"request-promise-core": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz",
@@ -38292,6 +38535,12 @@
"integrity": "sha512-AOvyNahXQuU7NN+VVvOOX+uW6FPaWdAOdRP5HfwYxAfCzXTFKRMoIMk+n+po318+ktcChx+F1Dd91G3YHeMKyg==",
"dev": true
},
+ "throttleit": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz",
+ "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=",
+ "dev": true
+ },
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
diff --git a/packages/block-editor/src/components/editor-skeleton/index.js b/packages/block-editor/src/components/editor-skeleton/index.js
index 5b566f87221ff..ca4f3646f529c 100644
--- a/packages/block-editor/src/components/editor-skeleton/index.js
+++ b/packages/block-editor/src/components/editor-skeleton/index.js
@@ -13,7 +13,7 @@ import { __ } from '@wordpress/i18n';
function useHTMLClass( className ) {
useEffect( () => {
const element =
- document && document.querySelector( `html:not(.${ className }` );
+ document && document.querySelector( `html:not(.${ className })` );
if ( ! element ) {
return;
}
diff --git a/packages/block-editor/src/components/link-control/style.scss b/packages/block-editor/src/components/link-control/style.scss
index 959e1f30c1126..6c2f53643270c 100644
--- a/packages/block-editor/src/components/link-control/style.scss
+++ b/packages/block-editor/src/components/link-control/style.scss
@@ -135,6 +135,8 @@ $block-editor-link-control-number-of-actions: 1;
.block-editor-link-control__search-item-header {
display: block;
margin-right: $grid-size-xlarge;
+ overflow: hidden;
+ white-space: nowrap;
}
.block-editor-link-control__search-item-icon {
@@ -144,10 +146,10 @@ $block-editor-link-control-number-of-actions: 1;
.block-editor-link-control__search-item-info,
.block-editor-link-control__search-item-title {
- text-overflow: ellipsis;
max-width: 230px;
overflow: hidden;
white-space: nowrap;
+ text-overflow: ellipsis;
}
.block-editor-link-control__search-item-title {
diff --git a/packages/block-editor/src/components/typewriter/index.js b/packages/block-editor/src/components/typewriter/index.js
index b9b2e18ea6104..5b68cea5080c6 100644
--- a/packages/block-editor/src/components/typewriter/index.js
+++ b/packages/block-editor/src/components/typewriter/index.js
@@ -236,12 +236,6 @@ class Typewriter extends Component {
}
render() {
- // There are some issues with Internet Explorer, which are probably not
- // worth spending time on. Let's disable it.
- if ( isIE ) {
- return this.props.children;
- }
-
// Disable reason: Wrapper itself is non-interactive, but must capture
// bubbling events from children to determine focus transition intents.
/* eslint-disable jsx-a11y/no-static-element-interactions */
@@ -261,12 +255,23 @@ class Typewriter extends Component {
}
}
+/**
+ * The exported component. The implementation of Typewriter faced technical
+ * challenges in Internet Explorer, and is simply skipped, rendering the given
+ * props children instead.
+ *
+ * @type {WPComponent}
+ */
+const TypewriterOrIEBypass = isIE
+ ? ( props ) => props.children
+ : withSelect( ( select ) => {
+ const { getSelectedBlockClientId } = select( 'core/block-editor' );
+ return { selectedBlockClientId: getSelectedBlockClientId() };
+ } )( Typewriter );
+
/**
* Ensures that the text selection keeps the same vertical distance from the
* viewport during keyboard events within this component. The vertical distance
* can vary. It is the last clicked or scrolled to position.
*/
-export default withSelect( ( select ) => {
- const { getSelectedBlockClientId } = select( 'core/block-editor' );
- return { selectedBlockClientId: getSelectedBlockClientId() };
-} )( Typewriter );
+export default TypewriterOrIEBypass;
diff --git a/packages/block-library/src/buttons/editor.scss b/packages/block-library/src/buttons/editor.scss
index 19b0acb884b58..2f7aaf1572892 100644
--- a/packages/block-library/src/buttons/editor.scss
+++ b/packages/block-library/src/buttons/editor.scss
@@ -17,7 +17,7 @@
}
.block-list-appender {
- display: inline-block !important;
+ display: inline-block;
margin: 0;
}
diff --git a/packages/block-library/src/columns/style.scss b/packages/block-library/src/columns/style.scss
index 44ec7ae38bb85..4499ac92f1a4f 100644
--- a/packages/block-library/src/columns/style.scss
+++ b/packages/block-library/src/columns/style.scss
@@ -31,8 +31,11 @@
overflow-wrap: break-word; // New standard.
// Between mobile and large viewports, allow 2 columns.
- @include break-small() {
- flex-basis: calc(50% - #{$grid-size-large});
+ @media (min-width: #{ ($break-small) }) and (max-width: #{ ($break-medium - 1) }) {
+ // As with mobile styles, this must be important since the Column
+ // assigns its own width as an inline style, which should take effect
+ // starting at `break-medium`.
+ flex-basis: calc(50% - #{$grid-size-large}) !important;
flex-grow: 0;
// Add space between the multiple columns. Themes can customize this if they wish to work differently.
diff --git a/packages/block-library/src/latest-posts/index.php b/packages/block-library/src/latest-posts/index.php
index 86706bd2b23a3..6e96f10461231 100644
--- a/packages/block-library/src/latest-posts/index.php
+++ b/packages/block-library/src/latest-posts/index.php
@@ -5,6 +5,26 @@
* @package WordPress
*/
+/**
+ * The excerpt length set by the Latest Posts core block
+ * set at render time and used by the block itself.
+ *
+ * @var int
+ */
+$block_core_latest_posts_excerpt_length = 0;
+
+/**
+ * Callback for the excerpt_length filter used by
+ * the Latest Posts block at render time.
+ *
+ * @return int Returns the global $block_core_latest_posts_excerpt_length variable
+ * to allow the excerpt_length filter respect the Latest Block setting.
+ */
+function block_core_latest_posts_get_excerpt_length() {
+ global $block_core_latest_posts_excerpt_length;
+ return $block_core_latest_posts_excerpt_length;
+}
+
/**
* Renders the `core/latest-posts` block on server.
*
@@ -13,6 +33,8 @@
* @return string Returns the post content with latest posts added.
*/
function render_block_core_latest_posts( $attributes ) {
+ global $block_core_latest_posts_excerpt_length;
+
$args = array(
'posts_per_page' => $attributes['postsToShow'],
'post_status' => 'publish',
@@ -21,6 +43,9 @@ function render_block_core_latest_posts( $attributes ) {
'suppress_filters' => false,
);
+ $block_core_latest_posts_excerpt_length = $attributes['excerptLength'];
+ add_filter( 'excerpt_length', 'block_core_latest_posts_get_excerpt_length', 20 );
+
if ( isset( $attributes['categories'] ) ) {
$args['category'] = $attributes['categories'];
}
@@ -111,6 +136,8 @@ function render_block_core_latest_posts( $attributes ) {
$list_items_markup .= "\n";
}
+ remove_filter( 'excerpt_length', 'block_core_latest_posts_get_excerpt_length', 20 );
+
$class = 'wp-block-latest-posts wp-block-latest-posts__list';
if ( isset( $attributes['align'] ) ) {
$class .= ' align' . $attributes['align'];
diff --git a/packages/block-library/src/social-link/block.json b/packages/block-library/src/social-link/block.json
index b30cfc9ae44d5..977f5731fd961 100644
--- a/packages/block-library/src/social-link/block.json
+++ b/packages/block-library/src/social-link/block.json
@@ -10,7 +10,7 @@
"type": "string"
},
"label": {
- "type": "number"
+ "type": "string"
}
}
}
diff --git a/packages/block-library/src/social-link/index.php b/packages/block-library/src/social-link/index.php
index a414cb6c275ee..32111ff72db73 100644
--- a/packages/block-library/src/social-link/index.php
+++ b/packages/block-library/src/social-link/index.php
@@ -15,7 +15,10 @@
function render_block_core_social_link( $attributes ) {
$service = ( isset( $attributes['service'] ) ) ? $attributes['service'] : 'Icon';
$url = ( isset( $attributes['url'] ) ) ? $attributes['url'] : false;
- $label = ( isset( $attributes['label'] ) ) ? $attributes['label'] : __( 'Link to ' ) . block_core_social_link_get_name( $service );
+ $label = ( isset( $attributes['label'] ) ) ?
+ $attributes['label'] :
+ /* translators: %s: Social Link service name */
+ sprintf( __( 'Link to %s' ), block_core_social_link_get_name( $service ) );
// Don't render a link if there is no URL set.
if ( ! $url ) {
@@ -23,7 +26,7 @@ function render_block_core_social_link( $attributes ) {
}
$icon = block_core_social_link_get_icon( $service );
- return '
' . $icon . '';
+ return ' ' . $icon . '';
}
/**
@@ -90,163 +93,163 @@ function block_core_social_link_services( $service = '', $field = '' ) {
$services_data = array(
'fivehundredpx' => array(
'name' => '500px',
- 'icon' => '',
+ 'icon' => '',
),
'amazon' => array(
'name' => 'Amazon',
- 'icon' => '',
+ 'icon' => '',
),
'bandcamp' => array(
'name' => 'Bandcamp',
- 'icon' => '',
+ 'icon' => '',
),
'behance' => array(
'name' => 'Behance',
- 'icon' => '',
+ 'icon' => '',
),
'chain' => array(
'name' => 'Link',
- 'icon' => '',
+ 'icon' => '',
),
'codepen' => array(
'name' => 'CodePen',
- 'icon' => '',
+ 'icon' => '',
),
'deviantart' => array(
'name' => 'DeviantArt',
- 'icon' => '',
+ 'icon' => '',
),
'dribbble' => array(
'name' => 'Dribbble',
- 'icon' => '',
+ 'icon' => '',
),
'dropbox' => array(
'name' => 'Dropbox',
- 'icon' => '',
+ 'icon' => '',
),
'etsy' => array(
'name' => 'Etsy',
- 'icon' => '',
+ 'icon' => '',
),
'facebook' => array(
'name' => 'Facebook',
- 'icon' => '',
+ 'icon' => '',
),
'feed' => array(
'name' => 'RSS Feed',
- 'icon' => '',
+ 'icon' => '',
),
'flickr' => array(
'name' => 'Flickr',
- 'icon' => '',
+ 'icon' => '',
),
'foursquare' => array(
'name' => 'Foursquare',
- 'icon' => '',
+ 'icon' => '',
),
'goodreads' => array(
'name' => 'Goodreads',
- 'icon' => '',
+ 'icon' => '',
),
'google' => array(
'name' => 'Google',
- 'icon' => '',
+ 'icon' => '',
),
'github' => array(
'name' => 'GitHub',
- 'icon' => '',
+ 'icon' => '',
),
'instagram' => array(
'name' => 'Instagram',
- 'icon' => '',
+ 'icon' => '',
),
'lastfm' => array(
'name' => 'Last.fm',
- 'icon' => '',
+ 'icon' => '',
),
'linkedin' => array(
'name' => 'LinkedIn',
- 'icon' => '',
+ 'icon' => '',
),
'mail' => array(
'name' => 'Mail',
- 'icon' => '',
+ 'icon' => '',
),
'mastodon' => array(
'name' => 'Mastodon',
- 'icon' => '',
+ 'icon' => '',
),
'meetup' => array(
'name' => 'Meetup',
- 'icon' => '',
+ 'icon' => '',
),
'medium' => array(
'name' => 'Medium',
- 'icon' => '',
+ 'icon' => '',
),
'pinterest' => array(
'name' => 'Pinterest',
- 'icon' => '',
+ 'icon' => '',
),
'pocket' => array(
'name' => 'Pocket',
- 'icon' => '',
+ 'icon' => '',
),
'reddit' => array(
'name' => 'Reddit',
- 'icon' => '',
+ 'icon' => '',
),
'skype' => array(
'name' => 'Skype',
- 'icon' => '',
+ 'icon' => '',
),
'snapchat' => array(
'name' => 'Snapchat',
- 'icon' => '',
+ 'icon' => '',
),
'soundcloud' => array(
'name' => 'Soundcloud',
- 'icon' => '',
+ 'icon' => '',
),
'spotify' => array(
'name' => 'Spotify',
- 'icon' => '',
+ 'icon' => '',
),
'tumblr' => array(
'name' => 'Tumblr',
- 'icon' => '',
+ 'icon' => '',
),
'twitch' => array(
'name' => 'Twitch',
- 'icon' => '',
+ 'icon' => '',
),
'twitter' => array(
'name' => 'Twitter',
- 'icon' => '',
+ 'icon' => '',
),
'vimeo' => array(
'name' => 'Vimeo',
- 'icon' => '',
+ 'icon' => '',
),
'vk' => array(
'name' => 'VK',
- 'icon' => '',
+ 'icon' => '',
),
'wordpress' => array(
'name' => 'WordPress',
- 'icon' => '',
+ 'icon' => '',
),
'yelp' => array(
'name' => 'Yelp',
- 'icon' => '',
+ 'icon' => '',
),
'youtube' => array(
'name' => 'YouTube',
- 'icon' => '',
+ 'icon' => '',
),
'share' => array(
'name' => 'Share Icon',
- 'icon' => '',
+ 'icon' => '',
),
);
diff --git a/packages/e2e-tests/plugins/meta-attribute-block.php b/packages/e2e-tests/plugins/meta-attribute-block.php
index 58eec7c04244a..4513ebafde26a 100644
--- a/packages/e2e-tests/plugins/meta-attribute-block.php
+++ b/packages/e2e-tests/plugins/meta-attribute-block.php
@@ -29,13 +29,23 @@ function init_test_meta_attribute_block_plugin() {
*/
function enqueue_test_meta_attribute_block() {
wp_enqueue_script(
- 'gutenberg-test-meta-attribute-block',
- plugins_url( 'meta-attribute-block/index.js', __FILE__ ),
+ 'gutenberg-test-meta-attribute-block-early',
+ plugins_url( 'meta-attribute-block/early.js', __FILE__ ),
array(
'wp-blocks',
'wp-element',
),
- filemtime( plugin_dir_path( __FILE__ ) . 'meta-attribute-block/index.js' ),
+ filemtime( plugin_dir_path( __FILE__ ) . 'meta-attribute-block/early.js' )
+ );
+
+ wp_enqueue_script(
+ 'gutenberg-test-meta-attribute-block-late',
+ plugins_url( 'meta-attribute-block/late.js', __FILE__ ),
+ array(
+ 'wp-blocks',
+ 'wp-element',
+ ),
+ filemtime( plugin_dir_path( __FILE__ ) . 'meta-attribute-block/late.js' ),
true
);
}
diff --git a/packages/e2e-tests/plugins/meta-attribute-block/early.js b/packages/e2e-tests/plugins/meta-attribute-block/early.js
new file mode 100644
index 0000000000000..7c2831e84e4de
--- /dev/null
+++ b/packages/e2e-tests/plugins/meta-attribute-block/early.js
@@ -0,0 +1,32 @@
+( function() {
+ var registerBlockType = wp.blocks.registerBlockType;
+ var el = wp.element.createElement;
+
+ registerBlockType( 'test/test-meta-attribute-block-early', {
+ title: 'Test Meta Attribute Block (Early Registration)',
+ icon: 'star',
+ category: 'common',
+
+ attributes: {
+ content: {
+ type: 'string',
+ source: 'meta',
+ meta: 'my_meta',
+ },
+ },
+
+ edit: function( props ) {
+ return el( 'input', {
+ className: 'my-meta-input',
+ value: props.attributes.content,
+ onChange: function( event ) {
+ props.setAttributes( { content: event.target.value } );
+ },
+ } );
+ },
+
+ save: function() {
+ return null;
+ },
+ } );
+} )();
diff --git a/packages/e2e-tests/plugins/meta-attribute-block/index.js b/packages/e2e-tests/plugins/meta-attribute-block/index.js
deleted file mode 100644
index 0910e648299ae..0000000000000
--- a/packages/e2e-tests/plugins/meta-attribute-block/index.js
+++ /dev/null
@@ -1,35 +0,0 @@
-( function() {
- var registerBlockType = wp.blocks.registerBlockType;
- var el = wp.element.createElement;
-
- registerBlockType( 'test/test-meta-attribute-block', {
- title: 'Test Meta Attribute Block',
- icon: 'star',
- category: 'common',
-
- attributes: {
- content: {
- type: "string",
- source: "meta",
- meta: "my_meta",
- },
- },
-
- edit: function( props ) {
- return el(
- 'input',
- {
- className: 'my-meta-input',
- value: props.attributes.content,
- onChange: function( event ) {
- props.setAttributes( { content: event.target.value } );
- },
- }
- );
- },
-
- save: function() {
- return null;
- },
- } );
-} )();
diff --git a/packages/e2e-tests/plugins/meta-attribute-block/late.js b/packages/e2e-tests/plugins/meta-attribute-block/late.js
new file mode 100644
index 0000000000000..362acab0f776a
--- /dev/null
+++ b/packages/e2e-tests/plugins/meta-attribute-block/late.js
@@ -0,0 +1,32 @@
+( function() {
+ var registerBlockType = wp.blocks.registerBlockType;
+ var el = wp.element.createElement;
+
+ registerBlockType( 'test/test-meta-attribute-block-late', {
+ title: 'Test Meta Attribute Block (Late Registration)',
+ icon: 'star',
+ category: 'common',
+
+ attributes: {
+ content: {
+ type: 'string',
+ source: 'meta',
+ meta: 'my_meta',
+ },
+ },
+
+ edit: function( props ) {
+ return el( 'input', {
+ className: 'my-meta-input',
+ value: props.attributes.content,
+ onChange: function( event ) {
+ props.setAttributes( { content: event.target.value } );
+ },
+ } );
+ },
+
+ save: function() {
+ return null;
+ },
+ } );
+} )();
diff --git a/packages/e2e-tests/specs/editor/plugins/__snapshots__/meta-attribute-block.test.js.snap b/packages/e2e-tests/specs/editor/plugins/__snapshots__/meta-attribute-block.test.js.snap
index 1b5c130366402..268c8b45d059f 100644
--- a/packages/e2e-tests/specs/editor/plugins/__snapshots__/meta-attribute-block.test.js.snap
+++ b/packages/e2e-tests/specs/editor/plugins/__snapshots__/meta-attribute-block.test.js.snap
@@ -1,5 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Block with a meta attribute Should persist the meta attribute properly 1`] = `""`;
+exports[`Block with a meta attribute Early Registration Should persist the meta attribute properly 1`] = `""`;
-exports[`Block with a meta attribute Should persist the meta attribute properly in a different post type 1`] = `""`;
+exports[`Block with a meta attribute Early Registration Should persist the meta attribute properly in a different post type 1`] = `""`;
+
+exports[`Block with a meta attribute Late Registration Should persist the meta attribute properly 1`] = `""`;
+
+exports[`Block with a meta attribute Late Registration Should persist the meta attribute properly in a different post type 1`] = `""`;
diff --git a/packages/e2e-tests/specs/editor/plugins/meta-attribute-block.test.js b/packages/e2e-tests/specs/editor/plugins/meta-attribute-block.test.js
index 68f4de2768fbb..22569268de629 100644
--- a/packages/e2e-tests/specs/editor/plugins/meta-attribute-block.test.js
+++ b/packages/e2e-tests/specs/editor/plugins/meta-attribute-block.test.js
@@ -24,68 +24,75 @@ describe( 'Block with a meta attribute', () => {
await deactivatePlugin( 'gutenberg-test-meta-attribute-block' );
} );
- it( 'Should persist the meta attribute properly', async () => {
- await insertBlock( 'Test Meta Attribute Block' );
- await page.keyboard.type( 'Value' );
+ describe.each( [ [ 'Early Registration' ], [ 'Late Registration' ] ] )(
+ '%s',
+ ( variant ) => {
+ it( 'Should persist the meta attribute properly', async () => {
+ await insertBlock( `Test Meta Attribute Block (${ variant })` );
+ await page.keyboard.type( 'Value' );
- // Regression Test: Previously the caret would wrongly reset to the end
- // of any input for meta-sourced attributes, due to syncing behavior of
- // meta attribute updates.
- //
- // See: https://github.com/WordPress/gutenberg/issues/15739
- await pressKeyTimes( 'ArrowLeft', 5 );
- await page.keyboard.type( 'Meta ' );
+ // Regression Test: Previously the caret would wrongly reset to the end
+ // of any input for meta-sourced attributes, due to syncing behavior of
+ // meta attribute updates.
+ //
+ // See: https://github.com/WordPress/gutenberg/issues/15739
+ await pressKeyTimes( 'ArrowLeft', 5 );
+ await page.keyboard.type( 'Meta ' );
- await saveDraft();
- await page.reload();
+ await saveDraft();
+ await page.reload();
- expect( await getEditedPostContent() ).toMatchSnapshot();
- const persistedValue = await page.evaluate(
- () => document.querySelector( '.my-meta-input' ).value
- );
- expect( persistedValue ).toBe( 'Meta Value' );
- } );
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ const persistedValue = await page.evaluate(
+ () => document.querySelector( '.my-meta-input' ).value
+ );
+ expect( persistedValue ).toBe( 'Meta Value' );
+ } );
- it( 'Should use the same value in all the blocks', async () => {
- await insertBlock( 'Test Meta Attribute Block' );
- await insertBlock( 'Test Meta Attribute Block' );
- await insertBlock( 'Test Meta Attribute Block' );
- await page.keyboard.type( 'Meta Value' );
+ it( 'Should use the same value in all the blocks', async () => {
+ await insertBlock( `Test Meta Attribute Block (${ variant })` );
+ await insertBlock( `Test Meta Attribute Block (${ variant })` );
+ await insertBlock( `Test Meta Attribute Block (${ variant })` );
+ await page.keyboard.type( 'Meta Value' );
- const inputs = await page.$$( '.my-meta-input' );
- await Promise.all(
- inputs.map( async ( input ) => {
- // Clicking the input selects the block,
- // and selecting the block enables the sync data mode
- // as otherwise the asynchronous re-rendering of unselected blocks
- // may cause the input to have not yet been updated for the other blocks
- await input.click();
- const inputValue = await input.getProperty( 'value' );
- expect( await inputValue.jsonValue() ).toBe( 'Meta Value' );
- } )
- );
- } );
+ const inputs = await page.$$( '.my-meta-input' );
+ await Promise.all(
+ inputs.map( async ( input ) => {
+ // Clicking the input selects the block,
+ // and selecting the block enables the sync data mode
+ // as otherwise the asynchronous re-rendering of unselected blocks
+ // may cause the input to have not yet been updated for the other blocks
+ await input.click();
+ const inputValue = await input.getProperty( 'value' );
+ expect( await inputValue.jsonValue() ).toBe(
+ 'Meta Value'
+ );
+ } )
+ );
+ } );
- it( 'Should persist the meta attribute properly in a different post type', async () => {
- await createNewPost( { postType: 'page' } );
- await insertBlock( 'Test Meta Attribute Block' );
- await page.keyboard.type( 'Value' );
+ it( 'Should persist the meta attribute properly in a different post type', async () => {
+ await createNewPost( { postType: 'page' } );
+ await insertBlock( `Test Meta Attribute Block (${ variant })` );
+ await page.keyboard.type( 'Value' );
- // Regression Test: Previously the caret would wrongly reset to the end
- // of any input for meta-sourced attributes, due to syncing behavior of
- // meta attribute updates.
- //
- // See: https://github.com/WordPress/gutenberg/issues/15739
- await pressKeyTimes( 'ArrowLeft', 5 );
- await page.keyboard.type( 'Meta ' );
+ // Regression Test: Previously the caret would wrongly reset to the end
+ // of any input for meta-sourced attributes, due to syncing behavior of
+ // meta attribute updates.
+ //
+ // See: https://github.com/WordPress/gutenberg/issues/15739
+ await pressKeyTimes( 'ArrowLeft', 5 );
+ await page.keyboard.type( 'Meta ' );
- await saveDraft();
- await page.reload();
+ await saveDraft();
+ await page.reload();
- expect( await getEditedPostContent() ).toMatchSnapshot();
- const persistedValue = await page.evaluate(
- () => document.querySelector( '.my-meta-input' ).value
- );
- expect( persistedValue ).toBe( 'Meta Value' );
- } );
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ const persistedValue = await page.evaluate(
+ () => document.querySelector( '.my-meta-input' ).value
+ );
+ expect( persistedValue ).toBe( 'Meta Value' );
+ } );
+ }
+ );
} );
diff --git a/packages/e2e-tests/specs/editor/various/a11y.test.js b/packages/e2e-tests/specs/editor/various/a11y.test.js
index 8f16e6a7ac636..0ca44d7fb7114 100644
--- a/packages/e2e-tests/specs/editor/various/a11y.test.js
+++ b/packages/e2e-tests/specs/editor/various/a11y.test.js
@@ -23,7 +23,7 @@ describe( 'a11y', () => {
':focus',
( focusedElement ) => {
return focusedElement.classList.contains(
- 'editor-post-publish-button__button'
+ 'block-editor-inserter__toggle'
);
}
);
diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js
index a18f5a207c114..d1d434cbd861f 100644
--- a/packages/edit-post/src/components/header/index.js
+++ b/packages/edit-post/src/components/header/index.js
@@ -57,6 +57,10 @@ function Header() {
return (
+
+
+
+
{ ! isPublishSidebarOpened && (
// This button isn't completely hidden by the publish sidebar.
@@ -88,10 +92,6 @@ function Header() {
-
-
-
-
);
}
diff --git a/packages/edit-post/src/components/header/style.scss b/packages/edit-post/src/components/header/style.scss
index 3c06b4dc15d9f..b862dfa1388bc 100644
--- a/packages/edit-post/src/components/header/style.scss
+++ b/packages/edit-post/src/components/header/style.scss
@@ -4,7 +4,6 @@
background: $white;
display: flex;
flex-wrap: wrap;
- flex-direction: row-reverse;
justify-content: space-between;
align-items: center;
// The header should never be wider than the viewport, or buttons might be hidden. Especially relevant at high zoom levels. Related to https://core.trac.wordpress.org/ticket/47603#ticket.
diff --git a/packages/editor/src/hooks/custom-sources-backwards-compatibility.js b/packages/editor/src/hooks/custom-sources-backwards-compatibility.js
index 8c91cd625da62..51d5bf4ac5fda 100644
--- a/packages/editor/src/hooks/custom-sources-backwards-compatibility.js
+++ b/packages/editor/src/hooks/custom-sources-backwards-compatibility.js
@@ -6,7 +6,7 @@ import { pickBy, mapValues, isEmpty, mapKeys } from 'lodash';
/**
* WordPress dependencies
*/
-import { useSelect } from '@wordpress/data';
+import { select as globalSelect, useSelect } from '@wordpress/data';
import { useEntityProp } from '@wordpress/core-data';
import { useMemo } from '@wordpress/element';
import { createHigherOrderComponent } from '@wordpress/compose';
@@ -116,3 +116,26 @@ addFilter(
'core/editor/custom-sources-backwards-compatibility/shim-attribute-source',
shimAttributeSource
);
+
+// The above filter will only capture blocks registered after the filter was
+// added. There may already be blocks registered by this point, and those must
+// be updated to apply the shim.
+//
+// The following implementation achieves this, albeit with a couple caveats:
+// - Only blocks registered on the global store will be modified.
+// - The block settings are directly mutated, since there is currently no
+// mechanism to update an existing block registration. This is the reason for
+// `getBlockType` separate from `getBlockTypes`, since the latter returns a
+// _copy_ of the block registration (i.e. the mutation would not affect the
+// actual registered block settings).
+//
+// `getBlockTypes` or `getBlockType` implementation could change in the future
+// in regards to creating settings clones, but the corresponding end-to-end
+// tests for meta blocks should cover against any potential regressions.
+//
+// In the future, we could support updating block settings, at which point this
+// implementation could use that mechanism instead.
+globalSelect( 'core/blocks' )
+ .getBlockTypes()
+ .map( ( { name } ) => globalSelect( 'core/blocks' ).getBlockType( name ) )
+ .forEach( shimAttributeSource );
diff --git a/packages/env/CHANGELOG.md b/packages/env/CHANGELOG.md
index 9400e9a113faa..774afe9e90411 100644
--- a/packages/env/CHANGELOG.md
+++ b/packages/env/CHANGELOG.md
@@ -1,18 +1,26 @@
## Master
+### New Feature
+
+- URLs for ZIP files are now supported as core, plugin, and theme sources.
+- The `.wp-env.json` coniguration file now accepts a `config` object for setting `wp-config.php` values.
+- A `.wp-env.override.json` configuration file can now be used to override fields from `.wp-env.json`.
+- You may now override the directory in which `wp-env` creates generated files with the `WP_ENV_HOME` environment variable. The default directory is `~/.wp-env/` (or `~/wp-env/` on Linux).
+- The `.wp-env.json` coniguration file now accepts `port` and `testsPort` options which can be used to set the ports on which the docker instance is mounted.
+
## 1.0.0 (2020-02-10)
### 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.
+- `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.
+- 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
-- When running scripts using `wp-env run`, the output will not be formatted if not written to terminal display, resolving an issue where piped or redirected output could be unintentionally padded with newlines.
+- When running scripts using `wp-env run`, the output will not be formatted if not written to terminal display, resolving an issue where piped or redirected output could be unintentionally padded with newlines.
diff --git a/packages/env/README.md b/packages/env/README.md
index 9e4ac9f7b4387..dadb8e406a20f 100644
--- a/packages/env/README.md
+++ b/packages/env/README.md
@@ -79,6 +79,8 @@ $ WP_ENV_PORT=3333 wp-env start
Running `docker ps` and inspecting the `PORTS` column allows you to determine which port `wp-env` is currently using.
+You may also specify the port numbers in your `.wp-env.json` file, but the environment variables take precedent.
+
### 3. Restart `wp-env`
Restarting `wp-env` will restart the underlying Docker containers which can fix many issues.
@@ -135,6 +137,8 @@ $ wp-env start
## Command reference
+`wp-env` creates generated files in the `wp-env` home directory. By default, this is `~/.wp-env`. The exception is Linux, where files are placed at `~/wp-env` [for compatibility with Snap Packages](https://github.com/WordPress/gutenberg/issues/20180#issuecomment-587046325). The `wp-env` home directory contains a subdirectory for each project named `/$md5_of_project_path`. To change the `wp-env` home directory, set the `WP_ENV_HOME` environment variable. For example, running `WP_ENV_HOME="something" wp-env start` will download the project files to the directory `./something/$md5_of_project_path` (relative to the current directory).
+
### `wp-env start [ref]`
```sh
@@ -175,24 +179,34 @@ Positionals:
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:
+`.wp-env.json` supports five 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. |
+| `"port"` | `string` | `"8888"` | The primary port number to use for the insallation. You'll access the instance through the port: 'http://localhost:8888'. |
+| `"testsPort"` | `string` | `"8889"` | The port number to use for the tests instance. |
+| `"config"` | `Object` | `"{ WP_DEBUG: true, SCRIPT_DEBUG: true }"` | Mapping of wp-config.php constants to their desired values. |
-| 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. |
+_Note: the port number environment variables (`WP_ENV_PORT` and `WP_ENV_TESTS_PORT`) take precedent over the .wp-env.json values._
-Several types of strings can be passed into these fields:
+Several types of strings can be passed into the `core`, `plugins`, and `themes` fields:
-| 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"` |
+| 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"` |
+| ZIP File | `http[s]:///.zip` | `"https://wordpress.org/wordpress-5.4-beta2.zip"` |
Remote sources will be downloaded into a temporary directory located in `~/.wp-env`.
+## .wp-env.override.json
+
+Any fields here will take precedence over .wp-env.json. This file is useful, when ignored from version control, to persist local development overrides.
+
### Examples
#### Latest production WordPress + current directory as a plugin
@@ -201,10 +215,8 @@ This is useful for plugin development.
```json
{
- "core": null,
- "plugins": [
- "."
- ]
+ "core": null,
+ "plugins": [ "." ]
}
```
@@ -214,10 +226,8 @@ This is useful for plugin development when upstream Core changes need to be test
```json
{
- "core": "WordPress/WordPress#master",
- "plugins": [
- "."
- ]
+ "core": "WordPress/WordPress#master",
+ "plugins": [ "." ]
}
```
@@ -227,10 +237,8 @@ This is useful for working on plugins and WordPress Core at the same time.
```json
{
- "core": "../wordpress-develop/build",
- "plugins": [
- "."
- ]
+ "core": "../wordpress-develop/build",
+ "plugins": [ "." ]
}
```
@@ -240,14 +248,21 @@ This is useful for integration testing: that is, testing how old versions of Wor
```json
{
- "core": "WordPress/WordPress#5.2.0",
- "plugins": [
- "WordPress/wp-lazy-loading",
- "WordPress/classic-editor",
- ],
- "themes": [
- "WordPress/theme-experiments"
- ]
+ "core": "WordPress/WordPress#5.2.0",
+ "plugins": [ "WordPress/wp-lazy-loading", "WordPress/classic-editor" ],
+ "themes": [ "WordPress/theme-experiments" ]
+}
+```
+
+#### Custom Port Numbers
+
+You can tell `wp-env` to use a custom port number so that your instance does not conflict with other `wp-env` instances.
+
+```json
+{
+ "plugins": [ "." ],
+ "port": 4013,
+ "testsPort": 4012
}
```
diff --git a/packages/env/lib/build-docker-compose-config.js b/packages/env/lib/build-docker-compose-config.js
index fae4a6d743e1b..36d97bace63de 100644
--- a/packages/env/lib/build-docker-compose-config.js
+++ b/packages/env/lib/build-docker-compose-config.js
@@ -51,6 +51,16 @@ module.exports = function buildDockerComposeConfig( config ) {
...themeMounts,
];
+ // Set the default ports based on the config values.
+ const developmentPorts = `\${WP_ENV_PORT:-${ config.port }}:80`;
+ const testsPorts = `\${WP_ENV_TESTS_PORT:-${ config.testsPort }}:80`;
+
+ // The www-data user in wordpress:cli has a different UID (82) to the
+ // www-data user in wordpress (33). Ensure we use the wordpress www-data
+ // user for CLI commands.
+ // https://github.com/docker-library/wordpress/issues/256
+ const cliUser = '33:33';
+
return {
version: '3.7',
services: {
@@ -63,9 +73,8 @@ module.exports = function buildDockerComposeConfig( config ) {
wordpress: {
depends_on: [ 'mysql' ],
image: 'wordpress',
- ports: [ '${WP_ENV_PORT:-8888}:80' ],
+ ports: [ developmentPorts ],
environment: {
- WORDPRESS_DEBUG: '1',
WORDPRESS_DB_NAME: 'wordpress',
},
volumes: developmentMounts,
@@ -73,9 +82,8 @@ module.exports = function buildDockerComposeConfig( config ) {
'tests-wordpress': {
depends_on: [ 'mysql' ],
image: 'wordpress',
- ports: [ '${WP_ENV_TESTS_PORT:-8889}:80' ],
+ ports: [ testsPorts ],
environment: {
- WORDPRESS_DEBUG: '1',
WORDPRESS_DB_NAME: 'tests-wordpress',
},
volumes: testsMounts,
@@ -84,11 +92,13 @@ module.exports = function buildDockerComposeConfig( config ) {
depends_on: [ 'wordpress' ],
image: 'wordpress:cli',
volumes: developmentMounts,
+ user: cliUser,
},
'tests-cli': {
depends_on: [ 'wordpress' ],
image: 'wordpress:cli',
volumes: testsMounts,
+ user: cliUser,
},
composer: {
image: 'composer',
diff --git a/packages/env/lib/cli.js b/packages/env/lib/cli.js
index b589e9cce5ecf..2ddc209e376f5 100644
--- a/packages/env/lib/cli.js
+++ b/packages/env/lib/cli.js
@@ -32,20 +32,47 @@ const withSpinner = ( command ) => ( ...args ) => {
time[ 1 ] / 1e6
).toFixed( 0 ) }ms)`
);
+ process.exit( 0 );
},
( error ) => {
- spinner.fail( error.message || error.err );
- if ( ! ( error instanceof env.ValidationError ) ) {
+ if ( error instanceof env.ValidationError ) {
+ // Error is a validation error. That means the user did something wrong.
+ spinner.fail( error.message );
+ process.exit( 1 );
+ } else if (
+ 'exitCode' in error &&
+ 'err' in error &&
+ 'out' in error
+ ) {
+ // Error is a docker-compose error. That means something docker-related failed.
+ // https://github.com/PDMLab/docker-compose/blob/master/src/index.ts
+ spinner.fail( 'Error while running docker-compose command.' );
+ if ( error.out ) {
+ process.stdout.write( error.out );
+ }
+ if ( error.err ) {
+ process.stderr.write( error.err );
+ }
+ process.exit( error.exitCode );
+ } else {
+ // Error is an unknown error. That means there was a bug in our code.
+ spinner.fail( error.message );
+ // Disable reason: Using console.error() means we get a stack trace.
// eslint-disable-next-line no-console
- console.error( `\n\n${ error.out || error.err }\n\n` );
+ console.error( error );
+ process.exit( 1 );
}
- process.exit( error.exitCode || 1 );
}
);
};
module.exports = function cli() {
yargs.usage( wpPrimary( '$0 ' ) );
+ yargs.option( 'debug', {
+ type: 'boolean',
+ describe: 'Enable debug output.',
+ default: false,
+ } );
yargs.command(
'start',
diff --git a/packages/env/lib/config.js b/packages/env/lib/config.js
index 817b71b9540dc..31e0d4bbb3715 100644
--- a/packages/env/lib/config.js
+++ b/packages/env/lib/config.js
@@ -28,13 +28,17 @@ 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.
+ * @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.
+ * @property {number} port The port on which to start the development WordPress environment.
+ * @property {number} testsPort The port on which to start the testing WordPress environment.
+ * @property {Object} config Mapping of wp-config.php constants to their desired values.
+ * @property {boolean} debug True if debug mode is enabled.
*/
/**
@@ -59,6 +63,7 @@ module.exports = {
const configDirectoryPath = path.dirname( configPath );
let config = null;
+ let overrideConfig = {};
try {
config = JSON.parse( await fs.readFile( configPath, 'utf8' ) );
@@ -76,6 +81,30 @@ module.exports = {
}
}
+ try {
+ overrideConfig = JSON.parse(
+ await fs.readFile(
+ configPath.replace(
+ /\.wp-env\.json$/,
+ '.wp-env.override.json'
+ ),
+ 'utf8'
+ )
+ );
+ } catch ( error ) {
+ if ( error.code === 'ENOENT' ) {
+ // Config override file does not exist. Do nothing - it's optional.
+ } else if ( error instanceof SyntaxError ) {
+ throw new ValidationError(
+ `Invalid .wp-env.override.json: ${ error.message }`
+ );
+ } else {
+ throw new ValidationError(
+ `Could not read .wp-env.override.json: ${ error.message }`
+ );
+ }
+ }
+
if ( config === null ) {
const type = await detectDirectoryType( configDirectoryPath );
if ( type === 'core' ) {
@@ -96,10 +125,18 @@ module.exports = {
core: null,
plugins: [],
themes: [],
+ port: 8888,
+ testsPort: 8889,
+ config: { WP_DEBUG: true, SCRIPT_DEBUG: true },
},
- config
+ config,
+ overrideConfig
);
+ config.port = getNumberFromEnvVariable( 'WP_ENV_PORT' ) || config.port;
+ config.testsPort =
+ getNumberFromEnvVariable( 'WP_ENV_TESTS_PORT' ) || config.testsPort;
+
if ( config.core !== null && typeof config.core !== 'string' ) {
throw new ValidationError(
'Invalid .wp-env.json: "core" must be null or a string.'
@@ -124,9 +161,32 @@ module.exports = {
);
}
+ if ( ! Number.isInteger( config.port ) ) {
+ throw new ValidationError(
+ 'Invalid .wp-env.json: "port" must be an integer.'
+ );
+ }
+
+ if ( ! Number.isInteger( config.testsPort ) ) {
+ throw new ValidationError(
+ 'Invalid .wp-env.json: "testsPort" must be an integer.'
+ );
+ }
+
+ if ( config.port === config.testsPort ) {
+ throw new ValidationError(
+ 'Invalid .wp-env.json: "testsPort" and "port" must be different.'
+ );
+ }
+
+ if ( typeof config.config !== 'object' ) {
+ throw new ValidationError(
+ 'Invalid .wp-env.json: "config" must be an object.'
+ );
+ }
+
const workDirectoryPath = path.resolve(
- os.homedir(),
- '.wp-env',
+ getHomeDirectory(),
md5( configPath )
);
@@ -134,6 +194,8 @@ module.exports = {
name: path.basename( configDirectoryPath ),
configDirectoryPath,
workDirectoryPath,
+ port: config.port,
+ testsPort: config.testsPort,
dockerComposeConfigPath: path.resolve(
workDirectoryPath,
'docker-compose.yml'
@@ -141,7 +203,8 @@ module.exports = {
coreSource: includeTestsPath(
parseSourceString( config.core, {
workDirectoryPath,
- } )
+ } ),
+ { workDirectoryPath }
),
pluginSources: config.plugins.map( ( sourceString ) =>
parseSourceString( sourceString, {
@@ -153,6 +216,7 @@ module.exports = {
workDirectoryPath,
} )
),
+ config: config.config,
};
},
};
@@ -193,6 +257,21 @@ function parseSourceString( sourceString, { workDirectoryPath } ) {
};
}
+ const zipFields = sourceString.match(
+ /^https?:\/\/([^\s$.?#].[^\s]*)\.zip$/
+ );
+ if ( zipFields ) {
+ return {
+ type: 'zip',
+ url: sourceString,
+ path: path.resolve(
+ workDirectoryPath,
+ encodeURIComponent( zipFields[ 1 ] )
+ ),
+ basename: encodeURIComponent( zipFields[ 1 ] ),
+ };
+ }
+
const gitHubFields = sourceString.match( /^([^\/]+)\/([^#]+)(?:#(.+))?$/ );
if ( gitHubFields ) {
return {
@@ -214,9 +293,11 @@ function parseSourceString( sourceString, { workDirectoryPath } ) {
* property set correctly. Only the 'core' source requires a testsPath.
*
* @param {Source|null} source A source object.
+ * @param {Object} options
+ * @param {string} options.workDirectoryPath Path to the work directory located in ~/.wp-env.
* @return {Source|null} A source object.
*/
-function includeTestsPath( source ) {
+function includeTestsPath( source, { workDirectoryPath } ) {
if ( source === null ) {
return null;
}
@@ -224,13 +305,65 @@ function includeTestsPath( source ) {
return {
...source,
testsPath: path.resolve(
- source.path,
- '..',
+ workDirectoryPath,
'tests-' + path.basename( source.path )
),
};
}
+/**
+ * Parses an environment variable which should be a number.
+ *
+ * Throws an error if the variable cannot be parsed to a number.
+ * Returns null if the environment variable has not been specified.
+ *
+ * @param {string} varName The environment variable to check (e.g. WP_ENV_PORT).
+ * @return {null|number} The number. Null if it does not exist.
+ */
+function getNumberFromEnvVariable( varName ) {
+ // Allow use of the default if it does not exist.
+ if ( ! process.env[ varName ] ) {
+ return null;
+ }
+
+ const maybeNumber = parseInt( process.env[ varName ] );
+
+ // Throw an error if it is not parseable as a number.
+ if ( isNaN( maybeNumber ) ) {
+ throw new ValidationError(
+ `Invalid environment variable: ${ varName } must be a number.`
+ );
+ }
+
+ return maybeNumber;
+}
+
+/**
+ * Gets the `wp-env` home directory in which generated files are created.
+ *
+ * By default, '~/.wp-env/'. On Linux, '~/wp-env/'. Can be overriden with the
+ * WP_ENV_HOME environment variable.
+ *
+ * @return {string} The absolute path to the `wp-env` home directory.
+ */
+function getHomeDirectory() {
+ // Allow user to override download location.
+ if ( process.env.WP_ENV_HOME ) {
+ return path.resolve( process.env.WP_ENV_HOME );
+ }
+
+ /**
+ * Installing docker with Snap Packages on Linux is common, but does not
+ * support hidden directories. Therefore we use a public directory on Linux.
+ *
+ * @see https://github.com/WordPress/gutenberg/issues/20180#issuecomment-587046325
+ */
+ return path.resolve(
+ os.homedir(),
+ os.platform() === 'linux' ? 'wp-env' : '.wp-env'
+ );
+}
+
/**
* Hashes the given string using the MD5 algorithm.
*
diff --git a/packages/env/lib/download-source.js b/packages/env/lib/download-source.js
index 507db9db5aa69..74bf5f1c4fb56 100644
--- a/packages/env/lib/download-source.js
+++ b/packages/env/lib/download-source.js
@@ -2,7 +2,20 @@
/**
* External dependencies
*/
+const util = require( 'util' );
const NodeGit = require( 'nodegit' );
+const fs = require( 'fs' );
+const requestProgress = require( 'request-progress' );
+const request = require( 'request' );
+const path = require( 'path' );
+
+/**
+ * Promisified dependencies
+ */
+const finished = util.promisify( require( 'stream' ).finished );
+const extractZip = util.promisify( require( 'extract-zip' ) );
+const rimraf = util.promisify( require( 'rimraf' ) );
+const copyDir = util.promisify( require( 'copy-dir' ) );
/**
* @typedef {import('./config').Source} Source
@@ -12,13 +25,17 @@ const NodeGit = require( 'nodegit' );
* 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 {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.
+ * @param {Object} options.spinner A CLI spinner which indicates progress.
+ * @param {boolean} options.debug True if debug mode is enabled.
*/
module.exports = async function downloadSource( source, options ) {
if ( source.type === 'git' ) {
await downloadGitSource( source, options );
+ } else if ( source.type === 'zip' ) {
+ await downloadZipSource( source, options );
}
};
@@ -26,11 +43,19 @@ module.exports = async function downloadSource( 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 {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.
+ * @param {Object} options.spinner A CLI spinner which indicates progress.
+ * @param {boolean} options.debug True if debug mode is enabled.
*/
-async function downloadGitSource( source, { onProgress } ) {
+async function downloadGitSource( source, { onProgress, spinner, debug } ) {
+ const log = debug
+ ? ( message ) => {
+ spinner.info( `NodeGit: ${ message }` );
+ spinner.start();
+ }
+ : () => {};
onProgress( 0 );
const gitFetchOptions = {
@@ -50,20 +75,22 @@ async function downloadGitSource( source, { onProgress } ) {
},
};
- // Clone or get the repo.
+ log( 'Cloning or getting the repo.' );
const repository = await NodeGit.Clone(
source.url,
source.path,
gitFetchOptions
- )
- // Repo already exists, get it.
- .catch( () => NodeGit.Repository.open( source.path ) );
+ ).catch( () => {
+ log( 'Repo already exists, get it.' );
+ return NodeGit.Repository.open( source.path );
+ } );
- // Checkout the specified ref.
+ log( 'Fetching the specified ref.' );
const remote = await repository.getRemote( 'origin' );
await remote.fetch( source.ref, gitFetchOptions.fetchOpts );
await remote.disconnect();
try {
+ log( 'Checking out the specified ref.' );
await repository.checkoutRef(
await repository
.getReference( 'FETCH_HEAD' )
@@ -77,9 +104,52 @@ async function downloadGitSource( source, { onProgress } ) {
}
);
} catch ( error ) {
- // Some commit refs need to be set as detached.
+ log( 'Ref needs to be set as detached.' );
await repository.setHeadDetached( source.ref );
}
onProgress( 1 );
}
+
+/**
+ * Downloads and extracts the zip file at `source.url` into `source.path`.
+ *
+ * @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.
+ * @param {Object} options.spinner A CLI spinner which indicates progress.
+ * @param {boolean} options.debug True if debug mode is enabled.
+ */
+async function downloadZipSource( source, { onProgress, spinner, debug } ) {
+ const log = debug
+ ? ( message ) => {
+ spinner.info( `NodeGit: ${ message }` );
+ spinner.start();
+ }
+ : () => {};
+ onProgress( 0 );
+
+ log( 'Downloading zip file.' );
+ const zipName = `${ source.path }.zip`;
+ const zipFile = fs.createWriteStream( zipName );
+ await finished(
+ requestProgress( request( source.url ) )
+ .on( 'progress', ( { percent } ) => onProgress( percent ) )
+ .pipe( zipFile )
+ );
+
+ log( 'Extracting to temporary folder.' );
+ const dirName = `${ source.path }.temp`;
+ await extractZip( zipName, { dir: dirName } );
+
+ log( 'Copying to mounted folder and cleaning up.' );
+ await Promise.all( [
+ rimraf( zipName ),
+ ...( await fs.promises.readdir( dirName ) ).map( ( file ) =>
+ copyDir( path.join( dirName, file ), source.path )
+ ),
+ ] );
+ await rimraf( dirName );
+
+ onProgress( 1 );
+}
diff --git a/packages/env/lib/env.js b/packages/env/lib/env.js
index 94421544d2ff9..02070c2b07672 100644
--- a/packages/env/lib/env.js
+++ b/packages/env/lib/env.js
@@ -7,12 +7,14 @@ const path = require( 'path' );
const fs = require( 'fs' ).promises;
const dockerCompose = require( 'docker-compose' );
const yaml = require( 'js-yaml' );
+const inquirer = require( 'inquirer' );
/**
* Promisified dependencies
*/
const copyDir = util.promisify( require( 'copy-dir' ) );
const sleep = util.promisify( setTimeout );
+const rimraf = util.promisify( require( 'rimraf' ) );
/**
* Internal dependencies
@@ -29,11 +31,27 @@ module.exports = {
/**
* Starts the development server.
*
- * @param {Object} options
- * @param {Object} options.spinner A CLI spinner which indicates progress.
+ * @param {Object} options
+ * @param {Object} options.spinner A CLI spinner which indicates progress.
+ * @param {boolean} options.debug True if debug mode is enabled.
*/
- async start( { spinner } ) {
- const config = await initConfig();
+ async start( { spinner, debug } ) {
+ /**
+ * If the Docker image is already running and the `wp-env` files have been
+ * deleted, the start command will not complete successfully. Stopping
+ * the container before continuing allows the docker entrypoint script,
+ * which restores the files, to run again when we start the containers.
+ *
+ * Additionally, this serves as a way to restart the container entirely
+ * should the need arise.
+ *
+ * @see https://github.com/WordPress/gutenberg/pull/20253#issuecomment-587228440
+ */
+ await module.exports.stop( { spinner, debug } );
+
+ await checkForLegacyInstall( spinner );
+
+ const config = await initConfig( { spinner, debug } );
spinner.text = 'Downloading WordPress.';
@@ -56,12 +74,15 @@ module.exports = {
// Preemptively start the database while we wait for sources to download.
dockerCompose.upOne( 'mysql', {
config: config.dockerComposeConfigPath,
+ log: config.debug,
} ),
( async () => {
if ( config.coreSource ) {
await downloadSource( config.coreSource, {
onProgress: getProgressSetter( 'core' ),
+ spinner,
+ debug: config.debug,
} );
await copyCoreFiles(
config.coreSource.path,
@@ -73,12 +94,16 @@ module.exports = {
...config.pluginSources.map( ( source ) =>
downloadSource( source, {
onProgress: getProgressSetter( source.basename ),
+ spinner,
+ debug: config.debug,
} )
),
...config.themeSources.map( ( source ) =>
downloadSource( source, {
onProgress: getProgressSetter( source.basename ),
+ spinner,
+ debug: config.debug,
} )
),
] );
@@ -87,8 +112,17 @@ module.exports = {
await dockerCompose.upMany( [ 'wordpress', 'tests-wordpress' ], {
config: config.dockerComposeConfigPath,
+ log: config.debug,
} );
+ if ( config.coreSource === null ) {
+ // Don't chown wp-content when it exists on the user's local filesystem.
+ await Promise.all( [
+ makeContentDirectoriesWritable( 'development', config ),
+ makeContentDirectoriesWritable( 'tests', config ),
+ ] );
+ }
+
try {
await checkDatabaseConnection( config );
} catch ( error ) {
@@ -116,15 +150,22 @@ module.exports = {
/**
* Stops the development server.
*
- * @param {Object} options
- * @param {Object} options.spinner A CLI spinner which indicates progress.
+ * @param {Object} options
+ * @param {Object} options.spinner A CLI spinner which indicates progress.
+ * @param {boolean} options.debug True if debug mode is enabled.
*/
- async stop( { spinner } ) {
- const { dockerComposeConfigPath } = await initConfig();
+ async stop( { spinner, debug } ) {
+ const { dockerComposeConfigPath } = await initConfig( {
+ spinner,
+ debug,
+ } );
spinner.text = 'Stopping WordPress.';
- await dockerCompose.down( { config: dockerComposeConfigPath } );
+ await dockerCompose.down( {
+ config: dockerComposeConfigPath,
+ log: debug,
+ } );
spinner.text = 'Stopped WordPress.';
},
@@ -132,12 +173,13 @@ module.exports = {
/**
* 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.
+ * @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.
+ * @param {boolean} options.debug True if debug mode is enabled.
*/
- async clean( { environment, spinner } ) {
- const config = await initConfig();
+ async clean( { environment, spinner, debug } ) {
+ const config = await initConfig( { spinner, debug } );
const description = `${ environment } environment${
environment === 'all' ? 's' : ''
@@ -170,13 +212,14 @@ module.exports = {
/**
* 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.
+ * @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.
+ * @param {boolean} options.debug True if debug mode is enabled.
*/
- async run( { container, command, spinner } ) {
- const config = await initConfig();
+ async run( { container, command, spinner, debug } ) {
+ const config = await initConfig( { spinner, debug } );
command = command.join( ' ' );
@@ -185,6 +228,7 @@ module.exports = {
const result = await dockerCompose.run( container, command, {
config: config.dockerComposeConfigPath,
commandOptions: [ '--rm' ],
+ log: config.debug,
} );
if ( result.out ) {
@@ -206,23 +250,90 @@ module.exports = {
ValidationError,
};
+/**
+ * Checks for legacy installs and provides
+ * the user the option to delete them.
+ *
+ * @param {Object} spinner A CLI spinner which indicates progress.
+ */
+async function checkForLegacyInstall( spinner ) {
+ const basename = path.basename( process.cwd() );
+ const installs = [
+ `../${ basename }-wordpress`,
+ `../${ basename }-tests-wordpress`,
+ ];
+ await Promise.all(
+ installs.map( ( install ) =>
+ fs
+ .access( install )
+ .catch( () =>
+ installs.splice( installs.indexOf( install ), 1 )
+ )
+ )
+ );
+ if ( ! installs.length ) {
+ return;
+ }
+
+ spinner.info(
+ `It appears that you have used a previous version of this tool where installs were kept in ${ installs.join(
+ ' and '
+ ) }. Installs are now in your home folder.\n`
+ );
+ const { yesDelete } = await inquirer.prompt( [
+ {
+ type: 'confirm',
+ name: 'yesDelete',
+ message:
+ 'Do you wish to delete these old installs to reclaim disk space?',
+ default: true,
+ },
+ ] );
+ if ( yesDelete ) {
+ await Promise.all( installs.map( ( install ) => rimraf( install ) ) );
+ spinner.info( 'Old installs deleted successfully.' );
+ }
+ spinner.start();
+}
+
/**
* 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.
*
+ * @param {Object} options
+ * @param {Object} options.spinner A CLI spinner which indicates progress.
+ * @param {boolean} options.debug True if debug mode is enabled.
+ *
* @return {Config} The-env config object.
*/
-async function initConfig() {
+async function initConfig( { spinner, debug } ) {
const configPath = path.resolve( '.wp-env.json' );
const config = await readConfig( configPath );
+ config.debug = debug;
await fs.mkdir( config.workDirectoryPath, { recursive: true } );
+ const dockerComposeConfig = buildDockerComposeConfig( config );
await fs.writeFile(
config.dockerComposeConfigPath,
- yaml.dump( buildDockerComposeConfig( config ) )
+ yaml.dump( dockerComposeConfig )
);
+ if ( config.debug ) {
+ spinner.info(
+ `Config:\n${ JSON.stringify(
+ config,
+ null,
+ 4
+ ) }\n\nDocker Compose Config:\n${ JSON.stringify(
+ dockerComposeConfig,
+ null,
+ 4
+ ) }`
+ );
+ spinner.start();
+ }
+
return config;
}
@@ -253,6 +364,35 @@ async function copyCoreFiles( fromPath, toPath ) {
} );
}
+/**
+ * Makes the WordPress content directories (wp-content, wp-content/plugins,
+ * wp-content/themes) owned by the www-data user. This ensures that WordPress
+ * can write to these directories.
+ *
+ * This is necessary when running wp-env with `"core": null` because Docker
+ * will automatically create these directories as the root user when binding
+ * volumes during `docker-compose up`, and `docker-compose up` doesn't support
+ * the `-u` option.
+ *
+ * See https://github.com/docker-library/wordpress/issues/436.
+ *
+ * @param {string} environment The environment to check. Either 'development' or 'tests'.
+ * @param {Config} config The wp-env config object.
+ */
+async function makeContentDirectoriesWritable(
+ environment,
+ { dockerComposeConfigPath, debug }
+) {
+ await dockerCompose.exec(
+ environment === 'development' ? 'wordpress' : 'tests-wordpress',
+ 'chown www-data:www-data wp-content wp-content/plugins wp-content/themes',
+ {
+ config: dockerComposeConfigPath,
+ log: debug,
+ }
+ );
+}
+
/**
* Performs the given action again and again until it does not throw an error.
*
@@ -281,10 +421,11 @@ async function retry( action, { times, delay = 5000 } ) {
*
* @param {Config} config The wp-env config object.
*/
-async function checkDatabaseConnection( { dockerComposeConfigPath } ) {
+async function checkDatabaseConnection( { dockerComposeConfigPath, debug } ) {
await dockerCompose.run( 'cli', 'wp db check', {
config: dockerComposeConfigPath,
commandOptions: [ '--rm' ],
+ log: debug,
} );
}
@@ -300,25 +441,41 @@ async function configureWordPress( environment, config ) {
const options = {
config: config.dockerComposeConfigPath,
commandOptions: [ '--rm' ],
+ log: config.debug,
};
+ const port = environment === 'development' ? config.port : config.testsPort;
+
// 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`,
+ [
+ 'wp',
+ 'core',
+ 'install',
+ `--url=localhost:${ port }`,
+ `--title=${ config.name }`,
+ '--admin_user=admin',
+ '--admin_password=password',
+ '--admin_email=wordpress@example.com',
+ '--skip-email',
+ ],
options
);
+ // Set wp-config.php values.
+ for ( const [ key, value ] of Object.entries( config.config ) ) {
+ const command = [ 'wp', 'config', 'set', key, value ];
+ if ( typeof value !== 'string' ) {
+ command.push( '--raw' );
+ }
+ await dockerCompose.run(
+ environment === 'development' ? 'cli' : 'tests-cli',
+ command,
+ options
+ );
+ }
+
// Activate all plugins.
for ( const pluginSource of config.pluginSources ) {
await dockerCompose.run(
@@ -345,10 +502,14 @@ async function configureWordPress( environment, config ) {
* @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 } ) {
+async function resetDatabase(
+ environment,
+ { dockerComposeConfigPath, debug }
+) {
const options = {
config: dockerComposeConfigPath,
commandOptions: [ '--rm' ],
+ log: debug,
};
const tasks = [];
diff --git a/packages/env/package.json b/packages/env/package.json
index 62dd6d06a2456..1cdd5d80bb412 100644
--- a/packages/env/package.json
+++ b/packages/env/package.json
@@ -35,9 +35,14 @@
"chalk": "^2.4.2",
"copy-dir": "^1.2.0",
"docker-compose": "^0.22.2",
+ "extract-zip": "^1.6.7",
+ "inquirer": "^7.0.4",
"js-yaml": "^3.13.1",
"nodegit": "^0.26.2",
"ora": "^4.0.2",
+ "request": "^2.88.2",
+ "request-progress": "^3.0.0",
+ "rimraf": "^3.0.2",
"terminal-link": "^2.0.0",
"yargs": "^14.0.0"
},
diff --git a/packages/env/test/cli.js b/packages/env/test/cli.js
index ef19cc816dd0b..2e2b30aff5bdf 100644
--- a/packages/env/test/cli.js
+++ b/packages/env/test/cli.js
@@ -8,6 +8,7 @@ const env = require( '../lib/env' );
/**
* Mocked dependencies
*/
+jest.spyOn( process, 'exit' ).mockImplementation( () => {} );
jest.mock( 'ora', () => () => ( {
start() {
return { text: '', succeed: jest.fn(), fail: jest.fn() };
@@ -84,8 +85,6 @@ describe( 'env cli', () => {
/* eslint-disable no-console */
env.start.mockRejectedValueOnce( {
message: 'failure message',
- out: 'failure message',
- exitCode: 2,
} );
const consoleError = console.error;
console.error = jest.fn();
@@ -98,28 +97,37 @@ describe( 'env cli', () => {
expect( spinner.fail ).toHaveBeenCalledWith( 'failure message' );
expect( console.error ).toHaveBeenCalled();
- expect( process.exit ).toHaveBeenCalledWith( 2 );
+ expect( process.exit ).toHaveBeenCalledWith( 1 );
console.error = consoleError;
process.exit = processExit;
/* eslint-enable no-console */
} );
- it( 'handles failed commands with errors.', async () => {
+ it( 'handles failed docker commands with errors.', async () => {
/* eslint-disable no-console */
- env.start.mockRejectedValueOnce( { err: 'failure error' } );
+ env.start.mockRejectedValueOnce( {
+ err: 'failure error',
+ out: 'message',
+ exitCode: 1,
+ } );
const consoleError = console.error;
console.error = jest.fn();
const processExit = process.exit;
process.exit = jest.fn();
+ const stderr = process.stderr.write;
+ process.stderr.write = jest.fn();
cli().parse( [ 'start' ] );
const { spinner } = env.start.mock.calls[ 0 ][ 0 ];
await env.start.mock.results[ 0 ].value.catch( () => {} );
- expect( spinner.fail ).toHaveBeenCalledWith( 'failure error' );
- expect( console.error ).toHaveBeenCalled();
+ expect( spinner.fail ).toHaveBeenCalledWith(
+ 'Error while running docker-compose command.'
+ );
+ expect( process.stderr.write ).toHaveBeenCalledWith( 'failure error' );
expect( process.exit ).toHaveBeenCalledWith( 1 );
console.error = consoleError;
process.exit = processExit;
+ process.stderr.write = stderr;
/* eslint-enable no-console */
} );
} );
diff --git a/packages/env/test/config.js b/packages/env/test/config.js
index ef886f078ce4c..181a5a3baec23 100644
--- a/packages/env/test/config.js
+++ b/packages/env/test/config.js
@@ -3,6 +3,7 @@
* External dependencies
*/
const { readFile } = require( 'fs' ).promises;
+const os = require( 'os' );
/**
* Internal dependencies
@@ -217,4 +218,200 @@ describe( 'readConfig', () => {
);
}
} );
+
+ it( 'should throw a validaton error if the ports are not numbers', async () => {
+ expect.assertions( 10 );
+ testPortNumberValidation( 'port', 'string' );
+ testPortNumberValidation( 'testsPort', [] );
+ testPortNumberValidation( 'port', {} );
+ testPortNumberValidation( 'testsPort', false );
+ testPortNumberValidation( 'port', null );
+ } );
+
+ it( 'should throw a validaton error if the ports are the same', async () => {
+ expect.assertions( 2 );
+ readFile.mockImplementation( () =>
+ Promise.resolve( JSON.stringify( { port: 8888, testsPort: 8888 } ) )
+ );
+ try {
+ await readConfig( '.wp-env.json' );
+ } catch ( error ) {
+ expect( error ).toBeInstanceOf( ValidationError );
+ expect( error.message ).toContain(
+ 'Invalid .wp-env.json: "testsPort" and "port" must be different.'
+ );
+ }
+ } );
+
+ it( 'should parse custom ports', async () => {
+ readFile.mockImplementation( () =>
+ Promise.resolve(
+ JSON.stringify( {
+ port: 1000,
+ } )
+ )
+ );
+ const config = await readConfig( '.wp-env.json' );
+ // Custom port is overriden while testsPort gets the deault value.
+ expect( config ).toMatchObject( {
+ port: 1000,
+ testsPort: 8889,
+ } );
+ } );
+
+ it( 'should throw an error if the port number environment variable is invalid', async () => {
+ readFile.mockImplementation( () =>
+ Promise.resolve( JSON.stringify( {} ) )
+ );
+ const oldPort = process.env.WP_ENV_PORT;
+ process.env.WP_ENV_PORT = 'hello';
+ try {
+ await readConfig( '.wp-env.json' );
+ } catch ( error ) {
+ expect( error ).toBeInstanceOf( ValidationError );
+ expect( error.message ).toContain(
+ 'Invalid environment variable: WP_ENV_PORT must be a number.'
+ );
+ }
+ process.env.WP_ENV_PORT = oldPort;
+ } );
+
+ it( 'should throw an error if the tests port number environment variable is invalid', async () => {
+ readFile.mockImplementation( () =>
+ Promise.resolve( JSON.stringify( {} ) )
+ );
+ const oldPort = process.env.WP_ENV_TESTS_PORT;
+ process.env.WP_ENV_TESTS_PORT = 'hello';
+ try {
+ await readConfig( '.wp-env.json' );
+ } catch ( error ) {
+ expect( error ).toBeInstanceOf( ValidationError );
+ expect( error.message ).toContain(
+ 'Invalid environment variable: WP_ENV_TESTS_PORT must be a number.'
+ );
+ }
+ process.env.WP_ENV_TESTS_PORT = oldPort;
+ } );
+
+ it( 'should use port environment values rather than config values if both are defined', async () => {
+ readFile.mockImplementation( () =>
+ Promise.resolve(
+ JSON.stringify( {
+ port: 1000,
+ testsPort: 2000,
+ } )
+ )
+ );
+ const oldPort = process.env.WP_ENV_PORT;
+ const oldTestsPort = process.env.WP_ENV_TESTS_PORT;
+ process.env.WP_ENV_PORT = 4000;
+ process.env.WP_ENV_TESTS_PORT = 3000;
+
+ const config = await readConfig( '.wp-env.json' );
+ expect( config ).toMatchObject( {
+ port: 4000,
+ testsPort: 3000,
+ } );
+
+ process.env.WP_ENV_PORT = oldPort;
+ process.env.WP_ENV_TESTS_PORT = oldTestsPort;
+ } );
+
+ it( 'should use 8888 and 8889 as the default port and testsPort values if nothing else is specified', async () => {
+ readFile.mockImplementation( () =>
+ Promise.resolve( JSON.stringify( {} ) )
+ );
+
+ const config = await readConfig( '.wp-env.json' );
+ expect( config ).toMatchObject( {
+ port: 8888,
+ testsPort: 8889,
+ } );
+ } );
+
+ it( 'should use the WP_ENV_HOME environment variable only if specified', async () => {
+ readFile.mockImplementation( () =>
+ Promise.resolve( JSON.stringify( {} ) )
+ );
+ const oldEnvHome = process.env.WP_ENV_HOME;
+
+ expect.assertions( 2 );
+
+ process.env.WP_ENV_HOME = 'here/is/a/path';
+ const configWith = await readConfig( '.wp-env.json' );
+ expect(
+ configWith.workDirectoryPath.includes( 'here/is/a/path' )
+ ).toBe( true );
+
+ process.env.WP_ENV_HOME = undefined;
+ const configWithout = await readConfig( '.wp-env.json' );
+ expect(
+ configWithout.workDirectoryPath.includes( 'here/is/a/path' )
+ ).toBe( false );
+
+ process.env.WP_ENV_HOME = oldEnvHome;
+ } );
+
+ it( 'should use the WP_ENV_HOME environment variable on Linux', async () => {
+ readFile.mockImplementation( () =>
+ Promise.resolve( JSON.stringify( {} ) )
+ );
+ const oldEnvHome = process.env.WP_ENV_HOME;
+ const oldOsPlatform = os.platform;
+ os.platform = () => 'linux';
+
+ expect.assertions( 2 );
+
+ process.env.WP_ENV_HOME = 'here/is/a/path';
+ const configWith = await readConfig( '.wp-env.json' );
+ expect(
+ configWith.workDirectoryPath.includes( 'here/is/a/path' )
+ ).toBe( true );
+
+ process.env.WP_ENV_HOME = undefined;
+ const configWithout = await readConfig( '.wp-env.json' );
+ expect(
+ configWithout.workDirectoryPath.includes( 'here/is/a/path' )
+ ).toBe( false );
+
+ process.env.WP_ENV_HOME = oldEnvHome;
+ os.platform = oldOsPlatform;
+ } );
+
+ it( 'should use a non-private folder on Linux', async () => {
+ readFile.mockImplementation( () =>
+ Promise.resolve( JSON.stringify( {} ) )
+ );
+ const oldOsPlatform = os.platform;
+ os.platform = () => 'linux';
+
+ expect.assertions( 2 );
+
+ const config = await readConfig( '.wp-env.json' );
+ expect( config.workDirectoryPath.includes( '.wp-env' ) ).toBe( false );
+ expect( config.workDirectoryPath.includes( 'wp-env' ) ).toBe( true );
+
+ os.platform = oldOsPlatform;
+ } );
} );
+
+/**
+ * Tests that readConfig will throw errors when invalid port numbers are passed.
+ *
+ * @param {string} portName The name of the port to test ('port' or 'testsPort')
+ * @param {any} value A value which should throw an error.
+ */
+async function testPortNumberValidation( portName, value ) {
+ readFile.mockImplementation( () =>
+ Promise.resolve( JSON.stringify( { [ portName ]: value } ) )
+ );
+ try {
+ await readConfig( '.wp-env.json' );
+ } catch ( error ) {
+ expect( error ).toBeInstanceOf( ValidationError );
+ expect( error.message ).toContain(
+ `Invalid .wp-env.json: "${ portName }" must be an integer.`
+ );
+ }
+ jest.clearAllMocks();
+}
diff --git a/packages/rich-text/src/to-dom.js b/packages/rich-text/src/to-dom.js
index 2e5c54e769b1e..0a63e94a95e3a 100644
--- a/packages/rich-text/src/to-dom.js
+++ b/packages/rich-text/src/to-dom.js
@@ -302,5 +302,24 @@ export function applySelection( { startPath, endPath }, current ) {
}
selection.addRange( range );
- activeElement.focus();
+
+ // This function is not intended to cause a shift in focus. Since the above
+ // selection manipulations may shift focus, ensure that focus is restored to
+ // its previous state. `activeElement` can be `null` or the body element if
+ // there is no focus, which is accounted for here in the explicit `blur` to
+ // restore to a state of non-focus.
+ if ( activeElement !== document.activeElement ) {
+ // The `instanceof` checks protect against edge cases where the focused
+ // element is not of the interface HTMLElement (does not have a `focus`
+ // or `blur` property).
+ //
+ // See: https://github.com/Microsoft/TypeScript/issues/5901#issuecomment-431649653
+ if ( activeElement ) {
+ if ( activeElement instanceof window.HTMLElement ) {
+ activeElement.focus();
+ }
+ } else if ( document.activeElement instanceof window.HTMLElement ) {
+ document.activeElement.blur();
+ }
+ }
}
]