├── 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 | [](https://travis-ci.org/markstory/asset_compress)
4 | [](https://codecov.io/github/markstory/asset_compress?branch=master)
5 | [](https://packagist.org/packages/markstory/asset_compress)
6 | [](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 |
--------------------------------------------------------------------------------