Skip to content

Commit bddf4e3

Browse files
committed
WordPress Abilities API Client
1 parent 1b75e1d commit bddf4e3

21 files changed

+25035
-2
lines changed

abilities-api.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,24 @@
3131

3232
if ( function_exists( 'add_action' ) ) {
3333
add_action( 'rest_api_init', array( 'WP_REST_Abilities_Init', 'register_routes' ) );
34+
35+
// Register the Abilities API client script
36+
add_action( 'init', 'wp_abilities_register_client_assets' );
37+
38+
// Auto-enqueue on admin pages for development and testing
39+
add_action( 'admin_enqueue_scripts', 'wp_abilities_admin_enqueue_scripts' );
40+
}
41+
42+
/**
43+
* Auto-enqueue Abilities API client on admin pages.
44+
*
45+
* This is primarily for development and testing purposes.
46+
*
47+
* @since 0.1.0
48+
* @return void
49+
*/
50+
function wp_abilities_admin_enqueue_scripts() {
51+
if ( wp_script_is( 'wp-abilities', 'registered' ) ) {
52+
wp_enqueue_script( 'wp-abilities' );
53+
}
3454
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
/**
3+
* Client script registration for the Abilities API.
4+
*
5+
* This file provides functions to register the Abilities API JavaScript client
6+
* for both plugin and Composer-based installations.
7+
*
8+
* @package WordPress
9+
* @subpackage Abilities_API
10+
* @since 0.1.0
11+
*/
12+
13+
declare( strict_types = 1 );
14+
15+
/**
16+
* Registers the Abilities API JavaScript client.
17+
*
18+
* Auto-detects whether running as a plugin or Composer package and registers
19+
* the client script accordingly.
20+
*
21+
* @since 0.1.0
22+
*
23+
* @return bool True if the script was registered, false if the build doesn't exist.
24+
*/
25+
function wp_abilities_register_client_assets(): bool {
26+
if ( wp_script_is( 'wp-abilities', 'registered' ) ) {
27+
return true;
28+
}
29+
30+
if ( defined( 'WP_ABILITIES_API_DIR' ) ) {
31+
// Running as a plugin
32+
$base_path = WP_ABILITIES_API_DIR;
33+
$base_url = plugins_url( '', dirname( __DIR__, 2 ) . '/abilities-api.php' );
34+
} else {
35+
// Running as a Composer package
36+
$base_path = dirname( __DIR__, 2 );
37+
38+
// For Composer, we need to determine the URL based on the installation location
39+
$plugin_dir = WP_PLUGIN_DIR;
40+
if ( strpos( $base_path, $plugin_dir ) === 0 ) {
41+
// Inside a plugin directory
42+
$relative_path = str_replace( $plugin_dir, '', $base_path );
43+
$base_url = plugins_url( $relative_path );
44+
} else {
45+
// Assume standard Composer vendor structure
46+
$base_url = plugins_url( 'vendor/wordpress/abilities-api', dirname( $base_path, 2 ) );
47+
}
48+
}
49+
50+
$client_path = trailingslashit( $base_path ) . 'packages/client/build/';
51+
52+
if ( ! file_exists( $client_path . 'index.js' ) ) {
53+
return false;
54+
}
55+
56+
$asset = require_once $client_path . 'index.asset.php';
57+
58+
$client_url = trailingslashit( $base_url ) . 'packages/client/build/index.js';
59+
60+
wp_register_script(
61+
'wp-abilities',
62+
$client_url,
63+
$asset['dependencies'],
64+
$asset['version'],
65+
true
66+
);
67+
68+
return true;
69+
}

includes/bootstrap.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,8 @@
3535
if ( ! class_exists( 'WP_REST_Abilities_Init' ) ) {
3636
require_once __DIR__ . '/rest-api/class-wp-rest-abilities-init.php';
3737
}
38+
39+
// Load client registration helper for Composer users.
40+
if ( ! function_exists( 'wp_abilities_register_client_assets' ) ) {
41+
require_once __DIR__ . '/abilities-api/client-registration.php';
42+
}

includes/rest-api/endpoints/class-wp-rest-abilities-list-controller.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,13 +128,13 @@ public function get_items( $request ) {
128128
if ( $page > 1 ) {
129129
$prev_page = $page - 1;
130130
$prev_link = add_query_arg( 'page', $prev_page, $base );
131-
$response->add_link( 'prev', $prev_link );
131+
$response->link_header( 'prev', $prev_link );
132132
}
133133

134134
if ( $page < $max_pages ) {
135135
$next_page = $page + 1;
136136
$next_link = add_query_arg( 'page', $next_page, $base );
137-
$response->add_link( 'next', $next_link );
137+
$response->link_header( 'next', $next_link );
138138
}
139139

140140
return $response;

packages/client/.eslintrc.cjs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
module.exports = {
2+
root: true,
3+
extends: [
4+
'plugin:@wordpress/eslint-plugin/recommended',
5+
'plugin:eslint-comments/recommended',
6+
],
7+
plugins: [ 'import' ],
8+
parser: '@typescript-eslint/parser',
9+
parserOptions: {
10+
ecmaVersion: 2021,
11+
sourceType: 'module',
12+
ecmaFeatures: {
13+
jsx: true,
14+
},
15+
project: './tsconfig.json',
16+
},
17+
settings: {
18+
'import/resolver': {
19+
typescript: {
20+
project: './tsconfig.json',
21+
},
22+
},
23+
},
24+
env: {
25+
browser: true,
26+
es6: true,
27+
node: true,
28+
},
29+
rules: {
30+
'@wordpress/dependency-group': 'error',
31+
'@wordpress/data-no-store-string-literals': 'error',
32+
'import/default': 'error',
33+
'import/no-extraneous-dependencies': [
34+
'error',
35+
{
36+
devDependencies: [
37+
'**/*.@(spec|test).@(j|t)s?(x)',
38+
'**/@(webpack|jest).config.@(j|t)s',
39+
'**/scripts/**',
40+
],
41+
},
42+
],
43+
},
44+
overrides: [
45+
{
46+
files: [ '**/*.ts?(x)' ],
47+
rules: {
48+
'@typescript-eslint/consistent-type-imports': 'error',
49+
'@typescript-eslint/no-shadow': 'error',
50+
'no-shadow': 'off',
51+
'jsdoc/require-param': 'off',
52+
'jsdoc/require-param-type': 'off',
53+
'jsdoc/require-returns-type': 'off',
54+
},
55+
},
56+
],
57+
};

packages/client/.gitignore

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Dependencies
2+
node_modules/
3+
4+
# Build output
5+
build/
6+
build-module/
7+
build-types/
8+
9+
# TypeScript
10+
*.tsbuildinfo
11+
12+
# OS
13+
.DS_Store
14+
Thumbs.db
15+
16+
# Logs
17+
npm-debug.log*
18+
yarn-debug.log*
19+
yarn-error.log*

packages/client/.prettierrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require( '@wordpress/prettier-config' );

packages/client/README.md

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# WordPress Abilities API Client
2+
3+
Client library for the WordPress Abilities API, providing a standardized way to discover and execute WordPress capabilities.
4+
5+
## Installation
6+
7+
The client is available in two ways:
8+
9+
### 1. As an npm Package (Coming Soon)
10+
11+
The npm package `@wordpress/abilities` is planned for future publication. Once published, you'll be able to install it via:
12+
13+
```bash
14+
npm install @wordpress/abilities
15+
```
16+
17+
The npm package is designed for use with WordPress build tools (`@wordpress/scripts`). It requires the Abilities API plugin to be active in WordPress or via the Composer package, as it references the globally-loaded `wp.abilities` script at runtime.
18+
19+
For now, the client is available through the WordPress script registration below.
20+
21+
### 2. As a WordPress Script (Available Now)
22+
23+
When the Abilities API is installed as a WordPress plugin, the client is automatically registered and available to enqueue:
24+
25+
```php
26+
wp_enqueue_script( 'wp-abilities' );
27+
```
28+
29+
#### For Composer Installations
30+
31+
If you've installed the Abilities API via Composer, you need to register the script first.
32+
33+
```php
34+
// Register the client script (usually in your plugin's init hook)
35+
add_action( 'init', function() {
36+
if ( function_exists( 'wp_abilities_register_client_script' ) ) {
37+
// Provide the path and URL to your vendor directory
38+
$base_path = __DIR__ . '/vendor/wordpress/abilities-api';
39+
$base_url = plugins_url( 'vendor/wordpress/abilities-api', __FILE__ );
40+
41+
wp_abilities_register_client_script( $base_path, $base_url );
42+
}
43+
} );
44+
45+
// Then enqueue it where needed
46+
add_action( 'wp_enqueue_scripts', function() {
47+
wp_enqueue_script( 'wp-abilities' );
48+
} );
49+
```
50+
51+
## Usage
52+
53+
```javascript
54+
// In your WordPress plugin or theme JavaScript
55+
const { listAbilities, getAbility, executeAbility } = wp.abilities;
56+
// or import { listAbilities, getAbility, executeAbility } from '@wordpress/abilities'; depending on your setup
57+
58+
// List all abilities
59+
const abilities = await listAbilities();
60+
61+
// Get a specific ability
62+
const ability = await getAbility( 'my-plugin/my-ability' );
63+
64+
// Execute an ability
65+
const result = await executeAbility( 'my-plugin/my-ability', {
66+
param1: 'value1',
67+
param2: 'value2'
68+
} );
69+
```
70+
71+
### Using with React and WordPress Data
72+
73+
The client includes a data store that integrates with `@wordpress/data` for use in React components:
74+
75+
```javascript
76+
import { useSelect } from '@wordpress/data';
77+
import { store as abilitiesStore } from '@wordpress/abilities';
78+
79+
function MyComponent() {
80+
const abilities = useSelect(
81+
( select ) => select( abilitiesStore ).getAbilities(),
82+
[]
83+
);
84+
85+
const specificAbility = useSelect(
86+
( select ) => select( abilitiesStore ).getAbility( 'my-plugin/my-ability' ),
87+
[]
88+
);
89+
90+
return (
91+
<div>
92+
<h2>All Abilities</h2>
93+
<ul>
94+
{ abilities.map( ( ability ) => (
95+
<li key={ ability.name }>
96+
<strong>{ ability.label }</strong>: { ability.description }
97+
</li>
98+
) ) }
99+
</ul>
100+
</div>
101+
);
102+
}
103+
```
104+
105+
## API Reference
106+
107+
### Functions
108+
109+
#### `listAbilities(): Promise<Ability[]>`
110+
111+
Returns all registered abilities. Automatically handles pagination to fetch all abilities across multiple pages if needed.
112+
113+
```javascript
114+
const abilities = await listAbilities();
115+
console.log( `Found ${abilities.length} abilities` );
116+
```
117+
118+
#### `getAbility(name: string): Promise<Ability | null>`
119+
120+
Returns a specific ability by name, or null if not found.
121+
122+
```javascript
123+
const ability = await getAbility( 'my-plugin/create-post' );
124+
if ( ability ) {
125+
console.log( `Found ability: ${ability.label}` );
126+
}
127+
```
128+
129+
#### `executeAbility(name: string, input?: Record<string, any>): Promise<any>`
130+
131+
Executes an ability with optional input parameters. The HTTP method is automatically determined based on the ability's type:
132+
133+
- `resource` type abilities use GET (read-only operations)
134+
- `tool` type abilities use POST (write operations)
135+
136+
```javascript
137+
// Execute a resource ability (GET)
138+
const data = await executeAbility( 'my-plugin/get-data', {
139+
id: 123
140+
} );
141+
142+
// Execute a tool ability (POST)
143+
const result = await executeAbility( 'my-plugin/create-item', {
144+
title: 'New Item',
145+
content: 'Item content'
146+
} );
147+
```
148+
149+
### Store Selectors
150+
151+
When using with `@wordpress/data`:
152+
153+
- `getAbilities()` - Returns all abilities from the store
154+
- `getAbility(name)` - Returns a specific ability from the store
155+
156+
## Development
157+
158+
```bash
159+
# Install dependencies
160+
npm install
161+
162+
# Build the package
163+
npm run build
164+
165+
# Run linting
166+
npm run lint:js
167+
168+
# Type checking
169+
npm run typecheck
170+
```

0 commit comments

Comments
 (0)