diff --git a/client/build/make_replacements.mjs b/client/build/make_replacements.mjs new file mode 100644 index 0000000000..038cc37da0 --- /dev/null +++ b/client/build/make_replacements.mjs @@ -0,0 +1,23 @@ +// Copyright 2024 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import replace from 'replace-in-file'; + +export async function makeReplacements(replacements) { + let results = []; + + for (const replacement of replacements) { + results = [...results, ...(await replace(replacement))]; + } +} diff --git a/client/package.json b/client/package.json index a1d6cdc737..816500fa43 100644 --- a/client/package.json +++ b/client/package.json @@ -95,6 +95,7 @@ "eslint-plugin-import": "^2.26.0", "esm": "^3.2.25", "file-loader": "^6.2.0", + "folder-hash": "^4.0.4", "html-webpack-plugin": "^5.1.0", "husky": "^1.3.1", "i18n-strings-files": "^2.0.0", diff --git a/client/src/cordova/dev.action.mjs b/client/src/cordova/dev.action.mjs new file mode 100644 index 0000000000..9cce62cd7e --- /dev/null +++ b/client/src/cordova/dev.action.mjs @@ -0,0 +1,142 @@ +// Copyright 2022 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import os from 'os'; +import path from 'path'; +import url from 'url'; + +import {createReloadServer} from '@outline/infrastructure/build/create_reload_server.mjs'; +import {getRootDir} from '@outline/infrastructure/build/get_root_dir.mjs'; +import {runAction} from '@outline/infrastructure/build/run_action.mjs'; +import {spawnStream} from '@outline/infrastructure/build/spawn_stream.mjs'; +import {hashElement} from 'folder-hash'; +import * as fs from 'fs-extra'; +import minimist from 'minimist'; + +import {makeReplacements} from '../../build/make_replacements.mjs'; + +const OUTPUT_PATH = 'output/build/client/macos'; +const OUTLINE_APP_PATH = 'Debug/Outline.app'; +const OUTLINE_APP_WWW_PATH = 'Contents/Resources/www'; +const RELOAD_SERVER_PORT = 35729; + +const getUIHash = async () => { + const hashResult = await hashElement( + path.join(getRootDir(), 'client/src/www'), + { + files: {include: ['**/*.ts', '**/*.html', '**/*.css', '**/*.js']}, + } + ); + + return hashResult.hash; +}; + +/** + * @description Runs the cordova app in development mode. + * + * @param {string[]} parameters + */ +export async function main(...givenParameters) { + const { + _: [platform = 'macos'], + buildMode = 'release', + sentryDsn = 'https://public@sentry.example.com/1', + versionName = '0.0.0-dev', + } = minimist(givenParameters); + + if (platform !== 'macos') { + throw new Error('This action currently only works for the MacOS platform.'); + } + + if (buildMode !== 'release') { + throw new Error('MacOS only renders properly in the release build mode.'); + } + + if (os.platform() !== 'darwin') { + throw new Error('You must be on MacOS to develop for MacOS.'); + } + + const parameters = [ + 'macos', + '--buildMode=release', + `--sentryDsn=${sentryDsn}`, + `--versionName=${versionName}`, + ]; + + await runAction('client/src/www/build', ...parameters); + await runAction('client/go/build', ...parameters); + await runAction('client/src/cordova/setup', ...parameters); + + await makeReplacements([ + { + files: path.join( + getRootDir(), + 'client/platforms/osx/**/index_cordova.html' + ), + from: '', + to: ` + + `, + }, + ]); + + await spawnStream( + 'xcodebuild', + '-scheme', + 'Outline', + '-workspace', + path.join(getRootDir(), 'client/src/cordova/apple/macos.xcworkspace'), + `SYMROOT=${path.join(getRootDir(), OUTPUT_PATH)}` + ); + + await spawnStream( + 'open', + path.join(getRootDir(), OUTPUT_PATH, OUTLINE_APP_PATH) + ); + + let previousUIHashResult = await getUIHash(); + + console.log(`Starting reload server @ port ${RELOAD_SERVER_PORT}...`); + createReloadServer(async () => { + const currentUIHashResult = await getUIHash(); + + if (previousUIHashResult === currentUIHashResult) { + return false; + } + + previousUIHashResult = currentUIHashResult; + + await runAction('client/src/www/build', ...parameters); + + await fs.copy( + path.join(getRootDir(), 'client/www'), + path.join(OUTPUT_PATH, OUTLINE_APP_PATH, OUTLINE_APP_WWW_PATH) + ); + + return true; + }).listen(RELOAD_SERVER_PORT); +} + +if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { + await main(...process.argv.slice(2)); +} diff --git a/client/src/cordova/setup.action.mjs b/client/src/cordova/setup.action.mjs index 3e4c390f95..9d7770c06b 100644 --- a/client/src/cordova/setup.action.mjs +++ b/client/src/cordova/setup.action.mjs @@ -21,10 +21,10 @@ import {runAction} from '@outline/infrastructure/build/run_action.mjs'; import {spawnStream} from '@outline/infrastructure/build/spawn_stream.mjs'; import chalk from 'chalk'; import cordovaLib from 'cordova-lib'; -import replace from 'replace-in-file'; import rmfr from 'rmfr'; import {getBuildParameters} from '../../build/get_build_parameters.mjs'; +import {makeReplacements} from '../../build/make_replacements.mjs'; const {cordova} = cordovaLib; const WORKING_CORDOVA_OSX_COMMIT = '07e62a53aa6a8a828fd988bc9e884c38c3495a67'; @@ -37,17 +37,20 @@ const WORKING_CORDOVA_OSX_COMMIT = '07e62a53aa6a8a828fd988bc9e884c38c3495a67'; * @param {string[]} parameters */ export async function main(...parameters) { - const {platform, buildMode, verbose, buildNumber, versionName} = getBuildParameters(parameters); + const {platform, buildMode, verbose, buildNumber, versionName} = + getBuildParameters(parameters); await runAction('client/src/www/build', ...parameters); await runAction('client/go/build', ...parameters); - - const CORDOVA_PROJECT_DIR = path.resolve(getRootDir(), 'client'); + + const CORDOVA_PROJECT_DIR = path.resolve(getRootDir(), 'client'); await rmfr(path.resolve(CORDOVA_PROJECT_DIR, 'platforms')); await rmfr(path.resolve(CORDOVA_PROJECT_DIR, 'plugins')); if (verbose) { - cordova.on('verbose', message => console.debug(`[cordova:verbose] ${message}`)); + cordova.on('verbose', message => + console.debug(`[cordova:verbose] ${message}`) + ); } // this is so cordova doesn't complain about not being in a cordova project @@ -57,7 +60,9 @@ export async function main(...parameters) { case 'android' + 'debug': return androidDebug(verbose); case 'android' + 'release': - console.warn('NOTE: You must open the Outline.zip file after building to upload to the Play Store.'); + console.warn( + 'NOTE: You must open the Outline.zip file after building to upload to the Play Store.' + ); return androidRelease(versionName, buildNumber, verbose); case 'ios' + 'debug': case 'maccatalyst' + 'debug': @@ -86,14 +91,6 @@ async function androidDebug(verbose) { }); } -async function makeReplacements(replacements) { - let results = []; - - for (const replacement of replacements) { - results = [...results, ...(await replace(replacement))]; - } -} - async function androidRelease(versionName, buildNumber, verbose) { await cordova.prepare({ platforms: ['android'], @@ -101,8 +98,20 @@ async function androidRelease(versionName, buildNumber, verbose) { verbose, }); - const manifestXmlGlob = path.join(getRootDir(), 'platforms', 'android', '**', 'AndroidManifest.xml'); - const configXmlGlob = path.join(getRootDir(), 'platforms', 'android', '**', 'config.xml'); + const manifestXmlGlob = path.join( + getRootDir(), + 'platforms', + 'android', + '**', + 'AndroidManifest.xml' + ); + const configXmlGlob = path.join( + getRootDir(), + 'platforms', + 'android', + '**', + 'config.xml' + ); await makeReplacements([ { @@ -130,7 +139,9 @@ async function androidRelease(versionName, buildNumber, verbose) { async function appleIosDebug(verbose) { if (os.platform() !== 'darwin') { - throw new Error('Building an Apple binary requires xcodebuild and can only be done on MacOS'); + throw new Error( + 'Building an Apple binary requires xcodebuild and can only be done on MacOS' + ); } await cordova.prepare({ @@ -140,19 +151,32 @@ async function appleIosDebug(verbose) { }); // TODO(daniellacosse): move this to a cordova hook - await spawnStream('rsync', '-avc', 'src/cordova/apple/xcode/ios/', 'platforms/ios/'); + await spawnStream( + 'rsync', + '-avc', + 'src/cordova/apple/xcode/ios/', + 'platforms/ios/' + ); } async function appleMacOsDebug(verbose) { if (os.platform() !== 'darwin') { - throw new Error('Building an Apple binary requires xcodebuild and can only be done on MacOS'); + throw new Error( + 'Building an Apple binary requires xcodebuild and can only be done on MacOS' + ); } console.warn( - chalk.yellow('Debug mode on the MacOS client is currently broken. Try running with `--buildMode=release` instead.') + chalk.yellow( + 'Debug mode on the MacOS client is currently broken. Try running with `--buildMode=release` instead.' + ) ); - await cordova.platform('add', [`github:apache/cordova-osx#${WORKING_CORDOVA_OSX_COMMIT}`], {save: false}); + await cordova.platform( + 'add', + [`github:apache/cordova-osx#${WORKING_CORDOVA_OSX_COMMIT}`], + {save: false} + ); await cordova.prepare({ platforms: ['osx'], @@ -161,7 +185,12 @@ async function appleMacOsDebug(verbose) { }); // TODO(daniellacosse): move this to a cordova hook - await spawnStream('rsync', '-avc', 'src/cordova/apple/xcode/macos/', 'platforms/osx/'); + await spawnStream( + 'rsync', + '-avc', + 'src/cordova/apple/xcode/macos/', + 'platforms/osx/' + ); } async function setAppleVersion(platform, versionName, buildNumber) { @@ -181,7 +210,9 @@ async function setAppleVersion(platform, versionName, buildNumber) { async function appleIosRelease(version, buildNumber, verbose) { if (os.platform() !== 'darwin') { - throw new Error('Building an Apple binary requires xcodebuild and can only be done on MacOS'); + throw new Error( + 'Building an Apple binary requires xcodebuild and can only be done on MacOS' + ); } await cordova.prepare({ @@ -191,17 +222,28 @@ async function appleIosRelease(version, buildNumber, verbose) { }); // TODO(daniellacosse): move this to a cordova hook - await spawnStream('rsync', '-avc', 'src/cordova/apple/xcode/ios/', 'platforms/ios/'); + await spawnStream( + 'rsync', + '-avc', + 'src/cordova/apple/xcode/ios/', + 'platforms/ios/' + ); await setAppleVersion('ios', version, buildNumber); } async function appleMacOsRelease(version, buildNumber, verbose) { if (os.platform() !== 'darwin') { - throw new Error('Building an Apple binary requires xcodebuild and can only be done on MacOS'); + throw new Error( + 'Building an Apple binary requires xcodebuild and can only be done on MacOS' + ); } - await cordova.platform('add', [`github:apache/cordova-osx#${WORKING_CORDOVA_OSX_COMMIT}`], {save: false}); + await cordova.platform( + 'add', + [`github:apache/cordova-osx#${WORKING_CORDOVA_OSX_COMMIT}`], + {save: false} + ); await cordova.prepare({ platforms: ['osx'], @@ -210,7 +252,12 @@ async function appleMacOsRelease(version, buildNumber, verbose) { }); // TODO(daniellacosse): move this to a cordova hook - await spawnStream('rsync', '-avc', 'src/cordova/apple/xcode/macos/', 'platforms/osx/'); + await spawnStream( + 'rsync', + '-avc', + 'src/cordova/apple/xcode/macos/', + 'platforms/osx/' + ); await setAppleVersion('osx', version, buildNumber); } diff --git a/client/src/www/index_cordova.html b/client/src/www/index_cordova.html index 6e2181e4a2..1ca43c19a7 100644 --- a/client/src/www/index_cordova.html +++ b/client/src/www/index_cordova.html @@ -26,5 +26,4 @@ - diff --git a/infrastructure/build/create_reload_server.mjs b/infrastructure/build/create_reload_server.mjs new file mode 100644 index 0000000000..3b6bb9775a --- /dev/null +++ b/infrastructure/build/create_reload_server.mjs @@ -0,0 +1,36 @@ +// Copyright 2024 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {createServer} from 'node:http'; + +import {WebSocketServer} from 'ws'; + +export function createReloadServer( + shouldReload = () => true, + reloadCheckIntervalMs = 1000 +) { + const server = createServer(); + const websocket = new WebSocketServer({server}); + + websocket.on('connection', connection => { + setInterval(async () => { + if (!(await shouldReload())) return; + + console.log('Reloading...'); + connection.send('reload'); + }, reloadCheckIntervalMs); + }); + + return server; +} diff --git a/infrastructure/package.json b/infrastructure/package.json index 8bd9bc4156..bb8fdfcb85 100644 --- a/infrastructure/package.json +++ b/infrastructure/package.json @@ -18,6 +18,7 @@ "dependencies": { "chalk": "^5.3.0", "node-fetch": "^3.3.2", - "node-forge": "^1.3.1" + "node-forge": "^1.3.1", + "ws": "^8.18.0" } } diff --git a/package-lock.json b/package-lock.json index e8ca4f81b1..73049e944a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -130,6 +130,7 @@ "eslint-plugin-import": "^2.26.0", "esm": "^3.2.25", "file-loader": "^6.2.0", + "folder-hash": "^4.0.4", "html-webpack-plugin": "^5.1.0", "husky": "^1.3.1", "i18n-strings-files": "^2.0.0", @@ -779,7 +780,8 @@ "dependencies": { "chalk": "^5.3.0", "node-fetch": "^3.3.2", - "node-forge": "^1.3.1" + "node-forge": "^1.3.1", + "ws": "^8.18.0" }, "devDependencies": { "@types/jasmine": "^5.1.4", @@ -808,6 +810,26 @@ "url": "https://opencollective.com/node-fetch" } }, + "infrastructure/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -6980,8 +7002,7 @@ }, "node_modules/@polymer/polymer": { "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@polymer/polymer/-/polymer-3.5.1.tgz", - "integrity": "sha512-JlAHuy+1qIC6hL1ojEUfIVD58fzTpJAoCxFwV5yr0mYTXV1H8bz5zy0+rC963Cgr9iNXQ4T9ncSjC2fkF9BQfw==", + "license": "BSD-3-Clause", "dependencies": { "@webcomponents/shadycss": "^1.9.1" } @@ -13386,9 +13407,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001649", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001649.tgz", - "integrity": "sha512-fJegqZZ0ZX8HOWr6rcafGr72+xcgJKI9oWfDW5DrD7ExUtgZC7a7R7ZYmZqplh7XDocFdGeIFn7roAxhOeYrPQ==", + "version": "1.0.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", "dev": true, "funding": [ { @@ -17498,9 +17519,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.4.tgz", - "integrity": "sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.5.tgz", + "integrity": "sha512-QR7/A7ZkMS8tZuoftC/jfqNkZLQO779SSW3YuZHP4eXpj3EffGLFcB/Xu9AAZQzLccTiCV+EmUo3ha4mQ9wnlA==", "dev": true }, "node_modules/electron-updater": { @@ -20041,6 +20062,43 @@ "readable-stream": "^2.3.6" } }, + "node_modules/folder-hash": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/folder-hash/-/folder-hash-4.0.4.tgz", + "integrity": "sha512-zEyYH+UsHEfJJcCRSf9ai5I4CTZwZ8ObONRuEI5hcEmJY5pS0FUWKruX9mMnYJrgC7MlPFDYnGsK1R+WFYjLlQ==", + "dev": true, + "dependencies": { + "debug": "^4.3.3", + "minimatch": "~5.1.2" + }, + "bin": { + "folder-hash": "bin/folder-hash" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/folder-hash/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/folder-hash/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", @@ -43574,6 +43632,7 @@ "eslint-plugin-import": "^2.26.0", "esm": "^3.2.25", "file-loader": "^6.2.0", + "folder-hash": "^4.0.4", "fs-extra": "^11.2.0", "html-webpack-plugin": "^5.1.0", "husky": "^1.3.1", @@ -44025,7 +44084,8 @@ "node-fetch": "^3.3.2", "node-forge": "^1.3.1", "stream-http": "^3.2.0", - "typescript": "^5.4.5" + "typescript": "^5.4.5", + "ws": "^8.18.0" }, "dependencies": { "node-fetch": { @@ -44037,6 +44097,12 @@ "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } + }, + "ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "requires": {} } } }, @@ -45806,8 +45872,6 @@ }, "@polymer/polymer": { "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@polymer/polymer/-/polymer-3.5.1.tgz", - "integrity": "sha512-JlAHuy+1qIC6hL1ojEUfIVD58fzTpJAoCxFwV5yr0mYTXV1H8bz5zy0+rC963Cgr9iNXQ4T9ncSjC2fkF9BQfw==", "requires": { "@webcomponents/shadycss": "^1.9.1" } @@ -50571,9 +50635,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001649", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001649.tgz", - "integrity": "sha512-fJegqZZ0ZX8HOWr6rcafGr72+xcgJKI9oWfDW5DrD7ExUtgZC7a7R7ZYmZqplh7XDocFdGeIFn7roAxhOeYrPQ==", + "version": "1.0.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", "dev": true }, "caseless": { @@ -53569,9 +53633,9 @@ } }, "electron-to-chromium": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.4.tgz", - "integrity": "sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.5.tgz", + "integrity": "sha512-QR7/A7ZkMS8tZuoftC/jfqNkZLQO779SSW3YuZHP4eXpj3EffGLFcB/Xu9AAZQzLccTiCV+EmUo3ha4mQ9wnlA==", "dev": true }, "electron-updater": { @@ -55488,6 +55552,36 @@ "readable-stream": "^2.3.6" } }, + "folder-hash": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/folder-hash/-/folder-hash-4.0.4.tgz", + "integrity": "sha512-zEyYH+UsHEfJJcCRSf9ai5I4CTZwZ8ObONRuEI5hcEmJY5pS0FUWKruX9mMnYJrgC7MlPFDYnGsK1R+WFYjLlQ==", + "dev": true, + "requires": { + "debug": "^4.3.3", + "minimatch": "~5.1.2" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",