diff --git a/CHANGELOG.md b/CHANGELOG.md index 04e88091a..b43e1dcb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,10 @@ # v1.7.27 ## mm/dd/2022 -1. [](#improved) +1. [](#new) + * Added support for generic `assets.link()` for external references. No pipeline support. + * Added support for `assets.addJSmodule()` with full pipeline support. +2. [](#improved) * Improved `Utils::download()` method to allow overrides on download name, mime and expires header * Improved `onPageFallBackUrl` event 3. [](#bugfix) diff --git a/system/blueprints/config/system.yaml b/system/blueprints/config/system.yaml index aff54b46c..c5427d125 100644 --- a/system/blueprints/config/system.yaml +++ b/system/blueprints/config/system.yaml @@ -992,6 +992,39 @@ form: validate: type: bool + assets.js_module_pipeline: + type: toggle + label: PLUGIN_ADMIN.JAVASCRIPT_MODULE_PIPELINE + help: PLUGIN_ADMIN.JAVASCRIPT_MODULE_PIPELINE_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + assets.js_module_pipeline_include_externals: + type: toggle + label: PLUGIN_ADMIN.JAVASCRIPT_MODULE_PIPELINE_INCLUDE_EXTERNALS + help: PLUGIN_ADMIN.JAVASCRIPT_MODULE_PIPELINE_INCLUDE_EXTERNALS_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + assets.js_module_pipeline_before_excludes: + type: toggle + label: PLUGIN_ADMIN.JAVASCRIPT_MODULE_PIPELINE_BEFORE_EXCLUDES + help: PLUGIN_ADMIN.JAVASCRIPT_MODULE_PIPELINE_BEFORE_EXCLUDES_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + assets.js_minify: type: toggle label: PLUGIN_ADMIN.JAVASCRIPT_MINIFY diff --git a/system/config/system.yaml b/system/config/system.yaml index 60abcee34..2de075b26 100644 --- a/system/config/system.yaml +++ b/system/config/system.yaml @@ -127,6 +127,9 @@ assets: # Configuration for Assets Mana js_pipeline: false # The JS pipeline is the unification of multiple JS resources into one file js_pipeline_include_externals: true # Include external URLs in the pipeline by default js_pipeline_before_excludes: true # Render the pipeline before any excluded files + js_module_pipeline: false # The JS Module pipeline is the unification of multiple JS Module resources into one file + js_module_pipeline_include_externals: true # Include external URLs in the pipeline by default + js_module_pipeline_before_excludes: true # Render the pipeline before any excluded files js_minify: true # Minify the JS during pipelining enable_asset_timestamp: false # Enable asset timestamps enable_asset_sri: false # Enable asset SRI diff --git a/system/src/Grav/Common/Assets.php b/system/src/Grav/Common/Assets.php index 36dc79dd0..2956ea45d 100644 --- a/system/src/Grav/Common/Assets.php +++ b/system/src/Grav/Common/Assets.php @@ -30,14 +30,21 @@ class Assets extends PropertyObject use TestingAssetsTrait; use LegacyAssetsTrait; + const LINK = 'link'; const CSS = 'css'; const JS = 'js'; + const JS_MODULE = 'js_module'; + const LINK_COLLECTION = 'assets_link'; const CSS_COLLECTION = 'assets_css'; const JS_COLLECTION = 'assets_js'; + const JS_MODULE_COLLECTION = 'assets_js_module'; + const LINK_TYPE = Assets\Link::class; const CSS_TYPE = Assets\Css::class; const JS_TYPE = Assets\Js::class; + const JS_MODULE_TYPE = Assets\JsModule::class; const INLINE_CSS_TYPE = Assets\InlineCss::class; const INLINE_JS_TYPE = Assets\InlineJs::class; + const INLINE_JS_MODULE_TYPE = Assets\InlineJsModule::class; /** @const Regex to match CSS and JavaScript files */ const DEFAULT_REGEX = '/.\.(css|js)$/i'; @@ -48,15 +55,16 @@ class Assets extends PropertyObject /** @const Regex to match JavaScript files */ const JS_REGEX = '/.\.js$/i'; + /** @const Regex to match JavaScriptModyle files */ + const JS_MODULE_REGEX = '/.\.mjs$/i'; + /** @var string */ protected $assets_dir; /** @var string */ protected $assets_url; /** @var array */ - protected $assets_css = []; - /** @var array */ - protected $assets_js = []; + protected $assets= []; // Following variables come from the configuration: /** @var bool */ @@ -66,19 +74,17 @@ class Assets extends PropertyObject /** @var bool */ protected $css_pipeline_before_excludes; /** @var bool */ - protected $inlinecss_pipeline_include_externals; - /** @var bool */ - protected $inlinecss_pipeline_before_excludes; - /** @var bool */ protected $js_pipeline; /** @var bool */ protected $js_pipeline_include_externals; /** @var bool */ protected $js_pipeline_before_excludes; /** @var bool */ - protected $inlinejs_pipeline_include_externals; + protected $js_module_pipeline; /** @var bool */ - protected $inlinejs_pipeline_before_excludes; + protected $js_module_pipeline_include_externals; + /** @var bool */ + protected $js_module_pipeline_before_excludes; /** @var array */ protected $pipeline_options = []; @@ -113,6 +119,12 @@ class Assets extends PropertyObject $this->assets_dir = $locator->findResource('asset://'); $this->assets_url = $locator->findResource('asset://', false); + // Initialize asset collection storage + $this->assets[self::LINK_COLLECTION] = []; + $this->assets[self::CSS_COLLECTION] = []; + $this->assets[self::JS_COLLECTION] = []; + $this->assets[self::JS_MODULE_COLLECTION] = []; + $this->config($asset_config); // Register any preconfigured collections @@ -193,6 +205,8 @@ class Assets extends PropertyObject call_user_func_array([$this, 'addCss'], $args); } elseif ($extension === 'js') { call_user_func_array([$this, 'addJs'], $args); + } elseif ($extension === 'mjs') { + call_user_func_array([$this, 'addJsModule'], $args); } } } @@ -222,7 +236,7 @@ class Assets extends PropertyObject return $this; } - if (($type === $this::CSS_TYPE || $type === $this::JS_TYPE) && isset($this->collections[$asset])) { + if ($this->isValidType($type) && isset($this->collections[$asset])) { $this->addType($collection, $type, $this->collections[$asset], $options); return $this; } @@ -230,7 +244,9 @@ class Assets extends PropertyObject // If pipeline disabled, set to position if provided, else after if (isset($options['pipeline'])) { if ($options['pipeline'] === false) { - $exclude_type = ($type === $this::JS_TYPE || $type === $this::INLINE_JS_TYPE) ? $this::JS : $this::CSS; + + $exclude_type = $this->getBaseType($type); + $excludes = strtolower($exclude_type . '_pipeline_before_excludes'); if ($this->{$excludes}) { $default = 'after'; @@ -263,12 +279,22 @@ class Assets extends PropertyObject // If exists if ($asset_object->init($asset, $options)) { - $this->$collection[md5($asset)] = $asset_object; + $this->assets[$collection][md5($asset)] = $asset_object; } return $this; } + /** + * Add a CSS asset or a collection of assets. + * + * @return $this + */ + public function addLink($asset) + { + return $this->addType($this::LINK_COLLECTION, $this::LINK_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::LINK_TYPE)); + } + /** * Add a CSS asset or a collection of assets. * @@ -309,6 +335,25 @@ class Assets extends PropertyObject return $this->addType($this::JS_COLLECTION, $this::INLINE_JS_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::INLINE_JS_TYPE)); } + /** + * Add a JS asset or a collection of assets. + * + * @return $this + */ + public function addJsModule($asset) + { + return $this->addType($this::JS_MODULE_COLLECTION, $this::JS_MODULE_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::JS_MODULE_TYPE)); + } + + /** + * Add an Inline JS asset or a collection of assets. + * + * @return $this + */ + public function addInlineJsModule($asset) + { + return $this->addType($this::JS_MODULE_COLLECTION, $this::INLINE_JS_MODULE_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::INLINE_JS_MODULE_TYPE)); + } /** * Add/replace collection. @@ -390,17 +435,17 @@ class Assets extends PropertyObject $pipeline_output = ''; $after_output = ''; - $assets = 'assets_' . $type; + $collection = 'assets_' . $type; $pipeline_enabled = $type . '_pipeline'; $render_pipeline = 'render' . ucfirst($type); - $group_assets = $this->filterAssets($this->$assets, 'group', $group); + $group_assets = $this->filterAssets($this->assets[$collection], 'group', $group); $pipeline_assets = $this->filterAssets($group_assets, 'position', 'pipeline', true); $before_assets = $this->filterAssets($group_assets, 'position', 'before', true); $after_assets = $this->filterAssets($group_assets, 'position', 'after', true); // Pipeline - if ($this->{$pipeline_enabled}) { + if ($this->{$pipeline_enabled} ?? false) { $options = array_merge($this->pipeline_options, ['timestamp' => $this->timestamp]); $pipeline = new Pipeline($options); @@ -432,9 +477,29 @@ class Assets extends PropertyObject * @param array $attributes * @return string */ - public function css($group = 'head', $attributes = []) + public function css($group = 'head', $attributes = [], $include_link = true) { - return $this->render('css', $group, $attributes); + $output = ''; + + if ($include_link) { + $output = $this->link($group, $attributes); + } + + $output .= $this->render(self::CSS, $group, $attributes); + + return $output; + } + + /** + * Build the CSS link tags. + * + * @param string $group name of the group + * @param array $attributes + * @return string + */ + public function link($group = 'head', $attributes = []) + { + return $this->render(self::LINK, $group, $attributes); } /** @@ -444,8 +509,58 @@ class Assets extends PropertyObject * @param array $attributes * @return string */ - public function js($group = 'head', $attributes = []) + public function js($group = 'head', $attributes = [], $include_js_module = true) { - return $this->render('js', $group, $attributes); + $output = $this->render(self::JS, $group, $attributes); + + if ($include_js_module) { + $output .= $this->jsModule($group, $attributes); + } + + return $output; + } + + /** + * Build the Javascript Modules tags + * + * @param $group + * @param $attributes + * @return string + */ + public function jsModule($group = 'head', $attributes = []) + { + return $this->render(self::JS_MODULE, $group, $attributes); + } + + public function all($group = 'head', $attributes = []) + { + $output = $this->css($group, $attributes, false); + $output .= $this->link($group, $attributes); + $output .= $this->js($group, $attributes, false); + $output .= $this->jsModule($group, $attributes); + return $output; + } + + protected function isValidType($type) + { + return in_array($type, [self::CSS_TYPE, self::JS_TYPE, self::JS_MODULE_TYPE]); + } + + protected function getBaseType($type) + { + switch ($type) { + case $this::JS_TYPE: + case $this::INLINE_JS_TYPE: + $base_type = $this::JS; + break; + case $this::JS_MODULE_TYPE: + case $this::INLINE_JS_MODULE_TYPE: + $base_type = $this::JS_MODULE; + break; + default: + $base_type = $this::CSS; + } + + return $base_type; } } diff --git a/system/src/Grav/Common/Assets/BaseAsset.php b/system/src/Grav/Common/Assets/BaseAsset.php index 21d9eef63..4f74f55f9 100644 --- a/system/src/Grav/Common/Assets/BaseAsset.php +++ b/system/src/Grav/Common/Assets/BaseAsset.php @@ -26,8 +26,9 @@ abstract class BaseAsset extends PropertyObject { use AssetUtilsTrait; - protected const CSS_ASSET = true; - protected const JS_ASSET = false; + protected const CSS_ASSET = 1; + protected const JS_ASSET = 2; + protected const JS_MODULE_ASSET = 3; /** @var string|false */ protected $asset; @@ -69,7 +70,7 @@ abstract class BaseAsset extends PropertyObject * @param array $elements * @param string|null $key */ - public function __construct(array $elements = [], $key = null) + public function __construct(array $elements = [], ?string $key = null) { $base_config = [ 'group' => 'head', diff --git a/system/src/Grav/Common/Assets/Css.php b/system/src/Grav/Common/Assets/Css.php index 701c94235..ea6b388fa 100644 --- a/system/src/Grav/Common/Assets/Css.php +++ b/system/src/Grav/Common/Assets/Css.php @@ -22,7 +22,7 @@ class Css extends BaseAsset * @param array $elements * @param string|null $key */ - public function __construct(array $elements = [], $key = null) + public function __construct(array $elements = [], ?string $key = null) { $base_options = [ 'asset_type' => 'css', diff --git a/system/src/Grav/Common/Assets/InlineCss.php b/system/src/Grav/Common/Assets/InlineCss.php index 943cef6b6..4984db4d1 100644 --- a/system/src/Grav/Common/Assets/InlineCss.php +++ b/system/src/Grav/Common/Assets/InlineCss.php @@ -22,7 +22,7 @@ class InlineCss extends BaseAsset * @param array $elements * @param string|null $key */ - public function __construct(array $elements = [], $key = null) + public function __construct(array $elements = [], ?string $key = null) { $base_options = [ 'asset_type' => 'css', diff --git a/system/src/Grav/Common/Assets/InlineJs.php b/system/src/Grav/Common/Assets/InlineJs.php index 9ad365574..e38a51aee 100644 --- a/system/src/Grav/Common/Assets/InlineJs.php +++ b/system/src/Grav/Common/Assets/InlineJs.php @@ -22,7 +22,7 @@ class InlineJs extends BaseAsset * @param array $elements * @param string|null $key */ - public function __construct(array $elements = [], $key = null) + public function __construct(array $elements = [], ?string $key = null) { $base_options = [ 'asset_type' => 'js', diff --git a/system/src/Grav/Common/Assets/InlineJsModule.php b/system/src/Grav/Common/Assets/InlineJsModule.php new file mode 100644 index 000000000..42ce6f14a --- /dev/null +++ b/system/src/Grav/Common/Assets/InlineJsModule.php @@ -0,0 +1,46 @@ + 'js_module', + 'attributes' => ['type' => 'module'], + 'position' => 'after' + ]; + + $merged_attributes = Utils::arrayMergeRecursiveUnique($base_options, $elements); + + parent::__construct($merged_attributes, $key); + } + + /** + * @return string + */ + public function render() + { + return 'renderAttributes(). ">\n" . trim($this->asset) . "\n\n"; + } + +} diff --git a/system/src/Grav/Common/Assets/Js.php b/system/src/Grav/Common/Assets/Js.php index fb67491f3..8687a86b1 100644 --- a/system/src/Grav/Common/Assets/Js.php +++ b/system/src/Grav/Common/Assets/Js.php @@ -22,7 +22,7 @@ class Js extends BaseAsset * @param array $elements * @param string|null $key */ - public function __construct(array $elements = [], $key = null) + public function __construct(array $elements = [], ?string $key = null) { $base_options = [ 'asset_type' => 'js', diff --git a/system/src/Grav/Common/Assets/JsModule.php b/system/src/Grav/Common/Assets/JsModule.php new file mode 100644 index 000000000..5c2a836c2 --- /dev/null +++ b/system/src/Grav/Common/Assets/JsModule.php @@ -0,0 +1,49 @@ + 'js_module', + 'attributes' => ['type' => 'module'] + ]; + + $merged_attributes = Utils::arrayMergeRecursiveUnique($base_options, $elements); + + parent::__construct($merged_attributes, $key); + } + + /** + * @return string + */ + public function render() + { + if (isset($this->attributes['loading']) && $this->attributes['loading'] === 'inline') { + $buffer = $this->gatherLinks([$this], self::JS_MODULE_ASSET); + return 'renderAttributes() . ">\n" . trim($buffer) . "\n\n"; + } + + return '\n"; + } +} diff --git a/system/src/Grav/Common/Assets/Link.php b/system/src/Grav/Common/Assets/Link.php new file mode 100644 index 000000000..ecafcea90 --- /dev/null +++ b/system/src/Grav/Common/Assets/Link.php @@ -0,0 +1,43 @@ + 'link', + ]; + + $merged_attributes = Utils::arrayMergeRecursiveUnique($base_options, $elements); + + parent::__construct($merged_attributes, $key); + } + + /** + * @return string + */ + public function render() + { + return 'renderAttributes() . $this->integrityHash($this->asset) . ">\n"; + } +} diff --git a/system/src/Grav/Common/Assets/Pipeline.php b/system/src/Grav/Common/Assets/Pipeline.php index a41e924a5..4e7348942 100644 --- a/system/src/Grav/Common/Assets/Pipeline.php +++ b/system/src/Grav/Common/Assets/Pipeline.php @@ -29,8 +29,9 @@ class Pipeline extends PropertyObject { use AssetUtilsTrait; - protected const CSS_ASSET = true; - protected const JS_ASSET = false; + protected const CSS_ASSET = 1; + protected const JS_ASSET = 2; + protected const JS_MODULE_ASSET = 3; /** @const Regex to match CSS urls */ protected const CSS_URL_REGEX = '{url\(([\'\"]?)(.*?)\1\)}'; @@ -169,7 +170,7 @@ class Pipeline extends PropertyObject * @param array $attributes * @return bool|string URL or generated content if available, else false */ - public function renderJs($assets, $group, $attributes = []) + public function renderJs($assets, $group, $attributes = [], $type = self::JS_ASSET) { // temporary list of assets to pipeline $inline_group = false; @@ -198,7 +199,7 @@ class Pipeline extends PropertyObject } // Concatenate files - $buffer = $this->gatherLinks($assets, self::JS_ASSET); + $buffer = $this->gatherLinks($assets, $type); // Minify if required if ($this->shouldMinify('js')) { @@ -223,6 +224,19 @@ class Pipeline extends PropertyObject return $output; } + /** + * Minify and concatenate JS files. + * + * @param array $assets + * @param string $group + * @param array $attributes + * @return bool|string URL or generated content if available, else false + */ + public function renderJs_Module($assets, $group, $attributes = []) + { + $attributes['type'] = 'module'; + return $this->renderJs($assets, $group, $attributes, self::JS_MODULE_ASSET); + } /** * Finds relative CSS urls() and rewrites the URL with an absolute one diff --git a/system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php b/system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php index ec00a6079..4e8524cce 100644 --- a/system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php +++ b/system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php @@ -30,7 +30,7 @@ trait TestingAssetsTrait */ public function exists($asset) { - return isset($this->collections[$asset]) || isset($this->assets_css[$asset]) || isset($this->assets_js[$asset]); + return isset($this->collections[$asset]) || isset($this->assets[self::CSS_COLLECTION][$asset]) || isset($this->assets[self::JS_COLLECTION][$asset]); } /** @@ -69,10 +69,10 @@ trait TestingAssetsTrait if (null !== $key) { $asset_key = md5($key); - return $this->assets_css[$asset_key] ?? null; + return $this->assets[self::CSS_COLLECTION][$asset_key] ?? null; } - return $this->assets_css; + return $this->assets[self::CSS_COLLECTION]; } /** @@ -88,10 +88,10 @@ trait TestingAssetsTrait if (null !== $key) { $asset_key = md5($key); - return $this->assets_js[$asset_key] ?? null; + return $this->assets[self::JS_COLLECTION][$asset_key] ?? null; } - return $this->assets_js; + return $this->assets[self::JS_COLLECTION]; } /** @@ -102,7 +102,7 @@ trait TestingAssetsTrait */ public function setCss($css) { - $this->assets_css = $css; + $this->assets[self::CSS_COLLECTION] = $css; return $this; } @@ -115,7 +115,7 @@ trait TestingAssetsTrait */ public function setJs($js) { - $this->assets_js = $js; + $this->assets[self::JS_COLLECTION] = $js; return $this; } @@ -129,8 +129,8 @@ trait TestingAssetsTrait public function removeCss($key) { $asset_key = md5($key); - if (isset($this->assets_css[$asset_key])) { - unset($this->assets_css[$asset_key]); + if (isset($this->assets[self::CSS_COLLECTION][$asset_key])) { + unset($this->assets[self::CSS_COLLECTION][$asset_key]); } return $this; @@ -145,8 +145,8 @@ trait TestingAssetsTrait public function removeJs($key) { $asset_key = md5($key); - if (isset($this->assets_js[$asset_key])) { - unset($this->assets_js[$asset_key]); + if (isset($this->assets[self::JS_COLLECTION][$asset_key])) { + unset($this->assets[self::JS_COLLECTION][$asset_key]); } return $this; @@ -201,7 +201,7 @@ trait TestingAssetsTrait */ public function resetJs() { - $this->assets_js = []; + $this->assets[self::JS_COLLECTION] = []; return $this; } @@ -213,7 +213,7 @@ trait TestingAssetsTrait */ public function resetCss() { - $this->assets_css = []; + $this->assets[self::CSS_COLLECTION] = []; return $this; } diff --git a/user/config/system.yaml b/user/config/system.yaml index 7a8ffe58c..2e992778d 100644 --- a/user/config/system.yaml +++ b/user/config/system.yaml @@ -29,6 +29,7 @@ assets: css_minify: true css_rewrite: true js_pipeline: false + js_module_pipeline: false js_minify: true errors: