Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
1eed6a7
Add wp-env local environment
kadamwhite Jul 13, 2026
2bd18a1
Modernization plan: Put a stake in the ground to begin work
kadamwhite Jul 13, 2026
e56f285
Add back-compat considerations to migration plan
kadamwhite Jul 13, 2026
67c2afb
Add phased modernization execution plan
kadamwhite Jul 13, 2026
11ab55b
Add deterministic wp-env seeding for integration tests
kadamwhite Jul 13, 2026
9429975
Retarget integration tests at wp-env; refresh expectations for modern WP
kadamwhite Jul 13, 2026
b3a7438
Handle modern WP self-link shape in discover(); skip discover suite
kadamwhite Jul 13, 2026
81a811c
docs: add handoff.md; note block/FSE test-data expansion in backlog
kadamwhite Jul 13, 2026
9480518
Merge phase 0: green test baseline against wp-env
kadamwhite Jul 13, 2026
4113576
plan: make the GitHub Actions migration explicit
kadamwhite Jul 13, 2026
a2b3c43
build: replace webpack + Grunt + Babel with tsdown
kadamwhite Jul 14, 2026
e41ca7b
test: migrate Jest to Vitest
kadamwhite Jul 14, 2026
c67129f
lint: ESLint 4 + .eslintrc.js -> ESLint 9 flat config + Prettier
kadamwhite Jul 14, 2026
3431dc8
lint: apply eslint --fix and fix real eslint:recommended findings
kadamwhite Jul 14, 2026
e51eb98
chore: Node 18+ floor, drop IE11, commit lockfile, retire Travis
kadamwhite Jul 14, 2026
5c0953c
ci: add GitHub Actions ci.yml, scaffold release.yml
kadamwhite Jul 14, 2026
d8f2c37
docs: update handoff.md for Phase 1 completion
kadamwhite Jul 14, 2026
715d5fe
Merge phase 1: modern toolchain (still JS)
kadamwhite Jul 14, 2026
7400033
test: describe the Phase 2 transport contract (currently failing)
kadamwhite Jul 15, 2026
a27add2
fetch: rewrite transport on the platform-native fetch and FormData
kadamwhite Jul 15, 2026
892954d
superagent: remove the transport; stub the subpath with a migration e…
kadamwhite Jul 15, 2026
18e11c2
build: bind the fetch transport into the main export
kadamwhite Jul 15, 2026
4a52b87
php: fix enqueue hook name and __FILE__ constant
kadamwhite Jul 15, 2026
a32b33d
docs: update transport documentation for the fetch-based default
kadamwhite Jul 15, 2026
bdd6594
fetch: restore client-side method guard, reject HEAD errors, browser-…
kadamwhite Jul 15, 2026
7220ab0
wp-request: type-check .file() attachments; carry File names on Node 18
kadamwhite Jul 15, 2026
50906c1
build: alias wpapi/fetch to the main bundle; ship interim typings
kadamwhite Jul 15, 2026
b0070f8
chore: remaining review-pass fixes
kadamwhite Jul 15, 2026
28cfbbe
docs: update handoff.md for Phase 2 completion
kadamwhite Jul 15, 2026
75bda75
Merge phase 2: transport modernization + superagent removal
kadamwhite Jul 15, 2026
031d5dc
build(ts): enable strict type checking across the source graph
kadamwhite Jul 15, 2026
55888fa
lib/util: convert to TypeScript
kadamwhite Jul 15, 2026
38fa299
lib: convert pagination, path-part-setter, and mixins to TypeScript
kadamwhite Jul 15, 2026
5b81e20
lib: convert the route pipeline core to TypeScript
kadamwhite Jul 15, 2026
edc758e
lib/constructors/wp-request: convert to TypeScript
kadamwhite Jul 15, 2026
8d296eb
entries + transport: convert to TypeScript (source conversion complete)
kadamwhite Jul 16, 2026
82c9d79
build: ship tsdown's generated declarations; drop the interim hand-wr…
kadamwhite Jul 16, 2026
9415392
docs: update handoff.md for Phase 3 completion; drop a stale comment
kadamwhite Jul 16, 2026
fff9fce
Merge phase 3: TypeScript migration
kadamwhite Jul 16, 2026
747a8ee
fetch-transport: don't pass an undefined filename to FormData.append
kadamwhite Jul 16, 2026
c066a2a
Merge fix: FormData filename coercion on Node 18-22
kadamwhite Jul 16, 2026
7302cd1
docs: record the Node 22 FormData filename fix in handoff.md
kadamwhite Jul 16, 2026
855aaff
deps: remove parse-link-header
kadamwhite Jul 16, 2026
11ad08b
chore: raise the Node.js floor to v24
kadamwhite Jul 16, 2026
f56d854
Merge: remove parse-link-header, raise Node floor to 24
kadamwhite Jul 16, 2026
5d929e7
bench: add default-mode startup benchmark script
kadamwhite Jul 16, 2026
c0e7760
route-tree: carry validation pattern as node data
kadamwhite Jul 16, 2026
c8c8f1a
build: precompute the default route tree
kadamwhite Jul 16, 2026
97b7583
wpapi: bootstrap default mode from the precomputed route tree
kadamwhite Jul 16, 2026
e731145
types: generate handler declarations from the precompute pass
kadamwhite Jul 16, 2026
d8acb79
ci: fail when the precomputed route files are stale
kadamwhite Jul 16, 2026
8d58a67
types: fix handler base type degrading to any in bundled declarations
kadamwhite Jul 16, 2026
98e3f39
review: minify generated tree JSON, fix stale comments
kadamwhite Jul 16, 2026
7f8d6f7
docs: record Phase 4 in handoff
kadamwhite Jul 16, 2026
a67327e
Merge: Phase 4 build-time route precompute
kadamwhite Jul 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions .eslintignore

This file was deleted.

76 changes: 0 additions & 76 deletions .eslintrc.js

This file was deleted.

97 changes: 97 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: CI

on:
push:
branches: [ main ]
pull_request:

jobs:
test:
name: Lint, typecheck, build & unit test (Node ${{ matrix.node-version }})
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [ 24, latest ]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run typecheck
# The precomputed route tree and handler typings are generated from
# default-routes.json and committed; fail if they are stale.
- run: |
npm run precompute-routes
git diff --exit-code lib/data/default-route-tree.json lib/data/default-route-handlers.ts
- run: npm run build
- run: npm run test:unit
- if: matrix.node-version == 'latest'
uses: actions/upload-artifact@v4
with:
name: dist
path: dist
retention-days: 1

# Smokes the built artifact under a production-only install (no devDependencies),
# which the test job above cannot catch: it verifies dist/ is self-contained and
# requires nothing outside the declared runtime dependencies.
dist-smoke:
name: Published dist works on Node ${{ matrix.node-version }}
runs-on: ubuntu-latest
needs: test
strategy:
matrix:
node-version: [ 24 ]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm ci --omit=dev
- uses: actions/download-artifact@v4
with:
name: dist
path: dist
- run: |
node -e "
const WPAPI = require('./dist/index.cjs');
const wp = new WPAPI({ endpoint: 'http://localhost/wp-json' });
if (typeof wp.posts !== 'function') throw new Error('CJS entry did not bootstrap');
if (typeof WPAPI.transport.get !== 'function') throw new Error('CJS entry has no bound transport');
"
node --input-type=module -e "
import WPAPI from './dist/index.mjs';
const wp = new WPAPI({ endpoint: 'http://localhost/wp-json' });
if (typeof wp.posts !== 'function') throw new Error('ESM entry did not bootstrap');
if (typeof WPAPI.transport.get !== 'function') throw new Error('ESM entry has no bound transport');
"
node -e "
try { require('./dist/superagent.cjs'); }
catch (e) { if (/wpapi.superagent was removed/.test(e.message)) process.exit(0); throw e; }
throw new Error('superagent stub did not throw its migration error');
"

integration:
name: Integration tests (wp-env)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run env:start
- run: npm run env:seed
- run: npm run test:integration
# `npm run env:logs` defaults to `--watch`, which never exits; pass
# `--no-watch` directly so this step prints once and completes.
- if: always()
run: npx @wordpress/env logs development --no-watch
- if: always()
run: npm run env:stop
30 changes: 30 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Release

# Scaffolded in Phase 1, replacing the local `release-npm` script; not yet
# exercised until publishing resumes (requires an `NPM_TOKEN` secret).
on:
release:
types: [ published ]

jobs:
publish:
name: Build, test & publish to npm
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
# tsdown/rolldown require Node >=22.18 to run the build itself.
node-version: 22
cache: npm
registry-url: https://registry.npmjs.org
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm run build
- run: npm run test:unit
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
7 changes: 2 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,5 @@ documentation/index.html
*.sublime-*
.ruby-version

# Built files (for use in browser)
browser/

# Consuming applications should maintain their own lockfile.
package-lock.json
# Built files (tsdown output: ESM/CJS/UMD + .d.ts)
dist/
7 changes: 7 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Route data: default-routes.json is machine-fetched from a live WP site;
# default-route-tree.json and default-route-handlers.ts are generated from it
# by build/scripts/precompute-default-routes.js. Formatting any of these would
# break the CI staleness check for the generated files.
lib/data/default-routes.json
lib/data/default-route-tree.json
lib/data/default-route-handlers.ts
7 changes: 7 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"useTabs": true,
"singleQuote": true,
"semi": true,
"trailingComma": "all",
"printWidth": 90
}
69 changes: 69 additions & 0 deletions .scratchpad/bench-startup.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Benchmark default-mode WPAPI startup cost against the built dist/ output.
// Usage: npm run build && node .scratchpad/bench-startup.mjs [samples]
//
// Reports, across N fresh node child processes:
// - load: time to require('dist/index.cjs')
// - init: time for the first default-mode `new WPAPI(...)` (route parse + factories)
// - init2: time for a second instantiation (factories already cached)
// Plus an in-process micro-benchmark of the default-route bootstrap work itself.

import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import path from 'node:path';

const repoRoot = path.resolve( path.dirname( fileURLToPath( import.meta.url ) ), '..' );
const samples = parseInt( process.argv[ 2 ], 10 ) || 30;

const childScript = `
const t0 = performance.now();
const WPAPI = require( ${ JSON.stringify( path.join( repoRoot, 'dist/index.cjs' ) ) } );
const t1 = performance.now();
new WPAPI( { endpoint: 'http://localhost/wp-json' } );
const t2 = performance.now();
new WPAPI( { endpoint: 'http://localhost/wp-json' } );
const t3 = performance.now();
console.log( JSON.stringify( { load: t1 - t0, init: t2 - t1, init2: t3 - t2 } ) );
`;

const results = [];
for ( let i = 0; i < samples; i++ ) {
const out = execFileSync( process.execPath, [ '-e', childScript ], { encoding: 'utf8' } );
results.push( JSON.parse( out ) );
}

const stats = ( key ) => {
const values = results.map( r => r[ key ] ).sort( ( a, b ) => a - b );
const mean = values.reduce( ( a, b ) => a + b, 0 ) / values.length;
const median = values[ Math.floor( values.length / 2 ) ];
return { mean, median, min: values[ 0 ], max: values[ values.length - 1 ] };
};

const fmt = ( s ) => `median ${ s.median.toFixed( 3 ) }ms mean ${ s.mean.toFixed( 3 ) }ms min ${ s.min.toFixed( 3 ) }ms max ${ s.max.toFixed( 3 ) }ms`;

console.log( `samples: ${ samples } (fresh node process each)` );
console.log( `require('dist/index.cjs') ${ fmt( stats( 'load' ) ) }` );
console.log( `first new WPAPI() (parse) ${ fmt( stats( 'init' ) ) }` );
console.log( `second new WPAPI() (cached) ${ fmt( stats( 'init2' ) ) }` );

// In-process micro-benchmark: repeat the first-instantiation bootstrap work by
// clearing the module cache so the lazy default-factory cache is rebuilt.
const microScript = `
const distPath = ${ JSON.stringify( path.join( repoRoot, 'dist/index.cjs' ) ) };
const ITERATIONS = 200;
// Warm up disk cache / JIT with one throwaway pass.
let WPAPI = require( distPath );
new WPAPI( { endpoint: 'http://localhost/wp-json' } );
const times = [];
for ( let i = 0; i < ITERATIONS; i++ ) {
delete require.cache[ require.resolve( distPath ) ];
WPAPI = require( distPath );
const t0 = performance.now();
new WPAPI( { endpoint: 'http://localhost/wp-json' } );
times.push( performance.now() - t0 );
}
times.sort( ( a, b ) => a - b );
const mean = times.reduce( ( a, b ) => a + b, 0 ) / times.length;
console.log( JSON.stringify( { mean, median: times[ Math.floor( times.length / 2 ) ] } ) );
`;
const micro = JSON.parse( execFileSync( process.execPath, [ '-e', microScript ], { encoding: 'utf8' } ) );
console.log( `bootstrap micro (200 iter) median ${ micro.median.toFixed( 3 ) }ms mean ${ micro.mean.toFixed( 3 ) }ms` );
17 changes: 17 additions & 0 deletions .scratchpad/type-consumer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Shipped-declaration consumer check

Strict-mode consumers exercising the generated `dist/*.d.*` typings after
`npm run build`. `consumer.cts`/`consumer.mts` must typecheck clean (require +
import paths, constructability, statics, chaining); every line flagged in
`negative.cts` must error (proves the types reject bad code, which is what
regressed in Phase 2). The `wp.posts()` negative lines guard the Phase 4
generated handler typings: if the declaration bundle ever degrades those
handlers back to `any`, the positives stay clean but these stop erroring.

Usage:

```
npx tsc --strict --noEmit --skipLibCheck --module node16 --moduleResolution node16 \
--target es2020 .scratchpad/type-consumer/consumer.cts .scratchpad/type-consumer/consumer.mts
npx tsc --strict --noEmit ... .scratchpad/type-consumer/negative.cts # expect 5 errors
```
34 changes: 34 additions & 0 deletions .scratchpad/type-consumer/consumer.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Strict-mode consumer exercising the shipped CJS declarations (require path).
import WPAPI = require( '../../dist/index.cjs' );

const wp = new WPAPI( { endpoint: 'http://example.com/wp-json' } );
const viaSite = WPAPI.site( 'http://example.com/wp-json' );
const req = wp.posts().perPage( 5 ).page( 2 );
const url: string = req.toString();
const chained = wp.url( 'http://x/' ).auth( { username: 'u', password: 'p' } );
async function run(): Promise<void> {
const posts = await wp.posts();
console.log( posts, url, viaSite, chained );
const discovered = await WPAPI.discover( 'http://example.com' );
console.log( discovered.pages() );
}
run();

// The wpapi/fetch alias must expose the same constructable type.
import WPAPIFetch = require( '../../dist/fetch.cjs' );
const wpF = new WPAPIFetch( { endpoint: 'http://example.com/wp-json' } );
console.log( wpF.media().toString() );

// Generated default-route handler typings (Phase 4): mixins and path-part
// setters chain with WPRequest methods, and namespace() is overloaded for
// the default namespaces.
const typedChain: string = wp.posts()
.categories( [ 1, 2 ] )
.sticky( true )
.id( 7 )
.toString();
console.log( typedChain, wp.pages().parent( 3 ).revisions( 12 ) );
console.log( wp.namespace( 'wp/v2' ).users().me() );
console.log( wp.namespace( 'oembed/1.0' ).proxy().param( 'url', 'http://x' ) );
// Handlers registered from custom route data still pass the index signature.
console.log( wp.namespace( 'someplugin/v1' ).customResource() );
11 changes: 11 additions & 0 deletions .scratchpad/type-consumer/consumer.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Strict-mode consumer exercising the shipped ESM declarations (import path).
import WPAPI from '../../dist/index.mjs';

const wp = new WPAPI( { endpoint: 'http://example.com/wp-json' } );
const req = wp.posts().perPage( 5 );
const url: string = req.toString();
async function run(): Promise<void> {
const posts = await wp.posts();
console.log( posts, url );
}
run();
8 changes: 8 additions & 0 deletions .scratchpad/type-consumer/negative.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import WPAPI = require( '../../dist/index.cjs' );
const wp = new WPAPI( { endpoint: 'http://example.com/wp-json' } );
// Each line below should be a type error:
wp.url( 'x' ).nonexistentMethod();
const n: number = wp.url( 'x' ).toString();
wp.url( 'x' ).auth( { username: 42 } );
wp.posts().nonexistentMethod();
wp.posts().sticky( 'yes' );
Loading
Loading