This commit is contained in:
2024-05-20 15:37:46 +03:00
commit 00b7dbd0b7
10404 changed files with 3285853 additions and 0 deletions

View File

@ -0,0 +1,358 @@
<?php
namespace Sphere\Debloat;
use Sphere\Debloat\OptimizeJs\Script;
/**
* JS Optimizations such as defer and delay.
*
* @author asadkn
* @since 1.0.0
*/
class OptimizeJs
{
protected $html;
/**
* @var array
*/
protected $scripts = [];
/**
* Include and exclude scripts for "delay".
*
* @var array
*/
protected $include_scripts = [];
protected $exclude_scripts = [];
/**
* Exclude scripts from defer.
*/
protected $exclude_defer = [];
/**
* Scripts already delayed or deffered.
*/
protected $done_delay = [];
protected $done_defer = [];
/**
* Scripts registered data from core.
*/
protected $enqueues = [];
public function __construct(string $html)
{
$this->html = $html;
}
public function process()
{
$this->find_scripts();
// Figure out valid scripts.
$this->setup_valid_scripts();
do_action('debloat/optimize_js_begin', $this);
// Early First pass to setup valid parents for child dependency checks.
array_map(function($script) {
$this->should_defer_script($script);
}, $this->scripts);
/**
* Setup scripts defer and delays and render the replacements.
*/
$has_delayed = false;
foreach ($this->scripts as $script) {
// Has to be done for all scripts.
if (Plugin::options()->minify_js) {
$this->minify_js($script);
$script->render = true;
}
// Defer script if not already deferred.
if ($this->should_defer_script($script)) {
$script->defer = true;
$script->render = true;
$has_delayed = true;
}
// Should this script be delayed.
if ($this->should_delay_script($script)) {
$script->delay = true;
$script->render = true;
$has_delayed = true;
}
else {
Util\debug_log('Skipping: ' . print_r($script, true));
}
if ($script->render) {
$this->html = str_replace($script->orig_html, $script->render(), $this->html);
}
}
if ($has_delayed) {
Plugin::delay_load()->enable(true);
}
return $this->html;
}
public function find_scripts()
{
/**
* Collect all valid enqueues.
*/
$all_scripts = [];
foreach (wp_scripts()->registered as $handle => $script) {
// Not an enqueued script, ignore.
if (!in_array($handle, wp_scripts()->done)) {
continue;
}
// Don't mutate original.
$script = clone $script;
$script->deps = array_map(function($id) {
// jQuery JS should be mapped to core. Add -js suffix for others.
return $id === 'jquery' ? 'jquery-core-js' : $id . '-js';
}, $script->deps);
// Add -js prefix to match the IDs that will retrieved from attrs.
$handle .= '-js';
$all_scripts[$handle] = $script;
// Pseudo entry for extras, adding a dependency.
if (isset($script->extra['after'])) {
$all_scripts[$handle . '-after'] = (object) [
'deps' => [$handle]
];
}
}
$this->enqueues = $all_scripts;
/**
* Find all scripts.
*/
if (!preg_match_all('#<script.*?</script>#si', $this->html, $matches)) {
return;
}
foreach ($matches[0] as $script) {
$script = Script::from_tag($script);
if ($script) {
$script->deps = $this->enqueues[$script->id]->deps ?? [];
// Note: There can be multiple scripts with same URL or id. So we don't use $script->id.
$this->scripts[] = $script;
// Inline script has jQuery content? Ensure dependency.
if (
!$script->url &&
!in_array('jquery-core-js', $script->deps) &&
strpos($script->content, 'jQuery') !== false
) {
$script->deps[] = 'jquery-core-js';
}
}
}
}
/**
* Check if the script has a parent dependency that is delayed/deferred.
*
* @param Script $script
* @param array $valid
* @return boolean
*/
public function check_dependency(Script $script, array $valid)
{
// Check if one of parent dependencies is delayed/deferred.
foreach ((array) $script->deps as $dep) {
if (isset($valid[$dep])) {
return true;
}
}
// For translations, if wp-i18n-js is valid, so should be translations.
if (preg_match('/-js-translations/', $script->id, $matches)) {
if (isset($valid['wp-i18n-js'])) {
return true;
}
}
// Special case: Inline script with a parent dep. If parent is true, so is child.
// Note: 'before' and 'extra' isn't accounted for and is not usually needed. Since 'extra' is
// usually localization and 'before' has to happen before anyways.
if (preg_match('/(.+?-js)-(before|after)/', $script->id, $matches)) {
$handle = $matches[1];
// Parent was valid, so is the current child.
if (isset($valid[$handle])) {
return true;
}
}
return false;
}
/**
* Should the script be delayed using one of the JS delay methods.
*
* @param Script $script
* @return boolean
*/
public function should_delay_script(Script $script)
{
if (!Plugin::options()->delay_js) {
return false;
}
// Excludes should be handled before includes and parents check.
foreach ($this->exclude_scripts as $exclude) {
if (Util\asset_match($exclude, $script, 'orig_html')) {
return false;
}
}
// Delay all.
if (Plugin::options()->delay_js_all) {
return true;
}
if ($this->check_dependency($script, $this->done_delay)) {
$this->done_delay[$script->id] = true;
return true;
}
foreach ($this->include_scripts as $include) {
if (Util\asset_match($include, $script, 'orig_html')) {
$this->done_delay[$script->id] = true;
return true;
}
}
return false;
}
/**
* Should the script be deferred.
*
* @param Script $script
* @return boolean
*/
public function should_defer_script(Script $script)
{
// Defer not enabled.
if (!Plugin::options()->defer_js) {
return false;
}
foreach ($this->exclude_defer as $exclude) {
if (Util\asset_match($exclude, $script, 'orig_html')) {
return false;
}
}
// For inline scripts: By default not deferred, unless child of a deferred.
if (!$script->url) {
if (Plugin::options()->defer_js_inline) {
return true;
}
if ($this->check_dependency($script, $this->done_defer)) {
$this->done_defer[$script->id] = true;
return true;
}
return false;
}
// If defer or async attr already exists on original script.
if ($script->defer || $script->async) {
return false;
}
$this->done_defer[$script->id] = true;
return true;
}
/**
* Setup includes and excludes based on options.
*
* @return void
*/
public function setup_valid_scripts()
{
// Used by both delay and defer.
$shared_excludes = [
// Lazyloads should generally be loaded as early as possible and not deferred.
'lazysizes.js',
'lazyload.js',
'lazysizes.min.js',
'lazyLoadOptions',
'lazyLoadThumb',
];
/**
* Defer scripts.
*/
$defer_excludes = array_merge(
Util\option_to_array(Plugin::options()->defer_js_excludes),
$shared_excludes
);
$defer_excludes[] = 'id:-js-extra';
$this->exclude_defer = apply_filters('debloat/defer_js_excludes', $defer_excludes);
/**
* Delayed load scripts.
*/
$excludes = Util\option_to_array(Plugin::options()->delay_js_excludes);
$excludes = array_merge($excludes, $shared_excludes, [
// Jetpack stats.
'url://stats.wp.com',
'_stq.push',
// WPML browser redirect.
'browser-redirect/app.js',
// Skip -js-extra as it's global variables and localization that shouldn't be delayed.
'id:-js-extra'
]);
$this->exclude_scripts = apply_filters('debloat/delay_js_excludes', $excludes);
$this->include_scripts = array_merge(
$this->include_scripts,
Util\option_to_array(Plugin::options()->delay_js_includes)
);
// Enable delay adsense.
if (Plugin::options()->delay_js_adsense) {
$this->include_scripts[] = 'adsbygoogle.js';
}
$this->include_scripts = apply_filters('debloat/delay_js_includes', $this->include_scripts);
}
/**
* Minify the JS file, if possible.
*
* @param Script $script
* @return void
*/
public function minify_js(Script $script)
{
$minifier = new Minifier($script);
$minifier->process();
}
}

View File

@ -0,0 +1,208 @@
<?php
namespace Sphere\Debloat\OptimizeJs;
use Sphere\Debloat\Util;
use Sphere\Debloat\Base\Asset;
/**
* Class for handling scripts.
*
* @author asadkn
* @since 1.0.0
*/
class Script extends Asset
{
public $defer = false;
public $async = false;
public $delay = false;
/**
* Dependencies to mirror from core script.
*
* @var array
*/
public $deps = [];
/**
* Inner content for inline scripts
*
* @var string
*/
public $content = '';
/**
* @var array
*/
public $attrs = [];
/**
* Original HTML tag.
*/
public $orig_html = '';
/**
* Whether the script is dirty and needs rendering.
*
* @var boolean
*/
public $render = false;
/**
* @var callable|null
*/
protected $minifier;
public function __construct(string $id, string $url = '', string $orig_html = '')
{
$this->id = $id;
$this->url = $url;
$this->orig_url = $url;
$this->orig_html = $orig_html;
if (!$this->id) {
$this->id = md5($this->url ?: uniqid('debloat_script', true));
}
// Has to be processed early as it might have to be searched for things like defer.
$this->process_content();
}
/**
* Factory method: Create an instance provided an HTML tag.
*
* @param string $tag
* @return boolean|self
*/
public static function from_tag(string $tag)
{
$attrs = Util\parse_attrs($tag);
// Only process scripts of type javascript or missing type (assumed JS by browser).
if (isset($attrs['type']) && strpos($attrs['type'], 'javascript') === false) {
return false;
}
$script = new self($attrs['id'] ?? '', $attrs['src'] ?? '', $tag);
// Note: We keep 'id' just in case if there was an original id.
$script->attrs = \array_diff_key($attrs, array_flip(['src', 'defer', 'async']));
if (isset($attrs['defer'])) {
$script->defer = true;
}
if (isset($attrs['async'])) {
$script->async = true;
}
return $script;
}
/**
* Render the HTML.
*
* @return string
*/
public function render()
{
// Add src if available.
if ($this->url) {
$this->attrs += [
'src' => $this->get_content_url(),
'id' => $this->id,
];
}
$this->process_delay();
// Setting local to avoid mutating.
$content = $this->content;
if (!$this->url && $content && is_callable($this->minifier)) {
$content = call_user_func($this->minifier, $content);
}
// Add defer for external scripts. For inline scripts, use data uri in src.
if ($this->defer) {
$this->attrs['defer'] = true;
if (!$this->url) {
$this->attrs['src'] = 'data:text/javascript;base64,' . base64_encode($content);
$content = '';
}
}
if ($this->async) {
$this->attrs['async'] = true;
}
$render_atts = implode(' ', $this->render_attrs($this->attrs));
$new_tag = sprintf(
'<script%1$s>%2$s</script>',
$render_atts ? " $render_atts" : '',
!$this->url ? $content : ''
);
return $new_tag;
}
/**
* Set the inner content, if any.
*
* @return void
*/
public function process_content()
{
if (!$this->url) {
$this->content = preg_replace('#<script[^>]*>(.+?)</script>\s*$#is', '\\1', $this->orig_html);
}
}
/**
* Set a callable minifier function.
*
* @param callable $minifier
* @return void
*/
public function set_minifier($minifier)
{
$this->minifier = $minifier;
}
/**
* Process and add delayed script attributes if needed.
*
* @return void
*/
public function process_delay()
{
if (!$this->delay) {
return;
}
// Defer / async shouldn't be enabled anymore. Delay load logic implies deferred anyways.
$this->defer = false;
$this->async = false;
// Add delay.
$this->attrs['data-debloat-delay'] = 1;
// Normal script with a URL.
if (!empty($this->attrs['src'])) {
$this->attrs['data-src'] = $this->attrs['src'];
}
// Inline script with content.
elseif ($this->content) {
if (!empty($this->attrs['type'])) {
$this->attrs['data-type'] = $this->attrs['type'];
}
$this->attrs['type'] = 'text/debloat-script';
}
unset($this->attrs['src']);
}
}