From a67ccfe993a6c34abc420d01bdd1cc9332b18cae Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Fri, 16 Aug 2019 16:19:16 +0800 Subject: [PATCH] Allow decompiled Backend JS assets (#4549) This change will allow the individual JS assets that are compiled into a full compilation file to be loaded individually instead, allowing the developer to see their changes immediately. It introduces a new configuration variable, `cms.decompileBackendAssets`, that controls this functionality. By default, it is false and not tied to the debug value, requiring it to be explicitly enabled. --- config/develop.php | 24 ++++++++ modules/backend/helpers/Backend.php | 56 +++++++++++++++++++ .../helpers/exception/DecompileException.php | 7 +++ modules/backend/lang/en/lang.php | 3 +- modules/backend/layouts/_head.htm | 23 ++++++-- modules/system/reportwidgets/Status.php | 4 ++ tests/fixtures/backend/assets/compilation.js | 5 ++ tests/fixtures/backend/assets/js/file1.js | 1 + tests/fixtures/backend/assets/js/file2.js | 1 + .../backend/assets/not-compilation.js | 1 + .../backend/helpers/BackendHelperTest.php | 33 +++++++++++ 11 files changed, 151 insertions(+), 7 deletions(-) create mode 100644 config/develop.php create mode 100644 modules/backend/helpers/exception/DecompileException.php create mode 100644 tests/fixtures/backend/assets/compilation.js create mode 100644 tests/fixtures/backend/assets/js/file1.js create mode 100644 tests/fixtures/backend/assets/js/file2.js create mode 100644 tests/fixtures/backend/assets/not-compilation.js create mode 100644 tests/unit/backend/helpers/BackendHelperTest.php diff --git a/config/develop.php b/config/develop.php new file mode 100644 index 000000000..cd4aee7d7 --- /dev/null +++ b/config/develop.php @@ -0,0 +1,24 @@ + false, + +]; diff --git a/modules/backend/helpers/Backend.php b/modules/backend/helpers/Backend.php index 9708c6b40..5d86cda82 100644 --- a/modules/backend/helpers/Backend.php +++ b/modules/backend/helpers/Backend.php @@ -8,6 +8,7 @@ use Redirect; use October\Rain\Router\Helper as RouterHelper; use System\Helpers\DateTime as DateTimeHelper; use Backend\Classes\Skin; +use Backend\Helpers\Exception\DecompileException; /** * Backend Helper @@ -156,4 +157,59 @@ class Backend return ''.e($defaultValue).''.PHP_EOL; } + + /** + * Decompiles the compilation asset files + * + * This is used to load each individual asset file, as opposed to using the compilation assets. This is useful only + * for development, to allow developers to test changes without having to re-compile assets. + * + * @param string $file The compilation asset file to decompile + * @param boolean $skinAsset If true, will load decompiled assets from the "skins" directory. + * @throws DecompileException If the compilation file cannot be decompiled + * @return array + */ + public function decompileAsset(string $file, bool $skinAsset = false) + { + if ($skinAsset) { + $assetFile = base_path(substr(Skin::getActive()->getPath($file, true), 1)); + } else { + $assetFile = base_path($file); + } + + if (!file_exists($assetFile)) { + throw new DecompileException('File ' . $file . ' does not exist to be decompiled.'); + } + if (!is_readable($assetFile)) { + throw new DecompileException('File ' . $file . ' cannot be decompiled. Please allow read access to the file.'); + } + + $contents = file_get_contents($assetFile); + + if (!preg_match('/^=require/m', $contents)) { + throw new DecompileException('File ' . $file . ' does not appear to be a compiled asset.'); + } + + // Find all assets that are compiled in this file + preg_match_all('/^=require\s+([A-z0-9-_+\.\/]+)$/m', $contents, $matches, PREG_SET_ORDER); + + if (!count($matches)) { + throw new DecompileException('Unable to extract any asset paths when decompiled file ' . $file . '.'); + } + + // Determine correct asset path + $directory = str_replace(basename($file), '', $file); + + return array_map(function ($match) use ($directory, $skinAsset) { + // Resolve relative asset paths + if ($skinAsset) { + $assetPath = base_path(substr(Skin::getActive()->getPath($directory . $match[1], true), 1)); + } else { + $assetPath = base_path($directory . $match[1]); + } + $realPath = str_replace(base_path(), '', realpath($assetPath)); + + return Url::asset($realPath); + }, $matches); + } } diff --git a/modules/backend/helpers/exception/DecompileException.php b/modules/backend/helpers/exception/DecompileException.php new file mode 100644 index 000000000..08e620c20 --- /dev/null +++ b/modules/backend/helpers/exception/DecompileException.php @@ -0,0 +1,7 @@ + 'Directory :name or its subdirectories is not writable for PHP. Please set corresponding permissions for the webserver on this directory.', 'extension' => 'The PHP extension :name is not installed. Please install this library and activate the extension.', 'plugin_missing' => 'The plugin :name is a dependency but is not installed. Please install this plugin.', - 'debug' => 'Debug mode is enabled. This is not recommended for production installations.' + 'debug' => 'Debug mode is enabled. This is not recommended for production installations.', + 'decompileBackendAssets' => 'Assets in the Backend are currently decompiled. This is not recommended for production installations.', ], 'editor' => [ 'menu_label' => 'Editor settings', diff --git a/modules/backend/layouts/_head.htm b/modules/backend/layouts/_head.htm index 68033f741..822cdae83 100644 --- a/modules/backend/layouts/_head.htm +++ b/modules/backend/layouts/_head.htm @@ -13,20 +13,31 @@ @@ -49,11 +60,11 @@ // Unregister all service workers before signing in to prevent cache issues, see github issue: #3707 navigator.serviceWorker.getRegistrations().then( function(registrations) { - for (let registration of registrations) { + for (let registration of registrations) { registration.unregister(); } }); - } + } diff --git a/modules/system/reportwidgets/Status.php b/modules/system/reportwidgets/Status.php index ea5570334..38810d561 100644 --- a/modules/system/reportwidgets/Status.php +++ b/modules/system/reportwidgets/Status.php @@ -103,6 +103,10 @@ class Status extends ReportWidgetBase $warnings[] = Lang::get('backend::lang.warnings.debug'); } + if (Config::get('develop.decompileBackendAssets', false)) { + $warnings[] = Lang::get('backend::lang.warnings.decompileBackendAssets'); + } + $requiredExtensions = [ 'GD' => extension_loaded('gd'), 'fileinfo' => extension_loaded('fileinfo'), diff --git a/tests/fixtures/backend/assets/compilation.js b/tests/fixtures/backend/assets/compilation.js new file mode 100644 index 000000000..3d4f4fa57 --- /dev/null +++ b/tests/fixtures/backend/assets/compilation.js @@ -0,0 +1,5 @@ +/* Comments + +=require js/file1.js +=require js/file2.js +*/ diff --git a/tests/fixtures/backend/assets/js/file1.js b/tests/fixtures/backend/assets/js/file1.js new file mode 100644 index 000000000..8fffa07fb --- /dev/null +++ b/tests/fixtures/backend/assets/js/file1.js @@ -0,0 +1 @@ +console.log('Test File 1'); diff --git a/tests/fixtures/backend/assets/js/file2.js b/tests/fixtures/backend/assets/js/file2.js new file mode 100644 index 000000000..7d129a4b8 --- /dev/null +++ b/tests/fixtures/backend/assets/js/file2.js @@ -0,0 +1 @@ +console.log('Test File 2'); diff --git a/tests/fixtures/backend/assets/not-compilation.js b/tests/fixtures/backend/assets/not-compilation.js new file mode 100644 index 000000000..40238d60d --- /dev/null +++ b/tests/fixtures/backend/assets/not-compilation.js @@ -0,0 +1 @@ +console.log('Legitimate file'); diff --git a/tests/unit/backend/helpers/BackendHelperTest.php b/tests/unit/backend/helpers/BackendHelperTest.php new file mode 100644 index 000000000..61752a3c0 --- /dev/null +++ b/tests/unit/backend/helpers/BackendHelperTest.php @@ -0,0 +1,33 @@ +decompileAsset('tests/fixtures/backend/assets/compilation.js'); + + $this->assertCount(2, $assets); + $this->assertContains('file1.js', $assets[0]); + $this->assertContains('file2.js', $assets[1]); + } + + public function testDecompileMissingFile() + { + $this->expectException(DecompileException::class); + + $backendHelper = new Backend; + $assets = $backendHelper->decompileAsset('tests/fixtures/backend/assets/missing.js'); + } + + public function testDecompileNonCompilationFile() + { + $this->expectException(DecompileException::class); + + $backendHelper = new Backend; + $assets = $backendHelper->decompileAsset('tests/fixtures/backend/assets/not-compilation.js'); + } +}