Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Env: Add support for ZIP URL sources. #20426

Merged
merged 1 commit into from
Feb 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 73 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/env/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### 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).
Expand Down
11 changes: 6 additions & 5 deletions packages/env/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,11 +194,12 @@ _Note: the port number environment variables (`WP_ENV_PORT` and `WP_ENV_TESTS_PO

Several types of strings can be passed into the `core`, `plugins`, and `themes` fields:

| Type | Format | Example(s) |
| ----------------- | -------------------------- | -------------------------------------------------------- |
| Relative path | `.<path>|~<path>` | `"./a/directory"`, `"../a/directory"`, `"~/a/directory"` |
| Absolute path | `/<path>|<letter>:\<path>` | `"/a/directory"`, `"C:\\a\\directory"` |
| GitHub repository | `<owner>/<repo>[#<ref>]` | `"WordPress/WordPress"`, `"WordPress/gutenberg#master"` |
| Type | Format | Example(s) |
| ----------------- | ----------------------------- | -------------------------------------------------------- |
| Relative path | `.<path>|~<path>` | `"./a/directory"`, `"../a/directory"`, `"~/a/directory"` |
| Absolute path | `/<path>|<letter>:\<path>` | `"/a/directory"`, `"C:\\a\\directory"` |
| GitHub repository | `<owner>/<repo>[#<ref>]` | `"WordPress/WordPress"`, `"WordPress/gutenberg#master"` |
| ZIP File | `http[s]://<host>/<path>.zip` | `"https://wordpress.org/wordpress-5.4-beta2.zip"` |
Remote sources will be downloaded into a temporary directory located in `~/.wp-env`.
Expand Down
15 changes: 15 additions & 0 deletions packages/env/lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,21 @@ function parseSourceString( sourceString, { workDirectoryPath } ) {
};
}

const zipFields = sourceString.match(
/^https?:\/\/([^\s$.?#].[^\s]*)\.zip$/
epiqueras marked this conversation as resolved.
Show resolved Hide resolved
);
if ( zipFields ) {
return {
type: 'zip',
url: sourceString,
path: path.resolve(
workDirectoryPath,
encodeURIComponent( zipFields[ 1 ] )
),
basename: encodeURIComponent( zipFields[ 1 ] ),
epiqueras marked this conversation as resolved.
Show resolved Hide resolved
};
}

const gitHubFields = sourceString.match( /^([^\/]+)\/([^#]+)(?:#(.+))?$/ );
if ( gitHubFields ) {
return {
Expand Down
61 changes: 59 additions & 2 deletions packages/env/lib/download-source.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,6 +34,8 @@ const NodeGit = require( 'nodegit' );
module.exports = async function downloadSource( source, options ) {
if ( source.type === 'git' ) {
await downloadGitSource( source, options );
} else if ( source.type === 'zip' ) {
await downloadZipSource( source, options );
}
};

Expand All @@ -36,8 +51,7 @@ module.exports = async function downloadSource( source, options ) {
*/
async function downloadGitSource( source, { onProgress, spinner, debug } ) {
const log = debug
? // eslint-disable-next-line no-console
( message ) => {
? ( message ) => {
spinner.info( `NodeGit: ${ message }` );
spinner.start();
}
Expand Down Expand Up @@ -96,3 +110,46 @@ async function downloadGitSource( source, { onProgress, spinner, debug } ) {

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 ) =>
epiqueras marked this conversation as resolved.
Show resolved Hide resolved
copyDir( path.join( dirName, file ), source.path )
),
] );
await rimraf( dirName );

onProgress( 1 );
}
3 changes: 3 additions & 0 deletions packages/env/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,13 @@
"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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting that request is deprecated, even at the time it was introduced here:

https://www.npmjs.com/package/request
request/request#3142

Which isn't necessarily to say that a deprecation is bad, but we can't expect any future maintenance (which could become problematic for security issues and the like). Another option like got, bent (from the author of request), or just straight https may be a more reliable long-term dependency. For the specific needs of progress events, the first of these alternatives does have the most compatible API for how we're using it here, though a combination of an https response Content-Length header plus an incrementing value in a stream iteration may suffice as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the heads up. Created an issue here: #21336

"request-progress": "^3.0.0",
"rimraf": "^3.0.2",
"terminal-link": "^2.0.0",
"yargs": "^14.0.0"
Expand Down