├── CHANGELOG.txt ├── LICENSE ├── README.mdown ├── composer.json ├── config ├── asset_compress.local.sample.ini └── asset_compress.sample.ini ├── phpcs.xml ├── phpstan-baseline.neon ├── phpstan.neon ├── src ├── AssetCompressPlugin.php ├── AssetScanner.php ├── Command │ ├── BuildCommand.php │ └── ClearCommand.php ├── Config │ └── ConfigFinder.php ├── Factory.php ├── Filter │ ├── ImportInline.php │ └── Sprockets.php ├── Middleware │ └── AssetCompressMiddleware.php └── View │ └── Helper │ └── AssetCompressHelper.php └── webroot └── js ├── dispatcher.js ├── dispatcher.test.html └── libs.js /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | 3.0.0 Changes 2 | 3 | ### Breaking Changes 4 | 5 | * Dynamic build files and all features related to them have been removed. 6 | Dynamic build files presented a few challenges around maintenance and went 7 | against my goals of creating a fast and simple way to integrate all your 8 | asset pipeline features into one plugin. 9 | * Prefixes on build files have been removed. In previous versions if you 10 | started a build file with `js_` or `css_`, these prefixes would be removed 11 | and used as the file suffix. You must now always include the file extension 12 | in the build target. 13 | * New `inlineCss` and `inlineScript` methods were added to the 14 | AssetCompressHelper. These new methods allow you to inline js/css 15 | on the page. Note that the assets will be rebuilt on each page request. 16 | 17 | ### Other Changes 18 | 19 | * Adopted PSR-2 20 | 21 | See [github releases](https://github.com/markstory/asset_compress/releases) 22 | for changelogs on previous releases. 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2009-2018, Mark Story. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all 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 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.mdown: -------------------------------------------------------------------------------- 1 | # Asset Compress 2 | 3 | [![Build Status](https://travis-ci.org/markstory/asset_compress.svg?branch=master)](https://travis-ci.org/markstory/asset_compress) 4 | [![codecov.io](https://codecov.io/github/markstory/asset_compress/coverage.svg?branch=master)](https://codecov.io/github/markstory/asset_compress?branch=master) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/markstory/asset_compress.svg?style=flat-square)](https://packagist.org/packages/markstory/asset_compress) 6 | [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE) 7 | 8 | 9 | Asset Compress is CakePHP plugin for helping reduce the number of requests, and optimizing the remaining requests your application makes for Javascript and CSS files. 10 | 11 | ### Features 12 | 13 | * Development mode builder that rebuilds assets on each request. 14 | * Command line build tool to compile static assets for deployment. 15 | * Built-in support for LESScss, Sass and CoffeeScript, as well as several 16 | minifiers. 17 | * Powerful and flexible filter system allowing you to add your own 18 | minifiers/pre-processors. 19 | * Simple configuration file. 20 | * Incremental builds that don't recompile assets when they don't need to 21 | be. 22 | 23 | ## Installing 24 | 25 | Add this plugin to your application with composer: 26 | 27 | php composer.phar require markstory/asset_compress 28 | 29 | Then make sure you load the plugin: 30 | 31 | // in src/Application.php 32 | // in the bootstrap() method add 33 | $this->addPlugin('AssetCompress'); 34 | 35 | Copy the `config/asset_compress.sample.ini` from the plugin to your app's 36 | `config/asset_compress.ini`. From there read the [wiki](http://github.com/markstory/asset_compress/wiki) 37 | for more information. 38 | 39 | ## Documentation 40 | 41 | Documentation for AssetCompress is available on the [github wiki pages](http://github.com/markstory/asset_compress/wiki) 42 | 43 | ## Issues 44 | 45 | Please report any issues you have with the plugin to the [issue tracker](http://github.com/markstory/asset_compress/issues) on github. 46 | 47 | ## License 48 | 49 | Asset Compress is offered under an [MIT license](http://www.opensource.org/licenses/mit-license.php). 50 | 51 | ### Authors 52 | 53 | See the [github contributors list](https://github.com/markstory/asset_compress/graphs/contributors) 54 | 55 | ### Changelog 56 | 57 | See CHANGELOG for changes only available on `master`. See 58 | [github releases](https://github.com/markstory/asset_compress/releases) for changelogs on previous releases. 59 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markstory/asset_compress", 3 | "description": "An asset compression plugin for CakePHP. Provides file concatenation and a flexible filter system for preprocessing and minification.", 4 | "type": "cakephp-plugin", 5 | "keywords": ["cakephp", "assets", "minifier", "less", "coffee-script", "sass"], 6 | "homepage": "https://github.com/markstory/asset_compress", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Mark Story", 11 | "homepage": "http://mark-story.com", 12 | "role": "Author" 13 | } 14 | ], 15 | "support": { 16 | "issues": "https://github.com/markstory/asset_compress/issues", 17 | "source": "https://github.com/markstory/asset_compress" 18 | }, 19 | "require": { 20 | "php": ">=8.1.0", 21 | "cakephp/cakephp": "^5.0", 22 | "markstory/mini-asset": "^2.0", 23 | "psr/http-server-handler": "^1.0", 24 | "psr/http-server-middleware": "^1.0" 25 | }, 26 | "require-dev": { 27 | "cakephp/cakephp-codesniffer": "^5.0", 28 | "phpunit/phpunit": "^10.1.0" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "AssetCompress\\": "src" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "AssetCompress\\Test\\": "tests", 38 | "TestApp\\": "tests/test_files/src" 39 | } 40 | }, 41 | "suggest": { 42 | "natxet/CssMin": "For using the CssMin filter.", 43 | "328/jsqueeze": "For using the JSqueeze filter.", 44 | "patchwork/jshrink": "For using the JShrink filter.", 45 | "scssphp/scssphp": "For using the ScssPHP filter.", 46 | "leafo/lessphp": "For using the LessPHP filter." 47 | }, 48 | "scripts": { 49 | "check": [ 50 | "@cs-check", 51 | "@stan", 52 | "@test" 53 | ], 54 | "cs-check": "phpcs -p src/ tests/TestCase/", 55 | "cs-fix": "phpcbf src/ tests/TestCase/", 56 | "test": "phpunit --stderr", 57 | "stan": "phpstan analyse src/ && psalm --show-info=false", 58 | "stan-test": "phpstan analyse tests/", 59 | "stan-setup": "cp composer.json composer.backup && composer require --dev phpstan/phpstan:~2.0 && mv composer.backup composer.json", 60 | "coverage-test": "phpunit --stderr --coverage-clover=clover.xml" 61 | }, 62 | "prefer-stable": true, 63 | "config": { 64 | "allow-plugins": { 65 | "dealerdirect/phpcodesniffer-composer-installer": true, 66 | "composer/package-versions-deprecated": true 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /config/asset_compress.local.sample.ini: -------------------------------------------------------------------------------- 1 | ; Local settings to support the use of AssetCompress in different installations 2 | ; You can use any of the configuration available to override original asset_compress.ini values 3 | 4 | ; General section will entirely override original settings 5 | [General] 6 | cacheConfig = true 7 | alwaysEnableController = true 8 | 9 | ; Any filter section will entirely override previously defined if any 10 | ; E.g. you can set your local filters paths 11 | [filter_CssMinFilter] 12 | path = /my/local/path/to/cssmin 13 | 14 | [filter_LessCss] 15 | node = /my/local/path/to/node 16 | node_modules = /my/local/path/to/node_modules 17 | 18 | ; Extension section will be merged with previously defined 19 | ; Array values like files[] will override any defined ones 20 | [js] 21 | baseUrl = http://cdn.example.com 22 | -------------------------------------------------------------------------------- /config/asset_compress.sample.ini: -------------------------------------------------------------------------------- 1 | ; General settings control basic behavior of the plugin 2 | ; 3 | ; * cacheConfig - set to true to cache the parsed configuration data 4 | ; so it doesn't get parsed on each request. 5 | ; 6 | ; * alwaysEnableController - Set to true to always enable the 7 | ; AssetsController. Generally you will want to disable the controller 8 | ; in production, as it could allow an attacker to request expensive 9 | ; resources repeatedly. However, if you need the controller available 10 | ; in production. You can enable this flag. 11 | ; 12 | ; * themes - Define which plugins are themes. Theme plugins will be scanned 13 | ; for asset files when building targets containing themed files. 14 | ; 15 | [General] 16 | cacheConfig = false 17 | alwaysEnableController = false 18 | themes[] = Red 19 | themes[] = Modern 20 | 21 | ; Define an extension type. 22 | ; 23 | ; _filters, _targets and other keys prefixed with this value 24 | ; are connected when the ini file is parsed. 25 | ; 26 | ; * cachePath - is where built files will be output 27 | ; * timestamp - Set to true to add a timestamp to build files. 28 | ; * paths - An array of paths where files used in builds can be found 29 | ; Supports glob expressions. 30 | ; * filters - A list of filters to be applied to all build files 31 | ; using this extension. 32 | ; * baseUrl - Set the base url this type of asset is served off of, good 33 | ; for using with CDN's 34 | [js] 35 | baseUrl = http://cdn.example.com 36 | timestamp = true 37 | paths[] = WEBROOT/js/* 38 | cachePath = WEBROOT/cache_js 39 | filters[] = Sprockets 40 | filters[] = YuiJs 41 | 42 | ; Each target should have a section defining the files 43 | ; everything after js_* is considered the build file. 44 | ; all files included in the build are relative to the parent 45 | ; paths key. 46 | ; 47 | ; targets can include their own filters. 48 | [libs.js] 49 | files[] = jquery.js 50 | files[] = mootools.js 51 | files[] = class.js 52 | filters[] = Uglifyjs 53 | 54 | ; Create the CSS extension 55 | [css] 56 | paths[] = WEBROOT/css/* 57 | cachePath = WEBROOT/cache_css 58 | 59 | [all.css] 60 | files[] = layout.css 61 | filters[] = CssMinFilter 62 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | tests/test_files/js/* 8 | tests/test_files/css/* 9 | 10 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: "#^Method AssetCompress\\\\Filter\\\\ImportInline\\:\\:scanner\\(\\) should return AssetCompress\\\\AssetScanner but returns MiniAsset\\\\AssetScanner\\.$#" 5 | count: 1 6 | path: src/Filter/ImportInline.php 7 | 8 | - 9 | message: "#^Method AssetCompress\\\\Filter\\\\Sprockets\\:\\:_scanner\\(\\) should return AssetCompress\\\\AssetScanner but returns MiniAsset\\\\AssetScanner\\.$#" 10 | count: 1 11 | path: src/Filter/Sprockets.php 12 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-baseline.neon 3 | 4 | parameters: 5 | level: 7 6 | bootstrapFiles: 7 | - tests/bootstrap.php 8 | paths: 9 | - src/ 10 | ignoreErrors: 11 | - identifier: missingType.iterableValue 12 | -------------------------------------------------------------------------------- /src/AssetCompressPlugin.php: -------------------------------------------------------------------------------- 1 | insertAfter(ErrorHandlerMiddleware::class, $middleware); 32 | 33 | return $middlewareQueue; 34 | } 35 | 36 | /** 37 | * Console hook 38 | * 39 | * @param \Cake\Console\CommandCollection $commands The command collection 40 | * @return \Cake\Console\CommandCollection 41 | */ 42 | public function console(CommandCollection $commands): CommandCollection 43 | { 44 | $commands->add('asset_compress build', BuildCommand::class); 45 | $commands->add('asset_compress clear', ClearCommand::class); 46 | 47 | return $commands; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/AssetScanner.php: -------------------------------------------------------------------------------- 1 | theme = $theme; 39 | parent::__construct($paths); 40 | } 41 | 42 | /** 43 | * Resolve a plugin or theme path into the file path without the search paths. 44 | * 45 | * @param string $path Path to resolve 46 | * @return string resolved path 47 | */ 48 | protected function _expandPrefix(string $path): string 49 | { 50 | if (preg_match(self::PLUGIN_PATTERN, $path)) { 51 | return $this->_expandPlugin($path); 52 | } 53 | if ($this->theme && preg_match(self::THEME_PATTERN, $path)) { 54 | return $this->_expandTheme($path); 55 | } 56 | 57 | return $path; 58 | } 59 | 60 | /** 61 | * Resolve a themed file to its full path. The file will be found on the 62 | * current theme's path. 63 | * 64 | * @param string $file The theme file to find. 65 | * @return string The expanded path 66 | */ 67 | protected function _expandTheme(string $file): string 68 | { 69 | $file = preg_replace(self::THEME_PATTERN, '', $file); 70 | 71 | return CorePlugin::path($this->theme) . 'webroot' . DS . $file; 72 | } 73 | 74 | /** 75 | * Resolve a plugin file to its full path. 76 | * 77 | * @param string $file The theme file to find. 78 | * @throws \RuntimeException when plugins are missing. 79 | * @return string The expanded path 80 | */ 81 | protected function _expandPlugin(string $file): string 82 | { 83 | preg_match(self::PLUGIN_PATTERN, $file, $matches); 84 | if (empty($matches[1]) || empty($matches[2])) { 85 | throw new RuntimeException('Missing required parameters'); 86 | } 87 | if (!CorePlugin::isLoaded($matches[1])) { 88 | throw new RuntimeException($matches[1] . ' is not a loaded plugin.'); 89 | } 90 | $path = CorePlugin::path($matches[1]); 91 | 92 | return $path . 'webroot' . DS . $matches[2]; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Command/BuildCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Generate files defined in AssetCompress configuration.') 32 | ->addOption('force', [ 33 | 'help' => 'Force assets to rebuild. Ignores timestamp rules.', 34 | 'short' => 'f', 35 | 'boolean' => true, 36 | ]) 37 | ->addOption('config', [ 38 | 'help' => 'The config file to use.', 39 | 'short' => 'c', 40 | 'default' => CONFIG . 'asset_compress.ini', 41 | ]) 42 | ->addOption('skip-plugins', [ 43 | 'help' => 'Don\'t load config files from plugin\'s .', 44 | 'boolean' => true, 45 | ]); 46 | 47 | return $parser; 48 | } 49 | 50 | /** 51 | * Clear built files. 52 | * 53 | * @param \Cake\Console\Arguments $args The command arguments. 54 | * @param \Cake\Console\ConsoleIo $io The console io 55 | * @return int The exit code 56 | */ 57 | public function execute(Arguments $args, ConsoleIo $io): int 58 | { 59 | $configFinder = new ConfigFinder(); 60 | $config = $configFinder->loadAll( 61 | (string)$args->getOption('config'), 62 | (bool)$args->getOption('skip-plugins'), 63 | ); 64 | $factory = new Factory($config); 65 | 66 | $themes = (array)$config->general('themes'); 67 | foreach ($themes as $theme) { 68 | $io->verbose('Building with theme = ' . $theme); 69 | $config->theme($theme); 70 | foreach ($factory->assetCollection() as $target) { 71 | if ($target->isThemed()) { 72 | $this->buildTarget($target, $factory, $args, $io); 73 | } 74 | } 75 | } 76 | $io->verbose('Building un-themed targets.'); 77 | foreach ($factory->assetCollection() as $target) { 78 | $this->buildTarget($target, $factory, $args, $io); 79 | } 80 | 81 | return static::CODE_SUCCESS; 82 | } 83 | 84 | /** 85 | * Generate and save the cached file for a build target. 86 | * 87 | * @param \MiniAsset\AssetTarget $build The build to generate. 88 | * @param \AssetCompress\Factory $factory Assetcompress factory 89 | * @param \Cake\Console\Arguments $args Arguments instance 90 | * @param \Cake\Console\ConsoleIo $io ConsoleIo instance 91 | * @return void 92 | */ 93 | protected function buildTarget(AssetTarget $build, Factory $factory, Arguments $args, ConsoleIo $io): void 94 | { 95 | $writer = $factory->writer(); 96 | $compiler = $factory->compiler(); 97 | 98 | $name = $writer->buildFileName($build); 99 | if ($writer->isFresh($build) && $args->getOption('force') === false) { 100 | $io->out('Skip building ' . $name . ' existing file is still fresh.'); 101 | 102 | return; 103 | } 104 | 105 | $writer->invalidate($build); 106 | $name = $writer->buildFileName($build); 107 | try { 108 | $io->out('Saving file for ' . $name); 109 | $contents = $compiler->generate($build); 110 | $writer->write($build, $contents); 111 | } catch (Exception $e) { 112 | $io->err('Error: ' . $e->getMessage()); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Command/ClearCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Remove files generated by AssetCompress') 32 | ->addOption('config', [ 33 | 'help' => 'The config file to use.', 34 | 'short' => 'c', 35 | 'default' => CONFIG . 'asset_compress.ini', 36 | ]); 37 | 38 | return $parser; 39 | } 40 | 41 | /** 42 | * Clear built files. 43 | * 44 | * @param \Cake\Console\Arguments $args The command arguments. 45 | * @param \Cake\Console\ConsoleIo $io The console io 46 | * @return int The exit code 47 | */ 48 | public function execute(Arguments $args, ConsoleIo $io): int 49 | { 50 | $configFinder = new ConfigFinder(); 51 | $config = $configFinder->loadAll((string)$args->getOption('config')); 52 | $factory = new Factory($config); 53 | 54 | $io->verbose('Clearing build timestamp.'); 55 | $writer = $factory->writer(); 56 | $writer->clearTimestamps(); 57 | 58 | $io->verbose('Clearing build files:'); 59 | $this->clearBuilds($config, $factory, $io); 60 | 61 | $io->verbose(''); 62 | $io->out('Complete'); 63 | 64 | return static::CODE_SUCCESS; 65 | } 66 | 67 | /** 68 | * clear the builds for a specific extension. 69 | * 70 | * @param \MiniAsset\AssetConfig $config The asset configuration. 71 | * @param \AssetCompress\Factory $factory The factory instance 72 | * @param \Cake\Console\ConsoleIo $io Consoleio 73 | * @return void 74 | */ 75 | protected function clearBuilds(AssetConfig $config, Factory $factory, ConsoleIo $io): void 76 | { 77 | $themes = (array)$config->general('themes'); 78 | if ($themes) { 79 | $config->theme($themes[0]); 80 | } 81 | $assets = $factory->assetCollection(); 82 | if (count($assets) === 0) { 83 | $io->err('No build targets defined, skipping'); 84 | 85 | return; 86 | } 87 | 88 | $targets = []; 89 | foreach (iterator_to_array($assets) as $target) { 90 | $this->clearPath($io, $target->outputDir() . DS, $themes, [$target->name()]); 91 | $targets[] = $target->name(); 92 | } 93 | 94 | $this->clearPath($io, CACHE . 'asset_compress' . DS, $themes, $targets); 95 | } 96 | 97 | /** 98 | * Clear a path of build targets. 99 | * 100 | * @param \Cake\Console\ConsoleIo $io The consoleio 101 | * @param string $path The root path to clear. 102 | * @param array $themes The themes to clear. 103 | * @param array $targets The build targets to clear. 104 | * @return void 105 | */ 106 | protected function clearPath(ConsoleIo $io, string $path, array $themes, array $targets): void 107 | { 108 | if (!file_exists($path)) { 109 | return; 110 | } 111 | 112 | $dir = new DirectoryIterator($path); 113 | foreach ($dir as $file) { 114 | $name = $base = $file->getFilename(); 115 | if (in_array($name, ['.', '..'])) { 116 | continue; 117 | } 118 | // timestamped files. 119 | if (preg_match('/^(.*)\.v\d+(\.[a-z]+)$/', $name, $matches)) { 120 | $base = $matches[1] . $matches[2]; 121 | } 122 | // themed files 123 | foreach ($themes as $theme) { 124 | if (strpos($base, $theme) === 0 && strpos($base, '-') !== false) { 125 | [, $base] = explode('-', $base); 126 | } 127 | } 128 | if (in_array($base, $targets)) { 129 | $io->verbose(' - Deleting ' . $path . $name); 130 | unlink($path . $name); 131 | continue; 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Config/ConfigFinder.php: -------------------------------------------------------------------------------- 1 | WWW_ROOT, 61 | ]); 62 | $this->_load($config, $path, '', $skipLocal); 63 | 64 | if ($skipPlugins) { 65 | if ($cache) { 66 | Cache::write('asset_compress_config', $config, $cache); 67 | } 68 | 69 | return $config; 70 | } 71 | 72 | $plugins = Plugin::loaded(); 73 | foreach ($plugins as $plugin) { 74 | $pluginConfig = Plugin::path($plugin) . 'config' . DS . 'asset_compress.ini'; 75 | $this->_load($config, $pluginConfig, $plugin . '.', $skipLocal); 76 | } 77 | 78 | if ($cache) { 79 | Cache::write('asset_compress_config', $config, $cache); 80 | } 81 | 82 | return $config; 83 | } 84 | 85 | /** 86 | * Load a config file and its `.local` file if it exists. 87 | * 88 | * @param \MiniAsset\AssetConfig $config The config object to update. 89 | * @param string $path The config file to load. 90 | * @param string $prefix The prefix to use. 91 | * @param bool $skipLocal Skip *.local.ini file lookup 92 | * @return void 93 | */ 94 | protected function _load(AssetConfig $config, string $path, string $prefix = '', bool $skipLocal = false): void 95 | { 96 | if (file_exists($path)) { 97 | $config->load($path, $prefix); 98 | } 99 | 100 | if ($skipLocal) { 101 | return; 102 | } 103 | 104 | $localConfig = (string)preg_replace('/(.*)\.ini$/', '$1.local.ini', $path); 105 | if (file_exists($localConfig)) { 106 | $config->load($localConfig, $prefix); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Factory.php: -------------------------------------------------------------------------------- 1 | config->get('general.timestampPath') ?: $tmpPath); 32 | } 33 | 34 | /** 35 | * Create a Caching Compiler 36 | * 37 | * @param string $outputDir The directory to output cached files to. 38 | * @param bool $debug Whether or not to enable debugging mode for the compiler. 39 | * @return \MiniAsset\Output\CachedCompiler 40 | */ 41 | public function cachedCompiler(string $outputDir = '', bool $debug = false): CachedCompiler 42 | { 43 | $outputDir = $outputDir ?: CACHE . 'asset_compress' . DS; 44 | $debug = $debug ?: Configure::read('debug'); 45 | 46 | return parent::cachedCompiler($outputDir, $debug); 47 | } 48 | 49 | /** 50 | * Create an AssetCacher 51 | * 52 | * @param string $path The path to read from. Defaults to the application CACHE path. 53 | * @return \MiniAsset\Output\AssetCacher 54 | */ 55 | public function cacher(string $path = ''): AssetCacher 56 | { 57 | if ($path == '') { 58 | $path = CACHE . 'asset_compress' . DS; 59 | } 60 | 61 | return parent::cacher($path); 62 | } 63 | 64 | /** 65 | * Create an AssetScanner 66 | * 67 | * @param array $paths The paths to read from. 68 | * @return \AssetCompress\AssetScanner 69 | */ 70 | public function scanner(array $paths): AssetScanner 71 | { 72 | return new AssetScanner($paths, $this->config->theme()); 73 | } 74 | 75 | /** 76 | * Create a single filter 77 | * 78 | * @param string $name The name of the filter to build. 79 | * @param array $config The configuration for the filter. 80 | * @return \MiniAsset\Filter\FilterInterface 81 | */ 82 | protected function buildFilter(string $name, array $config): FilterInterface 83 | { 84 | $className = App::className($name, 'Filter'); 85 | if (!class_exists((string)$className)) { 86 | $className = App::className('AssetCompress.' . $name, 'Filter'); 87 | } 88 | $className = $className ?: $name; 89 | 90 | return parent::buildFilter($className, $config); 91 | } 92 | 93 | /** 94 | * Create an AssetCompiler 95 | * 96 | * @param bool $debug Not used - Configure is used instead. 97 | * @return \MiniAsset\Output\Compiler 98 | */ 99 | public function compiler(bool $debug = false): Compiler 100 | { 101 | return new Compiler($this->filterRegistry(), Configure::read('debug')); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Filter/ImportInline.php: -------------------------------------------------------------------------------- 1 | scanner)) { 24 | return $this->scanner; 25 | } 26 | $this->scanner = new AssetScanner( 27 | $this->_settings['paths'], 28 | $this->_settings['theme'] ?? null, 29 | ); 30 | 31 | return $this->scanner; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Filter/Sprockets.php: -------------------------------------------------------------------------------- 1 | _scanner)) { 24 | return $this->_scanner; 25 | } 26 | $this->_scanner = new AssetScanner( 27 | $this->_settings['paths'], 28 | $this->_settings['theme'] ?? null, 29 | ); 30 | 31 | return $this->_scanner; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Middleware/AssetCompressMiddleware.php: -------------------------------------------------------------------------------- 1 | loadAll(); 42 | } 43 | $this->config = $config; 44 | } 45 | 46 | /** 47 | * Callable implementation for the middleware stack. 48 | * 49 | * @param \Psr\Http\Message\ServerRequestInterface $request The request. 50 | * @param \Psr\Http\Server\RequestHandlerInterface $handler The request handler. 51 | * @return \Psr\Http\Message\ResponseInterface A response. 52 | */ 53 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 54 | { 55 | $config = $this->config; 56 | $production = !Configure::read('debug'); 57 | if ($production && !$config->general('alwaysEnableController')) { 58 | return $handler->handle($request); 59 | } 60 | 61 | // Make sure the request looks like an asset. 62 | $targetName = $this->getName($config, $request->getUri()->getPath()); 63 | if (!is_string($targetName)) { 64 | return $handler->handle($request); 65 | } 66 | 67 | $queryParams = $request->getQueryParams(); 68 | if (isset($queryParams['theme'])) { 69 | $config->theme($queryParams['theme']); 70 | } 71 | $factory = new Factory($config); 72 | $assets = $factory->assetCollection(); 73 | if (!$assets->contains($targetName)) { 74 | return $handler->handle($request); 75 | } 76 | $build = $assets->get($targetName); 77 | 78 | try { 79 | $compiler = $factory->cachedCompiler(); 80 | $contents = $compiler->generate($build); 81 | } catch (Exception $e) { 82 | throw new NotFoundException($e->getMessage()); 83 | } 84 | 85 | return $this->respond($contents, $build); 86 | } 87 | 88 | /** 89 | * Respond with the asset. 90 | * 91 | * @param string $contents The asset contents. 92 | * @param \MiniAsset\AssetTarget $build The build target. 93 | * @return \Psr\Http\Message\ResponseInterface 94 | */ 95 | protected function respond(string $contents, AssetTarget $build): ResponseInterface 96 | { 97 | $response = new Response(); 98 | 99 | // Deliver built asset. 100 | $body = $response->getBody(); 101 | $body->write($contents); 102 | $body->rewind(); 103 | 104 | return $response->withHeader('Content-Type', $this->mapType($build)); 105 | } 106 | 107 | /** 108 | * Map an extension to a content type 109 | * 110 | * @param \MiniAsset\AssetTarget $build The build target. 111 | * @return string The mapped content type. 112 | */ 113 | protected function mapType(AssetTarget $build): string 114 | { 115 | $ext = $build->ext(); 116 | $types = [ 117 | 'css' => 'text/css', 118 | 'js' => 'application/javascript', 119 | 'svg' => 'image/svg+xml', 120 | ]; 121 | 122 | return $types[$ext] ?? 'application/octet-stream'; 123 | } 124 | 125 | /** 126 | * Returns the build name for a requested asset 127 | * 128 | * @param \MiniAsset\AssetConfig $config The config object to use. 129 | * @param string $url The url to get an asset name from. 130 | * @return string|false false if no build can be parsed from URL 131 | * with url path otherwise 132 | */ 133 | protected function getName(AssetConfig $config, string $url): bool|string 134 | { 135 | $parts = explode('.', $url); 136 | if (count($parts) < 2) { 137 | return false; 138 | } 139 | 140 | $path = $config->cachePath($parts[count($parts) - 1]); 141 | if (empty($path)) { 142 | return false; 143 | } 144 | 145 | $root = str_replace('\\', '/', WWW_ROOT); 146 | $path = str_replace('\\', '/', $path); 147 | $path = str_replace($root, '', $path); 148 | $path = '/' . ltrim($path, '/'); 149 | if (strpos($url, $path) !== 0) { 150 | return false; 151 | } 152 | 153 | return str_replace($path, '', $url); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/View/Helper/AssetCompressHelper.php: -------------------------------------------------------------------------------- 1 | 39 | */ 40 | protected array $_defaultConfig = [ 41 | 'skipPlugins' => false, 42 | 'skipLocal' => false, 43 | 'configPath' => CONFIG . 'asset_compress.ini', 44 | 'noconfig' => false, 45 | ]; 46 | 47 | /** 48 | * Configuration object 49 | * 50 | * @var \MiniAsset\AssetConfig 51 | */ 52 | protected AssetConfig $config; 53 | 54 | /** 55 | * Factory for other AssetCompress objects. 56 | * 57 | * @var \AssetCompress\Factory 58 | */ 59 | protected Factory $factory; 60 | 61 | /** 62 | * AssetCollection for the current config set. 63 | * 64 | * @var \MiniAsset\AssetCollection 65 | */ 66 | protected AssetCollection $collection; 67 | 68 | /** 69 | * AssetWriter instance 70 | * 71 | * @var \MiniAsset\Output\AssetWriter 72 | */ 73 | protected AssetWriter $writer; 74 | 75 | /** 76 | * Constructor - finds and parses the ini file the plugin uses. 77 | * 78 | * @param \Cake\View\View $view The view instance to use. 79 | * @param array $config The settings for the helper. 80 | * @return void 81 | */ 82 | public function __construct(View $view, array $config = []) 83 | { 84 | parent::__construct($view, $config); 85 | if (!$this->getConfig('noconfig')) { 86 | $skipPlugins = $this->getConfig('skipPlugins'); 87 | $skipLocal = $this->getConfig('skipLocal'); 88 | $configFinder = new ConfigFinder(); 89 | $this->assetConfig( 90 | $configFinder->loadAll($this->getConfig('configPath'), $skipPlugins, $skipLocal), 91 | ); 92 | } 93 | } 94 | 95 | /** 96 | * Modify the runtime configuration of the helper. 97 | * Used as a get/set for the ini file values. 98 | * 99 | * @param \MiniAsset\AssetConfig $config The config instance to set. 100 | * @return \MiniAsset\AssetConfig|null Either the current config object or null. 101 | */ 102 | public function assetConfig(?AssetConfig $config = null): ?AssetConfig 103 | { 104 | if ($config === null) { 105 | return $this->config; 106 | } 107 | $this->config = $config; 108 | 109 | return null; 110 | } 111 | 112 | /** 113 | * Get the AssetCompress factory based on the config object. 114 | * 115 | * @return \AssetCompress\Factory 116 | */ 117 | protected function factory(): Factory 118 | { 119 | if (empty($this->factory)) { 120 | $this->config->theme($this->getView()->getTheme()); 121 | $this->factory = new Factory($this->config); 122 | } 123 | 124 | return $this->factory; 125 | } 126 | 127 | /** 128 | * Get the AssetCollection 129 | * 130 | * @return \MiniAsset\AssetCollection 131 | */ 132 | protected function collection(): AssetCollection 133 | { 134 | if (empty($this->collection)) { 135 | $this->collection = $this->factory()->assetCollection(); 136 | } 137 | 138 | return $this->collection; 139 | } 140 | 141 | /** 142 | * Get the AssetWriter 143 | * 144 | * @return \MiniAsset\Output\AssetWriter 145 | */ 146 | protected function writer(): AssetWriter 147 | { 148 | if (empty($this->writer)) { 149 | $this->writer = $this->factory()->writer(); 150 | } 151 | 152 | return $this->writer; 153 | } 154 | 155 | /** 156 | * Adds an extension if the file doesn't already end with it. 157 | * 158 | * @param string $file Filename 159 | * @param string $ext Extension with . 160 | * @return string 161 | */ 162 | protected function _addExt(string $file, string $ext): string 163 | { 164 | if (substr($file, strlen($ext) * -1) !== $ext) { 165 | $file .= $ext; 166 | } 167 | 168 | return $file; 169 | } 170 | 171 | /** 172 | * Create a CSS file. Will generate link tags 173 | * for either the dynamic build controller, or the generated file if it exists. 174 | * 175 | * To create build files without configuration use addCss() 176 | * 177 | * Options: 178 | * 179 | * - All options supported by HtmlHelper::css() are supported. 180 | * - `raw` - Set to true to get one link element for each file in the build. 181 | * 182 | * @param string $file A build target to include. 183 | * @param array $options An array of options for the stylesheet tag. 184 | * @throws \RuntimeException 185 | * @return ?string A stylesheet tag 186 | */ 187 | public function css(string $file, array $options = []): ?string 188 | { 189 | $file = $this->_addExt($file, '.css'); 190 | if (!$this->collection()->contains($file)) { 191 | throw new RuntimeException( 192 | "Cannot create a stylesheet tag for a '$file'. That build is not defined.", 193 | ); 194 | } 195 | $output = ''; 196 | if (!empty($options['raw'])) { 197 | unset($options['raw']); 198 | $target = $this->collection()->get($file); 199 | foreach ($target->files() as $part) { 200 | $path = $this->_relativizePath($part->path()); 201 | $path = str_replace(DS, '/', $path); 202 | $output .= $this->Html->css($path, $options); 203 | } 204 | 205 | return $output; 206 | } 207 | 208 | $url = $this->url($file, $options); 209 | unset($options['full']); 210 | 211 | return $this->Html->css($url, $options); 212 | } 213 | 214 | /** 215 | * Create a script tag for a script asset. Will generate script tags 216 | * for either the dynamic build controller, or the generated file if it exists. 217 | * 218 | * To create build files without configuration use addScript() 219 | * 220 | * Options: 221 | * 222 | * - All options supported by HtmlHelper::css() are supported. 223 | * - `raw` - Set to true to get one script element for each file in the build. 224 | * 225 | * @param string $file A build target to include. 226 | * @param array $options An array of options for the script tag. 227 | * @throws \RuntimeException 228 | * @return ?string A script tag 229 | */ 230 | public function script(string $file, array $options = []): ?string 231 | { 232 | $file = $this->_addExt($file, '.js'); 233 | if (!$this->collection()->contains($file)) { 234 | throw new RuntimeException( 235 | "Cannot create a script tag for a '$file'. That build is not defined.", 236 | ); 237 | } 238 | $output = ''; 239 | if (!empty($options['raw'])) { 240 | unset($options['raw']); 241 | $target = $this->collection()->get($file); 242 | foreach ($target->files() as $part) { 243 | $path = $this->_relativizePath($part->path()); 244 | $path = str_replace(DS, '/', $path); 245 | $output .= $this->Html->script($path, $options); 246 | } 247 | 248 | return $output; 249 | } 250 | 251 | $url = $this->url($file, $options); 252 | unset($options['full']); 253 | 254 | return $this->Html->script($url, $options); 255 | } 256 | 257 | /** 258 | * Converts an absolute path into a web relative one. 259 | * 260 | * @param string $path The path to convert 261 | * @return string A webroot relative string. 262 | */ 263 | protected function _relativizePath(string $path): string 264 | { 265 | $plugins = Plugin::loaded(); 266 | $index = array_search('AssetCompress', $plugins); 267 | if ($index !== false) { 268 | unset($plugins[$index]); 269 | } 270 | 271 | foreach ($plugins as $plugin) { 272 | $pluginPath = Plugin::path($plugin) . 'webroot'; 273 | if (strpos($path, $pluginPath) === 0) { 274 | return str_replace($pluginPath, '/' . Inflector::underscore($plugin), $path); 275 | } 276 | } 277 | $path = str_replace(WWW_ROOT, '/', $path); 278 | 279 | return str_replace(DS, '/', $path); 280 | } 281 | 282 | /** 283 | * Get the URL for a given asset name. 284 | * 285 | * Takes an build filename, and returns the URL 286 | * to that build file. 287 | * 288 | * @param string $file The build file that you want a URL for. 289 | * @param array|bool $full Whether or not the URL should have the full base path. 290 | * @return string The generated URL. 291 | * @throws \RuntimeException when the build file does not exist. 292 | */ 293 | public function url(?string $file = null, bool|array $full = false): string 294 | { 295 | $collection = $this->collection(); 296 | if (!$collection->contains($file)) { 297 | throw new RuntimeException('Cannot get URL for build file that does not exist.'); 298 | } 299 | 300 | $options = $full; 301 | if (!is_array($full)) { 302 | $options = ['full' => $full]; 303 | } 304 | /** @var array $options */ 305 | $options += ['full' => false]; 306 | 307 | $target = $collection->get($file); 308 | $type = $target->ext(); 309 | 310 | $config = $this->assetConfig(); 311 | $baseUrl = $config->get($type . '.baseUrl'); 312 | $devMode = Configure::read('debug'); 313 | 314 | // CDN routes. 315 | if ($baseUrl && !$devMode) { 316 | return $baseUrl . $this->_getBuildName($target); 317 | } 318 | 319 | $root = str_replace('\\', '/', WWW_ROOT); 320 | $path = str_replace('\\', '/', $target->outputDir()); 321 | $path = str_replace($root, '/', $path); 322 | 323 | $route = null; 324 | if (!$devMode) { 325 | $path = rtrim($path, '/') . '/'; 326 | $route = $path . $this->_getBuildName($target); 327 | } 328 | if ($devMode || $config->general('alwaysEnableController')) { 329 | $route = $this->_getRoute($target, $path); 330 | } 331 | $route = str_replace(DS, '/', $route); 332 | 333 | if ($options['full']) { 334 | $base = Router::fullBaseUrl(); 335 | 336 | return $base . $route; 337 | } 338 | 339 | return $route; 340 | } 341 | 342 | /** 343 | * Get the build file name. 344 | * 345 | * Generates filenames that are intended for production use 346 | * with statically generated files. 347 | * 348 | * @param \MiniAsset\AssetTarget $build The build being resolved. 349 | * @return string The resolved build name. 350 | */ 351 | protected function _getBuildName(AssetTarget $build): string 352 | { 353 | return $this->writer()->buildFileName($build); 354 | } 355 | 356 | /** 357 | * Get the dynamic build path for an asset. 358 | * 359 | * This generates URLs that work with the development dispatcher filter. 360 | * 361 | * @param \MiniAsset\AssetTarget $file The build file you want to make a url for. 362 | * @param string $base The base path to fetch a url with. 363 | * @return string Generated URL. 364 | */ 365 | protected function _getRoute(AssetTarget $file, string $base): string 366 | { 367 | $query = []; 368 | 369 | if ($file->isThemed()) { 370 | $query['theme'] = $this->getView()->getTheme(); 371 | } 372 | 373 | $base = rtrim($base, '/') . '/'; 374 | $query = empty($query) ? '' : '?' . http_build_query($query); 375 | 376 | return $base . $file->name() . $query; 377 | } 378 | 379 | /** 380 | * Check if a build exists (is defined and have at least one file) in the ini file. 381 | * 382 | * @param string $file Name of the build that will be checked if exists. 383 | * @return bool True if the build file exists. 384 | */ 385 | public function exists(string $file): bool 386 | { 387 | return $this->collection()->contains($file); 388 | } 389 | 390 | /** 391 | * Create a CSS file. Will generate inline style tags 392 | * in production, or reference the dynamic build file in development 393 | * 394 | * To create build files without configuration use addCss() 395 | * 396 | * Options: 397 | * 398 | * - All options supported by HtmlHelper::css() are supported. 399 | * 400 | * @param string $file A build target to include. 401 | * @throws \RuntimeException 402 | * @return string style tag 403 | */ 404 | public function inlineCss(string $file): string 405 | { 406 | $collection = $this->collection(); 407 | if (!$collection->contains($file)) { 408 | throw new RuntimeException('Cannot create a stylesheet for a build that does not exist.'); 409 | } 410 | $compiler = $this->factory()->compiler(); 411 | $results = $compiler->generate($collection->get($file)); 412 | 413 | return $this->Html->tag('style', $results, ['type' => 'text/css']); 414 | } 415 | 416 | /** 417 | * Create an inline script tag for a script asset. Will generate inline script tags 418 | * in production, or reference the dynamic build file in development. 419 | * 420 | * To create build files without configuration use addScript() 421 | * 422 | * Options: 423 | * 424 | * - All options supported by HtmlHelper::css() are supported. 425 | * 426 | * @param string $file A build target to include. 427 | * @throws \RuntimeException 428 | * @return string script tag 429 | */ 430 | public function inlineScript(string $file): string 431 | { 432 | $collection = $this->collection(); 433 | if (!$collection->contains($file)) { 434 | throw new RuntimeException('Cannot create a script tag for a build that does not exist.'); 435 | } 436 | $compiler = $this->factory()->compiler(); 437 | $results = $compiler->generate($collection->get($file)); 438 | 439 | return $this->Html->tag('script', $results); 440 | } 441 | } 442 | -------------------------------------------------------------------------------- /webroot/js/dispatcher.js: -------------------------------------------------------------------------------- 1 | /* 2 | Fire Application events. Used to trigger logic for blocks of your application's Javascript 3 | When combined with the automatic javascript includer on the server you can call 4 | 5 | App.Dispatcher.dispatch('users/index'); 6 | 7 | This will fire: 8 | 9 | - App.users.beforeAction (if it exists) 10 | - App.users.index (if it exists) 11 | 12 | Will return the value from the called action or return false if the method was not found. 13 | */ 14 | App.Dispatcher = function () { 15 | var PATH_SEPARATOR = '/'; 16 | 17 | return { 18 | dispatch: function (url) { 19 | var params = this._parseUrl(url); 20 | if (App[params.controller] === undefined || App[params.controller][params.action] === undefined) { 21 | return false; 22 | } 23 | if (typeof App[params.controller].beforeAction == 'function') { 24 | App[params.controller].beforeAction(params); 25 | } 26 | return App[params.controller][params.action](params); 27 | }, 28 | 29 | _parseUrl: function (url) { 30 | var params = {}; 31 | var urlParts = url.split(PATH_SEPARATOR); 32 | if (urlParts.length == 1) { 33 | urlParts[1] = 'index'; 34 | } 35 | params.controller = urlParts[0]; 36 | params.action = urlParts[1]; 37 | return params; 38 | } 39 | } 40 | }(); 41 | 42 | /* 43 | Used to safely declare a controller namespace, so js files for actions can safely create their 44 | controller object. 45 | 46 | Example: 47 | 48 | App.makeController('users'); 49 | App.users.edit = { 50 | ... 51 | }; 52 | 53 | */ 54 | App.makeController = function (name) { 55 | if (this[name] === undef) { 56 | this[name] = {}; 57 | return this[name]; 58 | } 59 | return this[name]; 60 | }; 61 | -------------------------------------------------------------------------------- /webroot/js/dispatcher.test.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | Dispatcher.js test 7 | 8 | 9 | 10 | 11 | 12 | 13 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /webroot/js/libs.js: -------------------------------------------------------------------------------- 1 | /* 2 | License: 3 | MIT-style license. 4 | */ 5 | if (window.AssetCompress == undefined) { 6 | window.AssetCompress = {}; 7 | } 8 | if (window.basePath == undefined) { 9 | window.basePath = '/'; 10 | } 11 | 12 | // Set the url used to load additional js class files. 13 | if (AssetCompress.url == undefined) { 14 | AssetCompress.classUrl = window.basePath + 'asset_compress/js_files/get/' 15 | } 16 | 17 | /* 18 | Load class/resource files from the compressor. 19 | Will check window[name] for classes to help prevent duplicates from being loaded. 20 | 21 | Loads the files asynchrnonously by appending script tabs to If the last 22 | argument is a function it will be called once the file has completed loading. 23 | 24 | Example 25 | 26 | App.load('Template', 'OtherClass', function () { alert('files loaded'); }); 27 | 28 | Will load Template, and OtherClass through the asset_compressor and fire the function when complete. 29 | */ 30 | AssetCompress.load = function () { 31 | 32 | function _appendScript(filename, callback) { 33 | var head = document.getElementsByTagName("head")[0]; 34 | var script = document.createElement("script"); 35 | script.src = filename; 36 | var done = false; 37 | 38 | script.onload = script.onreadystatechange = function () { 39 | if (!done && (!this.readyState || this.readyState == "loaded" || this.readyState == "complete") ) { 40 | done = true; 41 | callback(); 42 | } 43 | } 44 | head.appendChild(script); 45 | } 46 | 47 | var args = Array.prototype.slice.call(arguments), 48 | readyCallback = function () {}, 49 | buildName = [], 50 | i, className, 51 | filename; 52 | 53 | if (typeof args[args.length - 1] == 'function') { 54 | readyCallback = args.pop(); 55 | } 56 | 57 | for (i = args.length; i--;) { 58 | className = args[i]; 59 | buildName.push(className); 60 | if (window[className] !== undefined) { 61 | delete args[i]; 62 | } 63 | } 64 | filename = AssetCompress.classUrl + 65 | AssetCompress.underscore(buildName.reverse().join('')) + '.js' + 66 | '?file[]=' + args.join('&file[]='); 67 | 68 | _appendScript(filename, readyCallback); 69 | }; 70 | 71 | AssetCompress.underscore = function (camelCased) { 72 | return camelCased.replace(/([A-Z])(?=[a-z0-9])/g, '_$1', '_\1').toLowerCase().substring(1); 73 | } 74 | --------------------------------------------------------------------------------