diff --git a/.github/workflows/WP_5_9.yaml b/.github/workflows/WP_6_6.yaml
similarity index 65%
rename from .github/workflows/WP_5_9.yaml
rename to .github/workflows/WP_6_6.yaml
index d39c56d..a3c67d6 100644
--- a/.github/workflows/WP_5_9.yaml
+++ b/.github/workflows/WP_6_6.yaml
@@ -1,4 +1,4 @@
-name: WP5.9 Test Suite [PHP7.2-8.1]
+name: WP 6.6 [PHP8.0-8.4] Tests
on:
push:
@@ -11,23 +11,22 @@ jobs:
strategy:
matrix:
operating-system: [ubuntu-latest]
- php-versions: [ '7.2', '7.3', '7.4', '8.0', '8.1']
+ php-versions: ['8.0', '8.1', '8.2', '8.3', '8.4']
runs-on: ${{ matrix.operating-system }}
services:
- # Setup MYSQL
mysql-service:
- image: 'mysql:5.7'
+ image: mysql:8.4
env:
MYSQL_ROOT_PASSWORD: 'crab'
MYSQL_DATABASE: pc_core_tests
ports:
- 3306:3306
options: >-
- --health-cmd="mysqladmin ping"
+ --health-cmd="healthcheck.sh --connect || mysqladmin ping -uroot -pcrab"
--health-interval=10s
- --health-timeout=5s
- --health-retries=3
-
+ --health-timeout=10s
+ --health-retries=10
+
steps:
- name: Checkout
uses: actions/checkout@v2
@@ -44,21 +43,21 @@ jobs:
- name: Clear existing composer
run: >
- sudo rm -rf vendor
- && rm -rf composer.lock
+ sudo rm -rf vendor
+ && rm -rf composer.lock
- name: Validate composer.json and composer.lock
run: composer validate
- name: Install dependencies
run: >
- rm -rf composer.lock
+ rm -rf composer.lock
&& composer clearcache
- && composer require php-stubs/wordpress-stubs:5.9.0 --dev --no-update
- && composer require roots/wordpress:5.9.4 --dev --no-update
- && composer require wp-phpunit/wp-phpunit:5.9.4 --dev --no-update
+ && composer require php-stubs/wordpress-stubs:6.6.* --dev --no-update
+ && composer require roots/wordpress:6.6.* --dev --no-update
+ && composer require wp-phpunit/wp-phpunit:6.6.* --dev --no-update
&& composer update --no-cache
-
- - name: Run Tests on WP5.9
+
+ - name: Run Tests on WP6.6
env:
environment_github: true
- run: composer all
\ No newline at end of file
+ run: composer all
diff --git a/.github/workflows/WP_6_1.yaml b/.github/workflows/WP_6_7.yaml
similarity index 64%
rename from .github/workflows/WP_6_1.yaml
rename to .github/workflows/WP_6_7.yaml
index 9d83072..3f6e96f 100644
--- a/.github/workflows/WP_6_1.yaml
+++ b/.github/workflows/WP_6_7.yaml
@@ -1,4 +1,4 @@
-name: WP6.1 [PHP7.2-8.1] Tests
+name: WP 6.7 [PHP8.0-8.4] Tests
on:
push:
@@ -11,23 +11,22 @@ jobs:
strategy:
matrix:
operating-system: [ubuntu-latest]
- php-versions: ['7.2', '7.3', '7.4', '8.0', '8.1']
+ php-versions: ['8.0', '8.1', '8.2', '8.3', '8.4']
runs-on: ${{ matrix.operating-system }}
services:
- # Setup MYSQL
mysql-service:
- image: 'mysql:5.7'
+ image: mysql:8.4
env:
MYSQL_ROOT_PASSWORD: 'crab'
MYSQL_DATABASE: pc_core_tests
ports:
- 3306:3306
options: >-
- --health-cmd="mysqladmin ping"
+ --health-cmd="healthcheck.sh --connect || mysqladmin ping -uroot -pcrab"
--health-interval=10s
- --health-timeout=5s
- --health-retries=3
-
+ --health-timeout=10s
+ --health-retries=10
+
steps:
- name: Checkout
uses: actions/checkout@v2
@@ -44,24 +43,21 @@ jobs:
- name: Clear existing composer
run: >
- sudo rm -rf vendor
- && rm -rf composer.lock
+ sudo rm -rf vendor
+ && rm -rf composer.lock
- name: Validate composer.json and composer.lock
run: composer validate
- name: Install dependencies
run: >
- rm -rf composer.lock
+ rm -rf composer.lock
&& composer clearcache
- && composer require php-stubs/wordpress-stubs:6.1.0 --dev --no-update
- && composer require roots/wordpress:6.* --dev --no-update
+ && composer require php-stubs/wordpress-stubs:6.7.* --dev --no-update
+ && composer require roots/wordpress:6.7.* --dev --no-update
+ && composer require wp-phpunit/wp-phpunit:6.7.* --dev --no-update
&& composer update --no-cache
- - name: Run Tests on Latest Version - WP6.1
+ - name: Run Tests on WP6.7
env:
environment_github: true
run: composer all
-
- - name: Codecov
- run: bash <(curl -s https://codecov.io/bash) -t ${{ secrets.CODECOV_TOKEN }}
-
\ No newline at end of file
diff --git a/.github/workflows/WP_6_0.yaml b/.github/workflows/WP_6_8.yaml
similarity index 62%
rename from .github/workflows/WP_6_0.yaml
rename to .github/workflows/WP_6_8.yaml
index e46f8aa..b8c310a 100644
--- a/.github/workflows/WP_6_0.yaml
+++ b/.github/workflows/WP_6_8.yaml
@@ -1,4 +1,4 @@
-name: WP6.0 Test Suite [PHP7.2-8.1]
+name: WP 6.8 [PHP8.0-8.4] Tests
on:
push:
@@ -11,23 +11,22 @@ jobs:
strategy:
matrix:
operating-system: [ubuntu-latest]
- php-versions: ['7.2', '7.3', '7.4', '8.0', '8.1']
+ php-versions: ['8.0', '8.1', '8.2', '8.3', '8.4']
runs-on: ${{ matrix.operating-system }}
services:
- # Setup MYSQL
mysql-service:
- image: 'mysql:5.7'
+ image: mysql:8.4
env:
MYSQL_ROOT_PASSWORD: 'crab'
MYSQL_DATABASE: pc_core_tests
ports:
- 3306:3306
options: >-
- --health-cmd="mysqladmin ping"
+ --health-cmd="healthcheck.sh --connect || mysqladmin ping -uroot -pcrab"
--health-interval=10s
- --health-timeout=5s
- --health-retries=3
-
+ --health-timeout=10s
+ --health-retries=10
+
steps:
- name: Checkout
uses: actions/checkout@v2
@@ -44,25 +43,21 @@ jobs:
- name: Clear existing composer
run: >
- sudo rm -rf vendor
- && rm -rf composer.lock
+ sudo rm -rf vendor
+ && rm -rf composer.lock
- name: Validate composer.json and composer.lock
run: composer validate
- name: Install dependencies
run: >
- rm -rf composer.lock
+ rm -rf composer.lock
&& composer clearcache
- && composer require php-stubs/wordpress-stubs:6.0.0 --dev --no-update
- && composer require roots/wordpress:6.0.0 --dev --no-update
- && composer require wp-phpunit/wp-phpunit:6.0.0 --dev --no-update
+ && composer require php-stubs/wordpress-stubs:6.8.* --dev --no-update
+ && composer require roots/wordpress:6.8.* --dev --no-update
+ && composer require wp-phpunit/wp-phpunit:6.8.* --dev --no-update
&& composer update --no-cache
- - name: Run Tests on Latest Version - WP6.0
+ - name: Run Tests on WP6.8
env:
environment_github: true
run: composer all
-
- - name: Codecov
- run: bash <(curl -s https://codecov.io/bash) -t ${{ secrets.CODECOV_TOKEN }}
-
\ No newline at end of file
diff --git a/.github/workflows/WP_6_9.yaml b/.github/workflows/WP_6_9.yaml
new file mode 100644
index 0000000..ef22bb4
--- /dev/null
+++ b/.github/workflows/WP_6_9.yaml
@@ -0,0 +1,70 @@
+name: WP 6.9 [PHP8.0-8.4] Tests
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master, develop ]
+
+jobs:
+ build:
+ strategy:
+ matrix:
+ operating-system: [ubuntu-latest]
+ php-versions: ['8.0', '8.1', '8.2', '8.3', '8.4']
+ runs-on: ${{ matrix.operating-system }}
+ services:
+ mysql-service:
+ image: mysql:8.4
+ env:
+ MYSQL_ROOT_PASSWORD: 'crab'
+ MYSQL_DATABASE: pc_core_tests
+ ports:
+ - 3306:3306
+ options: >-
+ --health-cmd="healthcheck.sh --connect || mysqladmin ping -uroot -pcrab"
+ --health-interval=10s
+ --health-timeout=10s
+ --health-retries=10
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-versions }}
+ extensions: mbstring, intl, pcov
+ ini-values: post_max_size=256M, log_errors=1
+ tools: pecl
+
+ - name: Check PHP Version
+ run: php -v
+
+ - name: Clear existing composer
+ run: >
+ sudo rm -rf vendor
+ && rm -rf composer.lock
+ - name: Validate composer.json and composer.lock
+ run: composer validate
+
+ - name: Install dependencies
+ run: >
+ rm -rf composer.lock
+ && composer clearcache
+ && composer require php-stubs/wordpress-stubs:6.9.* --dev --no-update
+ && composer require roots/wordpress:6.9.* --dev --no-update
+ && composer require wp-phpunit/wp-phpunit:6.9.* --dev --no-update
+ && composer update --no-cache
+
+ - name: Run Tests on WP6.9
+ env:
+ environment_github: true
+ run: composer all
+
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v4
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ files: ./clover.xml
+ fail_ci_if_error: false
diff --git a/.gitignore b/.gitignore
index 6801830..c16414e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,5 +5,4 @@ coverage-report
clover.xml
workspace.code-workspace
.phpunit.result.cache
-tests/.env
.vscode
\ No newline at end of file
diff --git a/.scrutinizer.yml b/.scrutinizer.yml
new file mode 100644
index 0000000..f45b34a
--- /dev/null
+++ b/.scrutinizer.yml
@@ -0,0 +1,85 @@
+checks:
+ php:
+ code_rating: true
+ duplication: true
+ fix_php_opening_tag: false
+ remove_php_closing_tag: false
+ one_class_per_file: false
+ side_effects_or_types: false
+ no_mixed_inline_html: false
+ require_braces_around_control_structures: false
+ php5_style_constructor: false
+ no_global_keyword: false
+ avoid_usage_of_logical_operators: false
+ psr2_class_declaration: false
+ no_underscore_prefix_in_properties: false
+ no_underscore_prefix_in_methods: false
+ blank_line_after_namespace_declaration: false
+ single_namespace_per_use: false
+ psr2_switch_declaration: false
+ psr2_control_structure_declaration: false
+ avoid_superglobals: false
+ security_vulnerabilities: false
+ no_exit: false
+
+build:
+ dependencies:
+ override:
+ - 'composer install --no-interaction --prefer-dist'
+ nodes:
+ analysis:
+ project_setup:
+ override:
+ - 'true'
+ tests:
+ override:
+ - php-scrutinizer-run
+
+tools:
+ php_analyzer:
+ enabled: true
+ filter:
+ excluded_paths: ['tests/*', 'docs/*', 'template/*', 'node_modules/*', 'vendor/*']
+ config:
+ checkstyle:
+ enabled: true
+ naming:
+ isser_method_name: ^.*$
+ utility_class_name: ^.*$
+ doc_comment_fixes:
+ enabled: false
+ reflection_fixes:
+ enabled: false
+ use_statement_fixes:
+ enabled: false
+ simplify_boolean_return:
+ enabled: true
+ php_changetracking: true
+ php_cpd: true
+ php_cs_fixer: false
+ php_mess_detector: true
+ php_pdepend: true
+ sensiolabs_security_checker: true
+
+filter:
+ paths:
+ - 'src/*'
+ excluded_paths:
+ - 'tests/*'
+ - 'docs/*'
+ - 'docs-gen/*'
+ - 'node_modules/*'
+ - 'vendor/*'
+ - 'template/*'
+
+coding_style:
+ php:
+ indentation:
+ general:
+ use_tabs: true
+ size: 4
+ spaces:
+ before_parentheses:
+ closure_definition: true
+ around_operators:
+ concatenation: true
diff --git a/README.md b/README.md
index a40a34d..5565743 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,19 @@
# PinkCrab Enqueue #
-[](https://packagist.org/packages/pinkcrab/enqueue)
-[](https://packagist.org/packages/pinkcrab/enqueue)
-[](https://packagist.org/packages/pinkcrab/enqueue)
-[](https://packagist.org/packages/pinkcrab/enqueue)
+[](https://packagist.org/packages/pinkcrab/enqueue)
+[](https://packagist.org/packages/pinkcrab/enqueue)
+[](https://packagist.org/packages/pinkcrab/enqueue)
+[](https://packagist.org/packages/pinkcrab/enqueue)


-[![WP5.9 Test Suite [PHP7.2-8.1]](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_5_9.yaml/badge.svg)](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_5_9.yaml)[![WP6.0 Test Suite [PHP7.2-8.1]](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_6_0.yaml/badge.svg)](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_6_0.yaml)[![WP6.1 [PHP7.2-8.1] Tests](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_6_1.yaml/badge.svg)](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_6_1.yaml)
-[](https://codecov.io/gh/Pink-Crab/Enqueue) [](https://scrutinizer-ci.com/g/Pink-Crab/Enqueue/?branch=master)
-[](https://codeclimate.com/github/Pink-Crab/Enqueue/maintainability)
+
+[![WP 6.6 [PHP8.0-8.4] Tests](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_6_6.yaml/badge.svg)](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_6_6.yaml)
+[![WP 6.7 [PHP8.0-8.4] Tests](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_6_7.yaml/badge.svg)](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_6_7.yaml)
+[![WP 6.8 [PHP8.0-8.4] Tests](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_6_8.yaml/badge.svg)](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_6_8.yaml)
+[![WP 6.9 [PHP8.0-8.4] Tests](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_6_9.yaml/badge.svg)](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_6_9.yaml)
+
+[](https://codecov.io/gh/Pink-Crab/Enqueue)
+[](https://scrutinizer-ci.com/g/Pink-Crab/Enqueue/?branch=master)
The PinkCrab Enqueue class allows for a clean and fluent alternative for enqueuing scripts and styles in WordPress.
@@ -19,7 +24,7 @@ composer require pinkcrab/enqueue
```
## Version ##
-**Release 1.3.0**
+**Release 1.5.0**
@@ -209,28 +214,104 @@ Enqueue::script('my_style')
//
```
### Async & Defer ###
-There is also some shortcuts for making any script or style be deferred or async tagged.
+
+Scripts can be marked as `async` or `defer`. As of **v1.5.0** these forward to
+WordPress's `wp_script_add_data( $handle, 'strategy', … )` API (WP 6.3+), which
+is dependency-chain aware — a deferred script waits for deferred deps to
+evaluate in order, rather than racing them via an unordered HTML attribute.
+
```php
src('http://www.site.com/my-style.css')
- ->async()
+Enqueue::script('my_script')
+ ->src('http://www.site.com/my-scripts.js')
+ ->defer()
->register();
-// Rendered as
-//
+// Rendered as (WP decides the exact attribute form)
+//
```
-or
```php
src('http://www.site.com/my-scripts.js')
- ->defer()
+ ->async()
->register();
+```
-// Rendered as
-//
+Calling `defer()` then `async()` (or vice versa) overwrites the strategy — a
+script can only have one. `defer()` / `async()` on a **style** is a no-op
+(style tags have no strategy concept).
+
+### Script Translations ###
+
+Register the handle for i18n via `wp_set_script_translations()` so strings
+loaded with `wp-i18n` resolve from a `.json` file in the given path.
+
+```php
+src(PLUGIN_BASE_URL . 'assets/js/admin-app.js')
+ ->translations('my-text-domain', PLUGIN_BASE_PATH . 'languages')
+ ->register();
+```
+
+The path is optional — WP falls back to its language directory when omitted.
+Only meaningful on scripts; silent no-op on styles.
+
+### Inline Snippets ###
+
+Append arbitrary inline JavaScript to the registered script handle via
+`with_code()`, which wraps `wp_add_inline_script()`. Multiple calls stack, and
+the optional second argument is the position (`'before'` or `'after'`,
+defaulting to `'after'`).
+
+This is **different** from the existing `inline()` method, which replaces
+the `src` with the whole-file contents and inlines them. `with_code()` keeps
+the script loading normally from its URL and attaches short snippets around it.
+
+```php
+src(PLUGIN_BASE_URL . 'assets/js/app.js')
+ ->with_code('window.PC_CONFIG = { apiKey: "...", nonce: "..." };', 'before')
+ ->with_code('window.PC_APP.boot();') // runs after the script has loaded
+ ->register();
+```
+
+### Inline Styles ###
+
+Same idea for styles — `with_style()` attaches inline CSS to a registered
+style handle via `wp_add_inline_style()`.
+
+```php
+src(PLUGIN_BASE_URL . 'assets/css/theme.css')
+ ->with_style(':root { --brand: #f06; }')
+ ->with_style('.block-editor .is-root-container { padding: 1rem; }')
+ ->register();
+```
+
+### Register Only ###
+
+Register the asset with WP without enqueueing it. Useful when another script
+will reference this one via a dependency array, or when a block's `block.json`
+names the handle — the asset is available but doesn't emit on every page load.
+
+```php
+src(PLUGIN_BASE_URL . 'assets/js/lib.js')
+ ->register_only()
+ ->register();
+
+// Later, another script can depend on it:
+Enqueue::script('my-app')
+ ->src(PLUGIN_BASE_URL . 'assets/js/app.js')
+ ->deps('my-lib')
+ ->register();
```
### Registration ###
@@ -276,19 +357,69 @@ class My_Thingy{
add_action('wp_loaded', [new My_Thingy, 'init']);
```
-## Gutenberg ##
+## Gutenberg / Block Editor ##
+
+### for_block ###
-When registering scripts and styles for use with Gutenberg blocks, it is necessary to only register the assets before `wp_enqueue_scripts` hook is called. To do this, all you need to is set `for_block()`.
+When registering scripts and styles for use with Gutenberg blocks declared in
+PHP, it is necessary to only register the assets — the block itself handles
+the enqueue. `for_block()` flags the Enqueue as register-only.
```php
add_action('init', function(){
- Enqueue::script('my_style')
- ->src('http://www.site.com/my-scripts.js')
- ->defer()
+ Enqueue::script('my_block_script')
+ ->src('http://www.site.com/block-script.js')
->for_block()
->register();
- // Register block as normal
+ // Register block as normal, naming 'my_block_script' in its script handles.
+});
+```
+
+### Block Editor Assets ###
+
+For scripts/styles that should only load inside the WordPress admin block
+editor screens, use `for_block_editor()`. This defers the registration/enqueue
+flow to the `enqueue_block_editor_assets` hook, so you don't need to register
+the hook yourself.
+
+```php
+src(PLUGIN_BASE_URL . 'assets/js/editor.js')
+ ->deps('wp-blocks', 'wp-element', 'wp-i18n')
+ ->translations('my-text-domain')
+ ->for_block_editor()
+ ->register();
+```
+
+### Block Styles ###
+
+Attach a stylesheet to a specific block name via
+`wp_enqueue_block_style()` so the CSS only loads on pages that render the
+block. Use `for_block_style( $block_name )`.
+
+```php
+src(PLUGIN_BASE_URL . 'assets/css/my-block.css')
+ ->ver('1.0.0')
+ ->for_block_style('my-plugin/my-block')
+ ->register();
+```
+
+### Block JSON ###
+
+Register a block type directly from its `block.json` metadata directory. This
+short-circuits the normal script/style registration — `block.json` is the
+source of truth for the block's asset declarations, so the fluent API's
+`src`/`deps`/`ver` aren't used.
+
+```php
+add_action('init', function(){
+ Enqueue::script('placeholder-handle')
+ ->block_json( __DIR__ . '/build/my-block' )
+ ->register();
});
```
@@ -353,10 +484,69 @@ add_action('init', function(){
/**
* Sets the version as last modified file time.
+ * Performs a HEAD request to the src URL via wp_remote_head().
+ *
+ * @return self
+ */
+ public function latest_version(): self
+
+/**
+ * Register the script for i18n via wp_set_script_translations().
+ *
+ * @param string $domain Text domain.
+ * @param string|null $path Optional path to JSON translation files.
+ * @return self
+ */
+ public function translations( string $domain, ?string $path = null ): self
+
+/**
+ * Attach an inline JS snippet to the registered handle via wp_add_inline_script().
+ *
+ * @param string $code JavaScript source.
+ * @param string $position 'before' or 'after'. Default 'after'.
+ * @return self
+ */
+ public function with_code( string $code, string $position = 'after' ): self
+
+/**
+ * Attach an inline CSS snippet to the registered handle via wp_add_inline_style().
+ *
+ * @param string $css CSS source.
+ * @return self
+ */
+ public function with_style( string $css ): self
+
+/**
+ * Register the asset with WP without enqueueing it.
+ *
+ * @param bool $register_only Default true.
+ * @return self
+ */
+ public function register_only( bool $register_only = true ): self
+
+/**
+ * Defer the registration to the enqueue_block_editor_assets hook.
+ *
+ * @param bool $for_block_editor Default true.
+ * @return self
+ */
+ public function for_block_editor( bool $for_block_editor = true ): self
+
+/**
+ * Attach a style to a named block via wp_enqueue_block_style().
+ *
+ * @param string $block_name e.g. 'core/paragraph' or 'my-plugin/my-block'.
+ * @return self
+ */
+ public function for_block_style( string $block_name ): self
+
+/**
+ * Register a block type from a block.json directory.
*
+ * @param string $path Absolute path to the directory containing block.json.
* @return self
*/
- public function lastEditedVersion(): self
+ public function block_json( string $path ): self
/**
* Should the script be called in the footer.
@@ -440,6 +630,8 @@ public function script_type( string $script_type ): self
This obviously can be passed around between different classes/functions
### Changelog ###
+* 1.5.0 - Drop PHP 7.x, require PHP 8.0+. Modernise the tooling chain (PHPStan 2.x at level max, PHPUnit 8|9, WPCS 3.x, phpunit-polyfills widened to include v4, symfony/var-dumper + css-selector + dom-crawler unpinned). Replace the WP 5.9/6.0/6.1 workflows with the WP 6.6-6.9 matrix (PHP 8.0-8.4, `mysql:8.4`) using `codecov/codecov-action@v4`. Suppress the WP 6.8 `wp_is_block_theme` early-call notice in `tests/wp-config.php`. Guard `latest_version()` against null src (avoids PHP 8 TypeError). Narrow `strtotime()` argument when the `Last-Modified` header is returned as a list. Swap the `trigger_error( …, E_USER_DEPRECATED )` in the legacy `lastest_version()` alias for `_deprecated_function()` so WP_UnitTestCase can track the deprecation properly. Drop the `Codeclimate Maintainability` badge from the README. **Swap direct curl for `wp_remote_head()`** in `does_file_exist()` / `latest_version()` so consumers can filter the HTTP layer and tests can mock via `pre_http_request`. **New features:** `translations()` wraps `wp_set_script_translations()`; `with_code()` attaches inline JS via `wp_add_inline_script()`; `with_style()` attaches inline CSS via `wp_add_inline_style()`; `for_block_editor()` defers registration to `enqueue_block_editor_assets`; `for_block_style( $block_name )` wraps `wp_enqueue_block_style()`; `register_only()` registers without enqueueing; `block_json()` wraps `register_block_type_from_metadata()`. **`defer()` / `async()` repurposed** to use the dep-aware `wp_script_add_data( $handle, 'strategy', … )` API (WP 6.3+) instead of splatting attributes through the `script_loader_tag` filter — runtime behaviour unchanged for consumers, rendering now handled by WP core.
+* 1.4.0 - Dev dependency bumps to support WP 6.1.
* 1.3.0 - Updated for php8, includes setting of custom script types, renamed lastest_version() to latest_version() and set deprecation notice.
* 1.2.1 : Now supports block use. If defined for block, scripts and styles will only be registered, not enqueued.
* 1.2.0 : Added in Attribute and Flag support with helpers for Aysnc and Defer
@@ -449,4 +641,4 @@ If you would like to make any suggestions or contributions to this little class,
### WordPress Core Functions ###
This package uses the following wp core functions. To use PHP Scoper, please add the following functions.
-['wp_enqueue_style', 'wp_register_script', 'wp_add_inline_script', 'wp_localize_script', 'wp_enqueue_script']
+['wp_register_style', 'wp_enqueue_style', 'wp_register_script', 'wp_enqueue_script', 'wp_add_inline_script', 'wp_add_inline_style', 'wp_localize_script', 'wp_set_script_translations', 'wp_script_add_data', 'wp_enqueue_block_style', 'register_block_type_from_metadata', 'wp_remote_head', 'wp_remote_retrieve_response_code', 'wp_remote_retrieve_header', 'is_wp_error', '_deprecated_function']
diff --git a/composer.json b/composer.json
index 2022b63..c5e6fe8 100644
--- a/composer.json
+++ b/composer.json
@@ -14,8 +14,7 @@
"autoload": {
"psr-4": {
"PinkCrab\\Enqueue\\": "src"
- },
- "files": []
+ }
},
"autoload-dev": {
"psr-4": {
@@ -23,36 +22,38 @@
}
},
"require-dev": {
- "phpunit/phpunit": "^7.0 || ^8.0",
- "phpstan/phpstan": "1.*",
- "szepeviktor/phpstan-wordpress": "<=1.1.7",
- "php-stubs/wordpress-stubs": "6.* || 5.9.*",
- "roots/wordpress": "6.1.*",
- "wp-phpunit/wp-phpunit": "6.1.*",
- "dealerdirect/phpcodesniffer-composer-installer": "<=1.0.0",
- "wp-coding-standards/wpcs": "<=2.3.0",
- "yoast/phpunit-polyfills": "^0.2.0 || ^1.0.0",
- "symfony/var-dumper": "<=6.2.7",
- "gin0115/wpunit-helpers": "1.1.*",
- "vlucas/phpdotenv": "<=5.5.0",
- "symfony/css-selector": "<=6.2.7",
- "symfony/dom-crawler": "<=6.2.7",
- "pinkcrab/function-constructors": "0.2.*"
+ "phpunit/phpunit": "^8.0 || ^9.0",
+ "phpstan/phpstan": "^2.0",
+ "szepeviktor/phpstan-wordpress": "^2.0",
+ "php-stubs/wordpress-stubs": "6.9.*",
+ "roots/wordpress": "6.9.*",
+ "wp-phpunit/wp-phpunit": "6.9.*",
+ "dealerdirect/phpcodesniffer-composer-installer": "*",
+ "wp-coding-standards/wpcs": "*",
+ "yoast/phpunit-polyfills": "^1.0.0 || ^2.0.0 || ^4.0.0",
+ "symfony/var-dumper": "*",
+ "gin0115/wpunit-helpers": "~1",
+ "vlucas/phpdotenv": "^5.4",
+ "symfony/css-selector": "*",
+ "symfony/dom-crawler": "*",
+ "pinkcrab/function-constructors": "^0.2"
},
"require": {
- "php": ">=7.2.0"
+ "php": ">=8.0.0"
},
"scripts": {
- "test": "phpunit --coverage-clover clover.xml --testdox",
- "coverage": "phpunit --coverage-html coverage-report --testdox",
+ "test": "vendor/bin/phpunit --colors=always --testdox --coverage-clover clover.xml",
+ "coverage": "vendor/bin/phpunit --colors=always --testdox --coverage-html coverage-report --coverage-clover clover.xml",
"analyse": "vendor/bin/phpstan analyse src -l8",
"sniff": "./vendor/bin/phpcs src/ -v",
"all": "composer test && composer analyse && composer sniff"
},
+ "minimum-stability": "dev",
+ "prefer-stable": true,
"config": {
"allow-plugins": {
"roots/wordpress-core-installer": true,
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
-}
\ No newline at end of file
+}
diff --git a/phpcs.xml b/phpcs.xml
index dacf3ac..df50103 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -19,6 +19,7 @@
+
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index cccf1f4..ee65564 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -1,5 +1,4 @@
includes:
- - vendor/phpstan/phpstan/conf/bleedingEdge.neon
- vendor/szepeviktor/phpstan-wordpress/extension.neon
parameters:
level: max
@@ -8,7 +7,3 @@ parameters:
- %currentWorkingDirectory%/src/
excludePaths:
- %currentWorkingDirectory%/tests/*
- bootstrapFiles:
- - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php
- ignoreErrors:
- - '^function get_headers expects (?:bool|int), int\|true given^'
diff --git a/src/Enqueue.php b/src/Enqueue.php
index 84f147e..d6b06c0 100644
--- a/src/Enqueue.php
+++ b/src/Enqueue.php
@@ -119,6 +119,62 @@ class Enqueue {
*/
protected $script_type = 'text/javascript';
+ /**
+ * Script translations domain + optional path for wp_set_script_translations().
+ *
+ * @var array{domain: string, path: string|null}|null
+ */
+ protected $translations = null;
+
+ /**
+ * Inline script snippets to attach to the handle via wp_add_inline_script().
+ *
+ * @var array
+ */
+ protected $inline_scripts = array();
+
+ /**
+ * Inline CSS strings to attach to the handle via wp_add_inline_style().
+ *
+ * @var array
+ */
+ protected $inline_styles = array();
+
+ /**
+ * Defer/async strategy forwarded to wp_script_add_data( $handle, 'strategy', $strategy ).
+ *
+ * @var string|null
+ */
+ protected $strategy = null;
+
+ /**
+ * When true, register() attaches the registration/enqueue flow to enqueue_block_editor_assets.
+ *
+ * @var bool
+ */
+ protected $for_block_editor = false;
+
+ /**
+ * When set, register_style() calls wp_enqueue_block_style() against this block name.
+ *
+ * @var string|null
+ */
+ protected $block_name = null;
+
+ /**
+ * Register the asset with WP but do not enqueue it. Handle remains available for deps.
+ *
+ * @var bool
+ */
+ protected $register_only = false;
+
+ /**
+ * Path to a block.json directory for register_block_type_from_metadata().
+ *
+ * @var string|null
+ */
+ protected $block_json_path = null;
+
/**
* Creates an Enqueue instance.
*
@@ -194,14 +250,28 @@ public function media( string $media ): self {
return $this;
}
+ /**
+ * Attaches an inline CSS snippet to the registered style handle.
+ *
+ * Calls stack — each call adds another entry. Only applies to styles; a
+ * silent no-op on a script Enqueue.
+ *
+ * @param string $css CSS source.
+ * @return self
+ */
+ public function with_style( string $css ): self {
+ $this->inline_styles[] = $css;
+ return $this;
+ }
+
/**
* DEPRECATED DUE TO TYPO
*
* see latest_version()
* @deprecated 1.3.0
*/
- public function lastest_version():self {
- trigger_error( 'Method ' . __METHOD__ . ' is deprecated', E_USER_DEPRECATED ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
+ public function lastest_version(): self {
+ _deprecated_function( __METHOD__, '1.3.0', __CLASS__ . '::latest_version()' );
return $this->latest_version();
}
@@ -212,18 +282,21 @@ public function lastest_version():self {
* @return self
*/
public function latest_version(): self {
- if ( $this->does_file_exist( $this->src ) ) {
-
- // If php8 or above set as bool, else int
- $associate = ( PHP_VERSION_ID >= 80000 ) ? true : 1;
+ if ( empty( $this->src ) ) {
+ return $this;
+ }
- $headers = get_headers( $this->src, $associate );
+ $response = \wp_remote_head( $this->src, array( 'timeout' => 1 ) );
+ if ( \is_wp_error( $response ) || \wp_remote_retrieve_response_code( $response ) !== 200 ) {
+ return $this;
+ }
- if ( is_array( $headers )
- && array_key_exists( 'Last-Modified', $headers )
- ) {
- $this->ver = strtotime( $headers['Last-Modified'] );
- }
+ $last_modified = \wp_remote_retrieve_header( $response, 'last-modified' );
+ if ( is_array( $last_modified ) ) {
+ $last_modified = end( $last_modified );
+ }
+ if ( ! empty( $last_modified ) ) {
+ $this->ver = strtotime( (string) $last_modified );
}
return $this;
}
@@ -231,20 +304,18 @@ public function latest_version(): self {
/**
* Checks to see if a file exist using URL (not path).
*
+ * Uses wp_remote_head() so test mocks via the pre_http_request filter can
+ * fake the response without a real network round-trip.
+ *
* @param string $url The URL of the file being checked.
* @return boolean true if it does, false if it doesnt.
*/
private function does_file_exist( string $url ): bool {
- $ch = curl_init( $url );
- if ( ! $ch ) {
+ $response = \wp_remote_head( $url, array( 'timeout' => 1 ) );
+ if ( \is_wp_error( $response ) ) {
return false;
}
- curl_setopt( $ch, CURLOPT_NOBODY, true );
- curl_setopt( $ch, CURLOPT_TIMEOUT_MS, 50 );
- curl_exec( $ch );
- $http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
- curl_close( $ch );
- return $http_code === 200;
+ return \wp_remote_retrieve_response_code( $response ) === 200;
}
/**
@@ -274,7 +345,7 @@ public function header(): self {
* @param boolean $inline
* @return self
*/
- public function inline( bool $inline = true ):self {
+ public function inline( bool $inline = true ): self {
$this->inline = $inline;
return $this;
}
@@ -290,6 +361,43 @@ public function localize( array $args ): self {
return $this;
}
+ /**
+ * Registers the script for translation via wp_set_script_translations().
+ *
+ * Only applies to scripts. On a style Enqueue this is a silent no-op so
+ * fluent chains don't need to branch.
+ *
+ * @param string $domain Text domain.
+ * @param string|null $path Optional path to the JSON translation files.
+ * @return self
+ */
+ public function translations( string $domain, ?string $path = null ): self {
+ $this->translations = array(
+ 'domain' => $domain,
+ 'path' => $path,
+ );
+ return $this;
+ }
+
+ /**
+ * Attaches an inline JavaScript snippet to the registered handle.
+ *
+ * Calls stack — each call adds another entry. `$position` is `'before'` or
+ * `'after'` (matching wp_add_inline_script). Only applies to scripts; a
+ * silent no-op on a style Enqueue.
+ *
+ * @param string $code JavaScript source.
+ * @param string $position 'before' or 'after'. Default 'after'.
+ * @return self
+ */
+ public function with_code( string $code, string $position = 'after' ): self {
+ $this->inline_scripts[] = array(
+ 'code' => $code,
+ 'position' => $position,
+ );
+ return $this;
+ }
+
/**
* Adds a Flag (attribute with no value) to a script/style tag
*
@@ -314,32 +422,28 @@ public function attribute( string $key, string $value ): self {
}
/**
- * Marks the script or style as deferred loaded.
+ * Marks the script as deferred loaded.
+ *
+ * Uses WordPress 6.3+'s dep-aware script strategy via wp_script_add_data()
+ * instead of splatting the attribute through the script_loader_tag filter.
*
* @return self
*/
public function defer(): self {
- // Remove ASYNC if set.
- if ( \array_key_exists( 'async', $this->attributes ) ) {
- unset( $this->attributes['async'] );
- }
-
- $this->attributes['defer'] = '';
+ $this->strategy = 'defer';
return $this;
}
/**
- * Marks the script or style as async loaded.
+ * Marks the script as async loaded.
+ *
+ * Uses WordPress 6.3+'s dep-aware script strategy via wp_script_add_data()
+ * instead of splatting the attribute through the script_loader_tag filter.
*
* @return self
*/
public function async(): self {
- // Remove DEFER if set.
- if ( \array_key_exists( 'defer', $this->attributes ) ) {
- unset( $this->attributes['defer'] );
- }
-
- $this->attributes['async'] = '';
+ $this->strategy = 'async';
return $this;
}
@@ -349,17 +453,108 @@ public function async(): self {
* @param bool $for_block Denotes if being enqueued for a block.
* @return self
*/
- public function for_block( bool $for_block = true ) : self {
+ public function for_block( bool $for_block = true ): self {
$this->for_block = $for_block;
return $this;
}
+ /**
+ * Defer the registration/enqueue to the enqueue_block_editor_assets hook.
+ *
+ * When set, calling register() attaches the rest of the flow to the block
+ * editor hook instead of running immediately. Use for scripts/styles that
+ * should only load inside the admin block editor.
+ *
+ * @param bool $for_block_editor Default true.
+ * @return self
+ */
+ public function for_block_editor( bool $for_block_editor = true ): self {
+ $this->for_block_editor = $for_block_editor;
+ return $this;
+ }
+
+ /**
+ * Attach the style to a named block via wp_enqueue_block_style().
+ *
+ * Only applies to styles. When set, register_style() calls
+ * wp_enqueue_block_style() against the block so the stylesheet loads only
+ * when the block is rendered on the front-end.
+ *
+ * @param string $block_name Block name (e.g. 'core/paragraph' or 'my-plugin/my-block').
+ * @return self
+ */
+ public function for_block_style( string $block_name ): self {
+ $this->block_name = $block_name;
+ return $this;
+ }
+
+ /**
+ * Register the asset with WP without enqueueing it.
+ *
+ * Useful when another handle will reference this one via a dep array, or
+ * when a block's block.json names it. Keeps the handle available without
+ * emitting it on every page.
+ *
+ * @param bool $register_only Default true.
+ * @return self
+ */
+ public function register_only( bool $register_only = true ): self {
+ $this->register_only = $register_only;
+ return $this;
+ }
+
+ /**
+ * Register a block type from a block.json metadata directory.
+ *
+ * When set, register() short-circuits to register_block_type_from_metadata()
+ * instead of the normal script/style registration path. The handle / src /
+ * deps / ver passed via the fluent API are ignored — block.json is the
+ * source of truth for asset declarations.
+ *
+ * @param string $path Absolute path to the directory containing block.json.
+ * @return self
+ */
+ public function block_json( string $path ): self {
+ $this->block_json_path = $path;
+ return $this;
+ }
+
/**
* Registers the file as either enqueued or inline parsed.
*
* @return void
*/
public function register(): void {
+ // block.json registration takes over completely — the handle/src/deps
+ // passed via the fluent API are ignored; block.json drives the assets.
+ if ( null !== $this->block_json_path ) {
+ \register_block_type_from_metadata( $this->block_json_path );
+ return;
+ }
+
+ // Block-editor scope: defer the rest of the flow to the editor hook.
+ if ( $this->for_block_editor ) {
+ add_action(
+ 'enqueue_block_editor_assets',
+ function () {
+ $this->register_inner();
+ }
+ );
+ return;
+ }
+
+ $this->register_inner();
+ }
+
+ /**
+ * Dispatches the registration to the script or style path.
+ *
+ * Extracted from register() so it can be re-entered from the
+ * enqueue_block_editor_assets callback.
+ *
+ * @return void
+ */
+ private function register_inner(): void {
if ( $this->type === 'script' ) {
$this->register_script();
}
@@ -375,20 +570,38 @@ public function register(): void {
* @return void
*/
private function register_style() {
+ // Block-bound style: wp_enqueue_block_style() registers and attaches it
+ // to the named block, loads only when the block renders.
+ if ( null !== $this->block_name ) {
+ \wp_enqueue_block_style(
+ $this->block_name,
+ array(
+ 'handle' => $this->handle,
+ 'src' => $this->src,
+ 'deps' => $this->deps,
+ 'ver' => $this->ver,
+ 'media' => $this->media,
+ )
+ );
+ } else {
+ \wp_register_style(
+ $this->handle,
+ $this->src,
+ $this->deps,
+ $this->ver,
+ $this->media
+ );
+
+ if ( false === $this->for_block && false === $this->register_only ) {
+ wp_enqueue_style( $this->handle );
+ }
+ }
- \wp_register_style(
- $this->handle,
- $this->src,
- $this->deps,
- $this->ver,
- $this->media
- );
- if ( false === $this->for_block ) {
- wp_enqueue_style( $this->handle );
+ foreach ( $this->inline_styles as $css ) {
+ \wp_add_inline_style( $this->handle, $css );
}
$this->add_style_attributes();
-
}
/**
@@ -406,9 +619,29 @@ private function register_script() {
$this->footer
);
- // Maybe add as an inline script.
+ // Script strategy (defer/async) via WP's dep-aware API.
+ if ( null !== $this->strategy ) {
+ \wp_script_add_data( $this->handle, 'strategy', $this->strategy );
+ }
+
+ // Script translations (i18n JSON lookup).
+ if ( null !== $this->translations ) {
+ \wp_set_script_translations(
+ $this->handle,
+ $this->translations['domain'],
+ $this->translations['path'] ?? ''
+ );
+ }
+
+ // Maybe add as an inline script (whole-file inline — legacy behaviour).
if ( $this->inline && $this->does_file_exist( $this->src ) ) {
- \wp_add_inline_script( $this->handle, file_get_contents( $this->src ) ?: '' );
+ $contents = file_get_contents( $this->src );
+ \wp_add_inline_script( $this->handle, false !== $contents ? $contents : '' );
+ }
+
+ // Attach inline code snippets added via with_code().
+ foreach ( $this->inline_scripts as $snippet ) {
+ \wp_add_inline_script( $this->handle, $snippet['code'], $snippet['position'] );
}
// Localize all values if defined.
@@ -416,8 +649,8 @@ private function register_script() {
\wp_localize_script( $this->handle, $this->handle, $this->localize );
}
- // Enqueue file if not used for a block.
- if ( false === $this->for_block ) {
+ // Enqueue file if not used for a block / not register-only.
+ if ( false === $this->for_block && false === $this->register_only ) {
\wp_enqueue_script( $this->handle );
}
@@ -441,7 +674,7 @@ private function add_script_attributes(): void {
// Add to any scripts.
add_filter(
'script_loader_tag',
- function( string $tag, string $handle, string $source ) use ( $attributes ): string {
+ function ( string $tag, string $handle, string $source ) use ( $attributes ): string {
// Bail if not our script.
if ( $this->handle !== $handle ) {
return $tag;
@@ -489,7 +722,7 @@ private function add_style_attributes(): void {
// Add to any relevant styles.
add_filter(
'style_loader_tag',
- function( string $tag, string $handle, string $href, string $media ) use ( $attributes ): string {
+ function ( string $tag, string $handle, string $href, string $media ) use ( $attributes ): string {
// Bail if not our script.
if ( $this->handle !== $handle ) {
return $tag;
@@ -523,9 +756,9 @@ public function script_type( string $script_type ) {
*
* @return string[]
*/
- private function get_attributes():array {
+ private function get_attributes(): array {
return array_map(
- function( string $key, ?string $value ): string {
+ function ( string $key, ?string $value ): string {
return null === $value
? "{$key}"
: "{$key}='{$value}'";
@@ -534,5 +767,4 @@ function( string $key, ?string $value ): string {
$this->attributes
);
}
-
}
diff --git a/tests/.env b/tests/.env
new file mode 100644
index 0000000..7bb1e2d
--- /dev/null
+++ b/tests/.env
@@ -0,0 +1,8 @@
+# WP Config Details
+
+# Please ensure the database exists before running the tests
+
+WP_DB_NAME=pc_core_tests
+WP_DB_USER=root
+WP_DB_PASS=
+WP_DB_HOST=127.0.0.1
\ No newline at end of file
diff --git a/tests/Fixtures/test-block/block.json b/tests/Fixtures/test-block/block.json
new file mode 100644
index 0000000..ba9d93b
--- /dev/null
+++ b/tests/Fixtures/test-block/block.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "pinkcrab-enqueue/test-block",
+ "title": "PinkCrab Enqueue Test Block",
+ "category": "widgets",
+ "description": "Fixture block for PinkCrab Enqueue integration tests.",
+ "textdomain": "pinkcrab-enqueue-test",
+ "supports": {
+ "html": false
+ }
+}
diff --git a/tests/Test_Enqueue.php b/tests/Test_Enqueue.php
index 7167377..aa58512 100644
--- a/tests/Test_Enqueue.php
+++ b/tests/Test_Enqueue.php
@@ -125,54 +125,46 @@ public function test_style_setters(): void {
}
- /** @testdox It should be possible to denote a scriptas async easily. */
+ /** @testdox It should be possible to denote a script as async easily. */
public function test_can_set_async_on_script(): void {
$script = self::create_script()->async();
- $attributes = Objects::get_property( $script, 'attributes' );
- $this->assertArrayHasKey( 'async', $attributes );
+ $this->assertSame( 'async', Objects::get_property( $script, 'strategy' ) );
}
/** @testdox It should be possible to denote a style as async easily. */
public function test_can_set_async_style(): void {
$style = self::create_style()->async();
- $attributes = Objects::get_property( $style, 'attributes' );
- $this->assertArrayHasKey( 'async', $attributes );
+ // async() sets the strategy flag regardless of type; wp_script_add_data
+ // on render is script-only, so on a style this is a harmless no-op.
+ $this->assertSame( 'async', Objects::get_property( $style, 'strategy' ) );
}
- /** @testdox It should be possible to denote a scriptas defer easily. */
+ /** @testdox It should be possible to denote a script as defer easily. */
public function test_can_set_defer_on_script(): void {
$script = self::create_script()->defer();
- $attributes = Objects::get_property( $script, 'attributes' );
- $this->assertArrayHasKey( 'defer', $attributes );
+ $this->assertSame( 'defer', Objects::get_property( $script, 'strategy' ) );
}
/** @testdox It should be possible to denote a style as defer easily. */
public function test_can_set_defer_style(): void {
$style = self::create_style()->defer();
- $attributes = Objects::get_property( $style, 'attributes' );
- $this->assertArrayHasKey( 'defer', $attributes );
+ $this->assertSame( 'defer', Objects::get_property( $style, 'strategy' ) );
}
/** @testdox It should not be possible to set both async and defer, either should unset the other */
public function test_can_only_be_async_or_defer(): void {
- $script = self::create_script()->async();
- $attributes = Objects::get_property( $script, 'attributes' );
- $this->assertArrayHasKey( 'async', $attributes );
- $this->assertArrayNotHasKey( 'defer', $attributes );
+ $script = self::create_script()->async();
+ $this->assertSame( 'async', Objects::get_property( $script, 'strategy' ) );
$script->defer();
- $attributes = Objects::get_property( $script, 'attributes' );
- $this->assertArrayHasKey( 'defer', $attributes );
- $this->assertArrayNotHasKey( 'async', $attributes );
+ $this->assertSame( 'defer', Objects::get_property( $script, 'strategy' ) );
$script->async();
- $attributes = Objects::get_property( $script, 'attributes' );
- $this->assertArrayHasKey( 'async', $attributes );
- $this->assertArrayNotHasKey( 'defer', $attributes );
+ $this->assertSame( 'async', Objects::get_property( $script, 'strategy' ) );
}
/** @testdox It should be possible to define if a script is added to the header */
@@ -226,12 +218,78 @@ public function test_can_register_script_with_custom_types(): void {
/** @testdox Calling the old, mistyped lastest_version() should throw deprecation notice. */
public function test_calling_old_latest_version_should_throw_deprecation_notice(): void {
- try {
- Enqueue::script('ff')->lastest_version();
- } catch (\Throwable $th) {
- $this->assertStringContainsString('Enqueue::lastest_version', $th->getMessage());
- $this->assertStringContainsString('is deprecated', $th->getMessage());
- }
+ $this->setExpectedDeprecated( 'PinkCrab\Enqueue\Enqueue::lastest_version' );
+ Enqueue::script( 'ff' )->src( 'https://example.test/js.js' )->lastest_version();
+ }
+
+ /** @testdox It should be possible to set script translations via translations(). */
+ public function test_can_set_translations(): void {
+ $script = self::create_script()->translations( 'my-domain', '/path/to/translations' );
+ $translations = Objects::get_property( $script, 'translations' );
+ $this->assertSame( 'my-domain', $translations['domain'] );
+ $this->assertSame( '/path/to/translations', $translations['path'] );
+
+ // Path is optional.
+ $script = self::create_script()->translations( 'just-domain' );
+ $translations = Objects::get_property( $script, 'translations' );
+ $this->assertSame( 'just-domain', $translations['domain'] );
+ $this->assertNull( $translations['path'] );
+ }
+
+ /** @testdox It should be possible to attach inline JS snippets via with_code() and have them accumulate. */
+ public function test_can_add_inline_code(): void {
+ $script = self::create_script()
+ ->with_code( 'var foo = 1;' )
+ ->with_code( 'var bar = 2;', 'before' );
+
+ $snippets = Objects::get_property( $script, 'inline_scripts' );
+ $this->assertCount( 2, $snippets );
+ $this->assertSame( 'var foo = 1;', $snippets[0]['code'] );
+ $this->assertSame( 'after', $snippets[0]['position'] );
+ $this->assertSame( 'var bar = 2;', $snippets[1]['code'] );
+ $this->assertSame( 'before', $snippets[1]['position'] );
+ }
+
+ /** @testdox It should be possible to attach inline CSS snippets via with_style() and have them accumulate. */
+ public function test_can_add_inline_style(): void {
+ $style = self::create_style()
+ ->with_style( '.foo { color: red; }' )
+ ->with_style( '.bar { color: blue; }' );
+
+ $snippets = Objects::get_property( $style, 'inline_styles' );
+ $this->assertCount( 2, $snippets );
+ $this->assertSame( '.foo { color: red; }', $snippets[0] );
+ $this->assertSame( '.bar { color: blue; }', $snippets[1] );
+ }
+
+ /** @testdox It should be possible to flag an asset for the block editor context. */
+ public function test_can_set_for_block_editor(): void {
+ $script = self::create_script()->for_block_editor();
+ $this->assertTrue( Objects::get_property( $script, 'for_block_editor' ) );
+
+ // Default arg is true; explicit false also works.
+ $script = self::create_script()->for_block_editor( false );
+ $this->assertFalse( Objects::get_property( $script, 'for_block_editor' ) );
+ }
+
+ /** @testdox It should be possible to attach a style to a named block via for_block_style(). */
+ public function test_can_set_for_block_style(): void {
+ $style = self::create_style()->for_block_style( 'my-plugin/my-block' );
+ $this->assertSame( 'my-plugin/my-block', Objects::get_property( $style, 'block_name' ) );
+ }
+
+ /** @testdox It should be possible to register without enqueueing via register_only(). */
+ public function test_can_set_register_only(): void {
+ $script = self::create_script()->register_only();
+ $this->assertTrue( Objects::get_property( $script, 'register_only' ) );
+
+ $script = self::create_script()->register_only( false );
+ $this->assertFalse( Objects::get_property( $script, 'register_only' ) );
+ }
+ /** @testdox It should be possible to point at a block.json directory via block_json(). */
+ public function test_can_set_block_json(): void {
+ $script = self::create_script()->block_json( '/path/to/block-dir' );
+ $this->assertSame( '/path/to/block-dir', Objects::get_property( $script, 'block_json_path' ) );
}
}
diff --git a/tests/Test_Enqueue_Functional.php b/tests/Test_Enqueue_Functional.php
index 635fdf6..94672fe 100644
--- a/tests/Test_Enqueue_Functional.php
+++ b/tests/Test_Enqueue_Functional.php
@@ -388,6 +388,351 @@ function() {
$this->assertArrayNotHasKey( 'script_loader_tag', $GLOBALS['wp_filter'] );
}
+ /** @testdox Calling defer() should register a 'defer' strategy via wp_script_add_data. */
+ public function test_defer_sets_strategy_data(): void {
+ Enqueue::script( 'script_defer' )
+ ->src( 'https://url.com/Fixtures/defer.js' )
+ ->ver( '1.0.0' )
+ ->defer()
+ ->register();
+
+ $this->assertArrayHasKey( 'script_defer', $GLOBALS['wp_scripts']->registered );
+ $this->assertSame( 'defer', $GLOBALS['wp_scripts']->registered['script_defer']->extra['strategy'] );
+ }
+
+ /** @testdox Calling async() should register an 'async' strategy via wp_script_add_data. */
+ public function test_async_sets_strategy_data(): void {
+ Enqueue::script( 'script_async' )
+ ->src( 'https://url.com/Fixtures/async.js' )
+ ->ver( '1.0.0' )
+ ->async()
+ ->register();
+
+ $this->assertArrayHasKey( 'script_async', $GLOBALS['wp_scripts']->registered );
+ $this->assertSame( 'async', $GLOBALS['wp_scripts']->registered['script_async']->extra['strategy'] );
+ }
+
+ /** @testdox translations() should register the handle for wp_set_script_translations. */
+ public function test_translations_registered_against_handle(): void {
+ Enqueue::script( 'script_i18n' )
+ ->src( 'https://url.com/Fixtures/i18n.js' )
+ ->ver( '1.0.0' )
+ ->translations( 'pinkcrab-enqueue-test', '/tmp/translations' )
+ ->register();
+
+ $this->assertArrayHasKey( 'script_i18n', $GLOBALS['wp_scripts']->registered );
+ $script = $GLOBALS['wp_scripts']->registered['script_i18n'];
+ $this->assertSame( 'pinkcrab-enqueue-test', $script->textdomain );
+ $this->assertSame( '/tmp/translations', $script->translations_path );
+ }
+
+ /** @testdox with_code() should attach inline snippets to the handle in the correct positions. */
+ public function test_with_code_attaches_inline_snippets(): void {
+ Enqueue::script( 'script_with_code' )
+ ->src( 'https://url.com/Fixtures/with_code.js' )
+ ->with_code( 'window.A = 1;' )
+ ->with_code( 'window.B = 2;', 'before' )
+ ->with_code( 'window.C = 3;', 'after' )
+ ->register();
+
+ $script = $GLOBALS['wp_scripts']->registered['script_with_code'];
+ $after = is_array( $script->extra['after'] ) ? implode( "\n", $script->extra['after'] ) : $script->extra['after'];
+ $before = is_array( $script->extra['before'] ) ? implode( "\n", $script->extra['before'] ) : $script->extra['before'];
+ $this->assertStringContainsString( 'window.A = 1;', $after );
+ $this->assertStringContainsString( 'window.C = 3;', $after );
+ $this->assertStringContainsString( 'window.B = 2;', $before );
+ }
+
+ /** @testdox with_style() should attach inline CSS to the style handle's 'after' slot. */
+ public function test_with_style_attaches_inline_css(): void {
+ Enqueue::style( 'style_with_css' )
+ ->src( 'https://url.com/Fixtures/with_css.css' )
+ ->with_style( '.foo { color: red; }' )
+ ->with_style( '.bar { color: blue; }' )
+ ->register();
+
+ $style = $GLOBALS['wp_styles']->registered['style_with_css'];
+ $after_css = implode( "\n", $style->extra['after'] );
+ $this->assertStringContainsString( '.foo { color: red; }', $after_css );
+ $this->assertStringContainsString( '.bar { color: blue; }', $after_css );
+ }
+
+ /** @testdox register_only() should register the script without enqueueing it. */
+ public function test_register_only_script_does_not_enqueue(): void {
+ Enqueue::script( 'script_register_only' )
+ ->src( 'https://url.com/Fixtures/register_only.js' )
+ ->register_only()
+ ->register();
+
+ $this->assertTrue( wp_script_is( 'script_register_only', 'registered' ) );
+ $this->assertFalse( wp_script_is( 'script_register_only', 'enqueued' ) );
+ }
+
+ /** @testdox register_only() should register the style without enqueueing it. */
+ public function test_register_only_style_does_not_enqueue(): void {
+ Enqueue::style( 'style_register_only' )
+ ->src( 'https://url.com/Fixtures/register_only.css' )
+ ->register_only()
+ ->register();
+
+ $this->assertTrue( wp_style_is( 'style_register_only', 'registered' ) );
+ $this->assertFalse( wp_style_is( 'style_register_only', 'enqueued' ) );
+ }
+
+ /** @testdox for_block_editor() should defer the registration to the enqueue_block_editor_assets hook. */
+ public function test_for_block_editor_defers_to_editor_hook(): void {
+ Enqueue::script( 'script_block_editor' )
+ ->src( 'https://url.com/Fixtures/block_editor.js' )
+ ->for_block_editor()
+ ->register();
+
+ // Before the hook fires, the script is NOT registered.
+ $this->assertFalse( wp_script_is( 'script_block_editor', 'registered' ) );
+
+ // Firing the editor hook should register + enqueue the script.
+ do_action( 'enqueue_block_editor_assets' );
+ $this->assertTrue( wp_script_is( 'script_block_editor', 'registered' ) );
+ $this->assertTrue( wp_script_is( 'script_block_editor', 'enqueued' ) );
+ }
+
+ /** @testdox for_block_style() should register the style and attach a render_block_{name} filter. */
+ public function test_for_block_style_attaches_to_block(): void {
+ // Block must be registered first for wp_enqueue_block_style to attach a filter to it.
+ \register_block_type(
+ 'pinkcrab-enqueue/style-target',
+ array(
+ 'api_version' => 3,
+ 'title' => 'Style Target',
+ 'category' => 'widgets',
+ )
+ );
+
+ Enqueue::style( 'block_bound_style' )
+ ->src( 'https://url.com/Fixtures/block_bound.css' )
+ ->ver( '1.0.0' )
+ ->for_block_style( 'pinkcrab-enqueue/style-target' )
+ ->register();
+
+ // wp_enqueue_block_style wires a deferred callback. Depending on the
+ // WP version and wp_should_load_block_assets_on_demand(), it lands on
+ // render_block, wp_enqueue_scripts/wp_footer, or enqueue_block_assets.
+ // Fire the full set to cover every branch.
+ apply_filters(
+ 'render_block',
+ 'test
',
+ array(
+ 'blockName' => 'pinkcrab-enqueue/style-target',
+ 'attrs' => array(),
+ 'innerBlocks' => array(),
+ 'innerHTML' => '',
+ 'innerContent' => array(),
+ ),
+ null
+ );
+ do_action( 'wp_enqueue_scripts' );
+ do_action( 'wp_footer' );
+ do_action( 'enqueue_block_assets' );
+
+ $this->assertTrue( wp_style_is( 'block_bound_style', 'registered' ) );
+
+ \unregister_block_type( 'pinkcrab-enqueue/style-target' );
+ }
+
+ /** @testdox block_json() should register a block type from the block.json directory. */
+ public function test_block_json_registers_block_type(): void {
+ $block_dir = __DIR__ . '/Fixtures/test-block';
+
+ Enqueue::script( 'ignored_handle' )
+ ->block_json( $block_dir )
+ ->register();
+
+ $registry = \WP_Block_Type_Registry::get_instance();
+ $this->assertTrue( $registry->is_registered( 'pinkcrab-enqueue/test-block' ) );
+
+ $registry->unregister( 'pinkcrab-enqueue/test-block' );
+ }
+
+ /** @testdox latest_version() is a no-op when no src has been set. */
+ public function test_latest_version_no_op_when_src_empty(): void {
+ $script = Enqueue::script( 'no_src' )->latest_version();
+ $this->assertFalse( \Gin0115\WPUnit_Helpers\Objects::get_property( $script, 'ver' ) );
+ }
+
+ /** @testdox latest_version() bails when wp_remote_head returns a WP_Error. */
+ public function test_latest_version_bails_on_http_error(): void {
+ add_filter(
+ 'pre_http_request',
+ static function () {
+ return new \WP_Error( 'http_request_failed', 'fake network fail' );
+ },
+ 10,
+ 3
+ );
+
+ $script = Enqueue::script( 'errored' )
+ ->src( 'https://fake.example/script.js' )
+ ->latest_version();
+
+ // ver stays at the default false (no timestamp parsed).
+ $this->assertFalse( \Gin0115\WPUnit_Helpers\Objects::get_property( $script, 'ver' ) );
+
+ remove_all_filters( 'pre_http_request' );
+ }
+
+ /** @testdox latest_version() bails when the remote returns a non-200 status. */
+ public function test_latest_version_bails_on_non_200(): void {
+ add_filter(
+ 'pre_http_request',
+ static function () {
+ return array(
+ 'headers' => array(),
+ 'body' => '',
+ 'response' => array(
+ 'code' => 404,
+ 'message' => 'Not Found',
+ ),
+ 'cookies' => array(),
+ 'filename' => null,
+ );
+ },
+ 10,
+ 3
+ );
+
+ $script = Enqueue::script( 'missing' )
+ ->src( 'https://fake.example/missing.js' )
+ ->latest_version();
+
+ $this->assertFalse( \Gin0115\WPUnit_Helpers\Objects::get_property( $script, 'ver' ) );
+
+ remove_all_filters( 'pre_http_request' );
+ }
+
+ /** @testdox latest_version() parses a scalar Last-Modified header into a unix timestamp. */
+ public function test_latest_version_sets_ver_from_last_modified(): void {
+ $modified = 'Mon, 20 Apr 2026 12:34:56 GMT';
+ add_filter(
+ 'pre_http_request',
+ static function () use ( $modified ) {
+ return array(
+ 'headers' => array( 'last-modified' => $modified ),
+ 'body' => '',
+ 'response' => array(
+ 'code' => 200,
+ 'message' => 'OK',
+ ),
+ 'cookies' => array(),
+ 'filename' => null,
+ );
+ },
+ 10,
+ 3
+ );
+
+ $script = Enqueue::script( 'latest' )
+ ->src( 'https://fake.example/latest.js' )
+ ->latest_version();
+
+ $this->assertSame( strtotime( $modified ), \Gin0115\WPUnit_Helpers\Objects::get_property( $script, 'ver' ) );
+
+ remove_all_filters( 'pre_http_request' );
+ }
+
+ /** @testdox latest_version() handles a Last-Modified header returned as an array (multi-valued). */
+ public function test_latest_version_handles_array_last_modified(): void {
+ $oldest = 'Mon, 01 Jan 2026 00:00:00 GMT';
+ $newest = 'Mon, 20 Apr 2026 12:34:56 GMT';
+ add_filter(
+ 'pre_http_request',
+ static function () use ( $oldest, $newest ) {
+ return array(
+ 'headers' => array( 'last-modified' => array( $oldest, $newest ) ),
+ 'body' => '',
+ 'response' => array(
+ 'code' => 200,
+ 'message' => 'OK',
+ ),
+ 'cookies' => array(),
+ 'filename' => null,
+ );
+ },
+ 10,
+ 3
+ );
+
+ $script = Enqueue::script( 'latest_array' )
+ ->src( 'https://fake.example/latest.js' )
+ ->latest_version();
+
+ // end() picks the last entry — the newer timestamp.
+ $this->assertSame( strtotime( $newest ), \Gin0115\WPUnit_Helpers\Objects::get_property( $script, 'ver' ) );
+
+ remove_all_filters( 'pre_http_request' );
+ }
+
+ /** @testdox inline() does not attach file contents when the remote HEAD request fails (WP_Error). */
+ public function test_inline_skips_file_contents_on_http_error(): void {
+ add_filter(
+ 'pre_http_request',
+ static function () {
+ return new \WP_Error( 'http_request_failed', 'fake network fail' );
+ },
+ 10,
+ 3
+ );
+
+ Enqueue::script( 'inline_errored' )
+ ->src( 'https://fake.example/dead.js' )
+ ->inline()
+ ->register();
+
+ $script = $GLOBALS['wp_scripts']->registered['inline_errored'];
+ // No inline contents should have been attached.
+ $this->assertFalse( isset( $script->extra['after'] ) && ! empty( $script->extra['after'] ) );
+
+ remove_all_filters( 'pre_http_request' );
+ }
+
+ /** @testdox inline() attaches the file contents to the registered script when does_file_exist returns true. */
+ public function test_inline_attaches_file_contents_when_remote_exists(): void {
+ $body = 'console.log("inline-me");';
+ add_filter(
+ 'pre_http_request',
+ static function () use ( $body ) {
+ return array(
+ 'headers' => array(),
+ 'body' => $body,
+ 'response' => array(
+ 'code' => 200,
+ 'message' => 'OK',
+ ),
+ 'cookies' => array(),
+ 'filename' => null,
+ );
+ },
+ 10,
+ 3
+ );
+
+ // Create a temp file locally because the legacy inline() path still
+ // uses file_get_contents(); the wp_remote_head does_file_exist check
+ // is the HTTP-mocked part.
+ $tmp = tempnam( sys_get_temp_dir(), 'pcenq' );
+ file_put_contents( $tmp, $body );
+
+ Enqueue::script( 'inline_file' )
+ ->src( $tmp )
+ ->inline()
+ ->register();
+
+ $script = $GLOBALS['wp_scripts']->registered['inline_file'];
+ $after = is_array( $script->extra['after'] ) ? implode( "\n", $script->extra['after'] ) : $script->extra['after'];
+ $this->assertStringContainsString( $body, $after );
+
+ unlink( $tmp );
+ remove_all_filters( 'pre_http_request' );
+ }
+
/** @testdox When registering a script, if the type is custom and manual id is added to attributes, it should not be auto generated. */
public function test_manual_id_added_to_attributes_if_custom_type_and_manual_id(): void {
// Enqueue
diff --git a/tests/wp-config.php b/tests/wp-config.php
index f5764eb..3791f5d 100644
--- a/tests/wp-config.php
+++ b/tests/wp-config.php
@@ -71,3 +71,15 @@
define( 'WP_PHP_BINARY', 'php' );
define( 'WPLANG', '' );
+
+// WP 6.8 emits an E_USER_NOTICE for wp_is_block_theme() being called too early
+// during tests. https://core.trac.wordpress.org/ticket/63086
+set_error_handler(
+ function ( $errno, $errstr ) {
+ if ( $errno === E_USER_NOTICE && strpos( $errstr, 'wp_is_block_theme' ) !== false ) {
+ return true;
+ }
+ return false;
+ },
+ E_USER_NOTICE
+);