├── .gitignore ├── LICENSE ├── README.md ├── composer.json └── src ├── CDNifyFacade.php ├── CDNifyRepository.php ├── CDNifyServiceProvider.php ├── CDNifyViewComposer.php ├── Commands └── CDNifyCommand.php ├── Contracts └── CDNifyRepositoryInterface.php └── Resources └── config └── cdnify.php /.gitignore: -------------------------------------------------------------------------------- 1 | .phpintel 2 | 3 | /vendor/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Daniel Hudson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # laravel-cdnify 2 | [![GitHub release](https://img.shields.io/github/release/metrique/laravel-cdnify.svg?maxAge=2592000)]() 3 | 4 | ## Features 5 | - Add a CDN to any path when a specified environment is active in Laravel 5. 6 | - Easily publish laravel-mix or laravel-elixir versioned assets to a file system of your choice. 7 | 8 | ## Installation 9 | ### Set up. 10 | 1. `composer require metrique/laravel-cdnify` 11 | 2. Optionally add `Metrique\CDNify\CDNifyServiceProvider::class` to the list of *Autoloaded Service Providers* in `config/app.php`. 12 | 3. Optionally add `'CDNify'=>Metrique\CDNify\CDNifyFacade::class` to the list of *Class Aliases* in `config/app.php`. 13 | 14 | CDNify supports Laravel Package Discovery and so steps 2 and 3 are optional. 15 | 16 | ### Config. 17 | Config defaults can be configured by editing `config/cdnify.php` in your main application directory. 18 | 19 | You can publish the `config/cdnify.php` config file to your application config directory by running `php artisan vendor:publish --tag="cdnify-config"` 20 | 21 | ## Usage 22 | ### Examples. 23 | #### Get Helper 24 | A resource exists in your mix-manifest.json, which has been created by Laravel Mix. 25 | `` 26 | 27 | #### Get the CDN as a string. 28 | `$cdnify->cdn();` 29 | 30 | #### Set a local path and get the full CDN path. 31 | `$cdnify->path('/some/static/resource.jpg')->toString();` 32 | 33 | ### CDNify 34 | $cdnify is automatically registered for use in all Laravel views. 35 | 36 | `$cdnify->defaults();` If *environments*, *mix* or *roundRobin* settings are changed, this will discard the changes in favour of the config settings. 37 | 38 | `$cdnify->cdn();` Returns a CDN path from the config, if roundRobin is set to true then it will iterate through the list of CDN's on each call. 39 | 40 | `$cdnify->path($path);` Sets the path to be CDNified. 41 | 42 | `$cdnify->toString();` Returns the CDN and path as a string. 43 | 44 | `$cdnify->get($path, $params = []);` Helper utility combining the path and toString methods. You may pass an array of params (mix, environments, roundRobin) to override the settings once. 45 | 46 | `$cdnify->environments($environments);` Set the environments where the path should be CDNified. 47 | 48 | `$cdnify->mix($bool);` Sets whether mix should be used, if available. 49 | 50 | `$cdnify->roundRobin($bool);` Enables round robin iteration on the cdn list. 51 | 52 | ### CDNify command 53 | `php artisan metrique:cdnify` 54 | This command will run `npm run production` or `gulp --production` and then deploy any assets listed in mix-manifest.json to s3 (or other disk), via the Laravel Filesystem. 55 | 56 | ### Options 57 | `--build-source[=BUILD-SOURCE]` Sets the path to the source files that are to be uploaded. [default: "/build"] 58 | 59 | `--build-dest[=BUILD-DEST]` Sets the path where files are to be uploaded. [default: "/build"]. 60 | 61 | `--disk[=DISK]` Set disk/upload method. [default: "s3"] 62 | 63 | `--force` Toggle force upload of files. 64 | 65 | `--skip-build` Skips the running `npm run production` or `gulp --production` build process. 66 | 67 | `--manifest[=MANIFEST]` Set manifest location. [default: "/build/mix-manifest.json"] 68 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metrique/laravel-cdnify", 3 | "description": "Add a CDN to any path when a specified environment is active.", 4 | "type": "Library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Daniel Hudson", 9 | "email": "support@metrique.co.uk", 10 | "homepage": "https://metrique.co.uk" 11 | } 12 | ], 13 | "autoload": { 14 | "psr-4": { 15 | "Metrique\\CDNify\\": "src/" 16 | } 17 | }, 18 | "require": { 19 | "laravel/framework": ">=5.5" 20 | }, 21 | "require-dev": { 22 | "php": "^7.0" 23 | }, 24 | "extra": { 25 | "laravel": { 26 | "providers": [ 27 | "Metrique\\CDNify\\CDNifyServiceProvider" 28 | ], 29 | "aliases": { 30 | "CDNify": "Metrique\\CDNify\\CDNifyFacade" 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/CDNifyFacade.php: -------------------------------------------------------------------------------- 1 | defaults(); 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function defaults() 28 | { 29 | $this->cdn = array_values(config('cdnify.cdn', [])); 30 | $this->renameQueryStrings = config('cdnify.rename_query_strings', true); 31 | $this->roundRobinLength = count($this->cdn); 32 | 33 | $this->mix(config('cdnify.mix', false)); 34 | $this->environments(config('cdnify.environments', [])); 35 | $this->roundRobin(config('cdnify.round_robin')); 36 | 37 | return $this; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function get($path, $params = []) 44 | { 45 | $resets = collect([ 46 | 'mix' => $this->mix, 47 | 'environments' => $this->environments, 48 | 'roundRobin' => $this->roundRobin, 49 | ]); 50 | 51 | $params = $resets->merge($params)->only($resets->keys()->all()); 52 | 53 | $params->each(function ($item, $key) { 54 | $this->{$key}($item); 55 | }); 56 | 57 | $path = $this->path($path)->toString(); 58 | 59 | $resets->each(function ($item, $key) { 60 | $this->{$key}($item); 61 | }); 62 | 63 | return $path; 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function toString() 70 | { 71 | $path = $this->path ?: false; 72 | 73 | if ($path === false) { 74 | return false; 75 | } 76 | 77 | if ($this->mix === true) { 78 | $path = $this->renameQueryString( 79 | $this->mixOrElixir($this->path) 80 | ); 81 | } 82 | 83 | if (in_array(env('APP_ENV'), $this->environments)) { 84 | return $this->cdn().$path; 85 | } 86 | 87 | return $path; 88 | } 89 | 90 | /** 91 | * {@inheritdoc} 92 | */ 93 | public function cdn($path = null) 94 | { 95 | if (!$this->roundRobin) { 96 | return $this->cdn[0]; 97 | } 98 | 99 | if (++$this->roundRobinIndex > ($this->roundRobinLength - 1)) { 100 | $this->roundRobinIndex = 0; 101 | } 102 | 103 | return $this->cdn[$this->roundRobinIndex] . $path ?? ''; 104 | } 105 | 106 | /** 107 | * {@inheritdoc} 108 | */ 109 | public function path($path) 110 | { 111 | $this->path = $path; 112 | 113 | return $this; 114 | } 115 | 116 | /** 117 | * {@inheritdoc} 118 | */ 119 | public function environments($environments) 120 | { 121 | if (is_array($environments)) { 122 | $this->environments = $environments; 123 | } 124 | 125 | return $this; 126 | } 127 | 128 | /** 129 | * {@inheritdoc} 130 | */ 131 | public function mix($bool) 132 | { 133 | if (is_bool($bool)) { 134 | $this->mix = $bool; 135 | } 136 | 137 | return $this; 138 | } 139 | 140 | /** 141 | * {@inheritdoc} 142 | */ 143 | public function roundRobin($bool) 144 | { 145 | if (is_bool($bool)) { 146 | $this->roundRobin = $bool; 147 | } 148 | 149 | return $this; 150 | } 151 | 152 | /** 153 | * {@inheritdoc} 154 | * @param [type] $path [description] 155 | * @param array $params [description] 156 | * @return [type] [description] 157 | */ 158 | public function renameQueryString($path, $params = ['key' => 'id', 'separator' => '-']) 159 | { 160 | if (!$this->renameQueryStrings) { 161 | return $path; 162 | } 163 | 164 | $parsed_path = parse_url($path); 165 | $pathinfo = pathinfo($parsed_path['path']); 166 | 167 | if (!$parsed_path) { 168 | return $parsed_path['path']; 169 | } 170 | 171 | // Check if file is in whitelist 172 | $isInWhitelist = collect( 173 | config('cdnify.rename_whitelist', []) 174 | )->reduce(function ($carry, $item) use ($pathinfo) { 175 | if ($carry) { 176 | return $carry; 177 | } 178 | 179 | return $pathinfo['basename'] == $item; 180 | }, false); 181 | 182 | if ($isInWhitelist) { 183 | return $parsed_path['path']; 184 | } 185 | 186 | // Extract query hash from query string 187 | parse_str($parsed_path['query'], $query); 188 | $hash = $query[$params['key']] ?? null; 189 | 190 | if (!empty($hash)) { 191 | $hash = $params['separator'].$hash; 192 | } 193 | 194 | // Insert hash before extension. 195 | return sprintf( 196 | '%s/%s%s.%s', 197 | $pathinfo['dirname'], 198 | $pathinfo['filename'], 199 | $hash, 200 | $pathinfo['extension'] 201 | ); 202 | } 203 | 204 | protected function mixOrElixir($path) 205 | { 206 | if (config('cdnify.prefer_elixir', false)) { 207 | if (function_exists('elixir')) { 208 | return elixir($path); 209 | } 210 | } 211 | 212 | if (function_exists('mix')) { 213 | return mix($path); 214 | } 215 | 216 | if (function_exists('elixir')) { 217 | return elixir($path); 218 | } 219 | 220 | return $path; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/CDNifyServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 21 | __DIR__.'/Resources/config/cdnify.php' => config_path('cdnify.php'), 22 | ], 'cdnify-config'); 23 | 24 | // Commands 25 | $this->commands('command.metrique.cdnify'); 26 | 27 | // View composer 28 | view()->composer('*', 'Metrique\CDNify\CDNifyViewComposer'); 29 | } 30 | 31 | /** 32 | * Register the application services. 33 | * 34 | * @return void 35 | */ 36 | public function register() 37 | { 38 | $this->registerCDNifyRepository(); 39 | $this->registerCommands(); 40 | } 41 | 42 | /** 43 | * Register the CDNifyRepository singleton binding. 44 | * 45 | * @return void 46 | */ 47 | public function registerCDNifyRepository() 48 | { 49 | $this->app->singleton( 50 | CDNifyRepositoryInterface::class, 51 | CDNifyRepository::class 52 | ); 53 | } 54 | 55 | /** 56 | * Register the artisan commands. 57 | * 58 | * @return void 59 | */ 60 | private function registerCommands() 61 | { 62 | $this->app->bind('command.metrique.cdnify', function ($app) { 63 | return new CDNifyCommand(); 64 | }); 65 | } 66 | } -------------------------------------------------------------------------------- /src/CDNifyViewComposer.php: -------------------------------------------------------------------------------- 1 | cdnify = $cdnify; 27 | } 28 | 29 | /** 30 | * Bind data to the view. 31 | * 32 | * @param View $view 33 | * @return void 34 | */ 35 | public function compose(View $view) 36 | { 37 | $view->with('cdnify', $this->cdnify); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Commands/CDNifyCommand.php: -------------------------------------------------------------------------------- 1 | 0, 22 | 'uploaded' => 0, 23 | ]; 24 | 25 | /** 26 | * The console command name. 27 | * 28 | * @var string 29 | */ 30 | protected $signature = 'metrique:cdnify 31 | {--source : Set build source path.} 32 | {--dest : Set build dest path.} 33 | {--disk : Set disk/upload method.} 34 | {--force : Toggle force upload of files.} 35 | {--manifest : Set manifest location.} 36 | {--skip-build : Skip the build step.} 37 | {--detail : Show upload detail.}'; 38 | 39 | /** 40 | * The console command description. 41 | * 42 | * @var string 43 | */ 44 | protected $description = 'Deploy laravel-mix versioned assets.'; 45 | 46 | /** 47 | * Set disk/upload method. 48 | * 49 | * @var string 50 | */ 51 | protected $disk = 's3'; 52 | 53 | /** 54 | * The build source path. 55 | * 56 | * @var string 57 | */ 58 | protected $build_source; 59 | 60 | /** 61 | * The build dest path. 62 | * 63 | * @var string 64 | */ 65 | protected $build_dest; 66 | 67 | /** 68 | * Force reuploading of files. 69 | * 70 | * @var bool 71 | */ 72 | protected $force; 73 | 74 | /** 75 | * The manifest files to use. 76 | * 77 | * @var array 78 | */ 79 | protected $manifest; 80 | 81 | /** 82 | * Detailed output 83 | * 84 | * @var bool 85 | */ 86 | protected $detail; 87 | 88 | /** 89 | * Create a new command instance. 90 | */ 91 | public function __construct() 92 | { 93 | $this->cdnify = resolve(CDNifyRepositoryInterface::class); 94 | parent::__construct(); 95 | } 96 | 97 | /** 98 | * Execute the console command. 99 | */ 100 | public function handle() 101 | { 102 | $this->setDefaults(); 103 | $this->setOptions(); 104 | 105 | $this->br(); 106 | $this->comment('metrique/laravel-cdnify'); 107 | 108 | if ($this->confirmJob()) { 109 | try { 110 | // 1. Compile, copy and version assets. 111 | $this->build(); 112 | 113 | // 2. Load newly created manifest file and parse ready for asset upload! 114 | $this->manifest(); 115 | 116 | // 3. Upload the files. 117 | $this->upload(); 118 | } catch (\Exception $e) { 119 | $this->error( 120 | 'Encountered an error processing this request, please fix and try again.' 121 | ); 122 | $this->error( 123 | $e->getMessage() 124 | ); 125 | } 126 | } 127 | 128 | $this->br(); 129 | $this->info("Finished..."); 130 | } 131 | 132 | protected function confirmJob() 133 | { 134 | $job = sprintf( 135 | 'This will upload assets from %s to your chosen data store (%s)...', 136 | $this->manifest, 137 | $this->disk 138 | ); 139 | 140 | if (!$this->skip_build) { 141 | $job = sprintf( 142 | 'This will compile and upload assets from %s to your chosen data store (%s)...', 143 | $this->manifest, 144 | $this->disk 145 | ); 146 | } 147 | 148 | $this->info($job); 149 | 150 | return $this->confirm('Do you wish to continue?', true); 151 | } 152 | /** 153 | * Loads defaults from the config file. 154 | */ 155 | private function setDefaults() 156 | { 157 | $this->build_source = config('cdnify.command.build_source', '/'); 158 | $this->build_dest = config('cdnify.command.build_dest', ''); 159 | $this->detail = false; 160 | $this->disk = config('cdnify.command.disk', 's3'); 161 | $this->force = config('cdnify.command.force', false); 162 | $this->skip_build = config('cdnify.command.skip_build', false); 163 | $this->manifest = config('cdnify.command.manifest', '/build/rev-manifest.json'); 164 | } 165 | 166 | /** 167 | * Parses the command line options. 168 | */ 169 | private function setOptions() 170 | { 171 | // Build 172 | if (is_string($this->option('source'))) { 173 | $this->build_source = $this->option('source'); 174 | } 175 | 176 | if (is_string($this->option('dest'))) { 177 | $this->build_dest = $this->option('dest'); 178 | } 179 | 180 | // Disk 181 | if (is_string($this->option('disk'))) { 182 | $this->disk = $this->option('disk'); 183 | } 184 | 185 | // Force 186 | if (is_bool($this->option('force'))) { 187 | $this->force = $this->option('force'); 188 | } 189 | 190 | // Skip build 191 | if (is_bool($this->option('skip-build'))) { 192 | $this->skip_build = $this->option('skip-build'); 193 | } 194 | 195 | // Manifest 196 | if (is_string($this->option('manifest'))) { 197 | $this->manifest = $this->option('manifest'); 198 | } 199 | 200 | // Detail 201 | if (is_bool($this->option('detail'))) { 202 | $this->detail = $this->option('detail'); 203 | } 204 | } 205 | 206 | /** 207 | * Runs Elixir or Gulp in production mode. 208 | */ 209 | private function build() 210 | { 211 | if ($this->skip_build) { 212 | return false; 213 | } 214 | 215 | if (function_exists('mix')) { 216 | return $this->system('npm run production'); 217 | } 218 | 219 | if (function_exists('elixir')) { 220 | return $this->system('gulp --production'); 221 | } 222 | } 223 | 224 | /** 225 | * Reads the manifest file. 226 | */ 227 | private function manifest() 228 | { 229 | $manifestFile = public_path().$this->manifest; 230 | 231 | if (file_exists($manifestFile)) { 232 | $this->manifest = $this->isValidJson(file_get_contents($manifestFile)); 233 | } 234 | } 235 | 236 | /** 237 | * Transmits assets included in the manifest files to storage. 238 | */ 239 | private function upload() 240 | { 241 | $disk = $this->disk; 242 | 243 | $this->info(sprintf("Start asset upload to %s...\n", $this->disk)); 244 | 245 | array_walk($this->manifest, function ($asset) { 246 | $src = sprintf('%s%s/%s', public_path(), $this->build_source, parse_url($asset)['path']); 247 | $src = str_replace('//', '/', $src); 248 | 249 | $dest = sprintf('%s/%s', $this->build_dest, $this->cdnify->renameQueryString($asset)); 250 | $dest = str_replace('//', '/', $dest); 251 | 252 | // Does the file exist locally? 253 | if (!file_exists($src)) { 254 | $this->_comment(sprintf('Skipping. Local file doesn\'t exist. (%s)', $src)); 255 | $this->counts['skipped']++; 256 | 257 | return $asset; 258 | } 259 | 260 | // Storing the file now... 261 | $storage = Storage::disk($this->disk); 262 | 263 | // Exists already on S3 check... 264 | if ($storage->exists($dest) && $this->force == false) { 265 | $this->_comment(sprintf('Skipping. Asset exists on %s. (%s)', $this->disk, $dest)); 266 | $this->counts['skipped']++; 267 | 268 | return $asset; 269 | } 270 | 271 | // Store! 272 | $this->_comment(sprintf('Sending asset to %s. (%s)', $this->disk, $dest)); 273 | 274 | if ($storage->put($dest, file_get_contents($src)) !== true) { 275 | $this->_error('Fail...'); 276 | throw new \Exception('Sending asset failed, aborting!', 1); 277 | } 278 | 279 | $this->counts['uploaded']++; 280 | 281 | if ($this->detail) { 282 | $this->_info('Success...'); 283 | } 284 | }); 285 | 286 | $this->br(); 287 | $this->info(sprintf('Asset upload to %s completed.', $this->disk)); 288 | $this->comment(sprintf('%d files uploaded', $this->counts['uploaded'])); 289 | $this->comment(sprintf('%d files skipped', $this->counts['skipped'])); 290 | } 291 | 292 | /** 293 | * Calls systems commands. 294 | * 295 | * @param string $cmd 296 | * 297 | * @return bool 298 | */ 299 | private function system($cmd) 300 | { 301 | $this->info(sprintf('Start system command. (%s)', $cmd)); 302 | 303 | if (!system($cmd)) { 304 | $this->error(sprintf('System command failed... (%s)', $cmd)); 305 | throw new \Exception('System command failed.', 1); 306 | } 307 | 308 | $this->info(sprintf('End system command. (%s)', $cmd)); 309 | 310 | return true; 311 | } 312 | 313 | /** 314 | * Validates json data. 315 | * 316 | * @param string $json 317 | * 318 | * @return string 319 | */ 320 | private function isValidJson($json) 321 | { 322 | $json = json_decode($json, true); 323 | 324 | if (json_last_error() !== JSON_ERROR_NONE) { 325 | throw new \Exception('Invalid json file.'); 326 | } 327 | 328 | return $json; 329 | } 330 | 331 | /** 332 | * Helper method to make new lines, and comments look pretty! 333 | */ 334 | public function br() 335 | { 336 | $this->info(''); 337 | } 338 | 339 | private function _info($string, $verbosity = null) 340 | { 341 | if ($this->detail) { 342 | return $this->info($string); 343 | } 344 | } 345 | 346 | private function _comment($string, $verbosity = null) 347 | { 348 | if ($this->detail) { 349 | return $this->comment($string); 350 | } 351 | } 352 | 353 | private function _error($string, $verbosity = null) 354 | { 355 | if ($this->detail) { 356 | return $this->error($string); 357 | } 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /src/Contracts/CDNifyRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | 'id', 'separator' => '-']); 79 | } 80 | -------------------------------------------------------------------------------- /src/Resources/config/cdnify.php: -------------------------------------------------------------------------------- 1 | [ 13 | 'cloudfront' => 'https://'.env('AWS_CLOUDFRONT', ''), 14 | ], 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Laravel Mix and Elixir. 19 | |-------------------------------------------------------------------------- 20 | | 21 | | 'mix' specifies if cdnify should use mix() to wrap the path, 22 | | When mix is not found cdnify will fall back to use elixir. 23 | | 24 | | 'rename_query_strings' specifies that any versioned asset should 25 | | be renamed prior them being uploaded and retrieved from a cdn... 26 | | 27 | */ 28 | 'mix' => true, 29 | 'prefer_elixir' => false, 30 | 'rename_query_strings' => true, 31 | 'rename_extension_whitelist' => ['woff2', 'woff', 'ttf'], 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Environment. 36 | |-------------------------------------------------------------------------- 37 | | 38 | | This specifies which environments require a cdn path prefixing. 39 | | 40 | */ 41 | 'environments' => [ 42 | 'staging', 43 | 'production', 44 | ], 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Round robin. 49 | |-------------------------------------------------------------------------- 50 | | 51 | | This will rotate through the list of provided CDNs each time a call 52 | | to cdn() is made. 53 | | 54 | */ 55 | 'round_robin' => false, 56 | 57 | /* 58 | |-------------------------------------------------------------------------- 59 | | Command settings. 60 | |-------------------------------------------------------------------------- 61 | | 62 | | This holds the list of defaults to be used with the metrique:cdnify 63 | | command. These options can be changed in this config and also 64 | | overridden by command line flags. 65 | | 66 | */ 67 | 'command' => [ 68 | 'build_source' => '/build', 69 | 'build_dest' => '/build', 70 | 'disk' => 's3', 71 | 'force' => false, 72 | 'skip_gulp' => false, 73 | 'manifest' => '/build/rev-manifest.json', 74 | ], 75 | ]; 76 | --------------------------------------------------------------------------------