diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 3e811fa8..00000000 --- a/.eslintignore +++ /dev/null @@ -1,13 +0,0 @@ -# /node_modules/* and /bower_components/* in the project root are ignored by default - -# Ignore built files -browser/ - -# Ignore generated coverage directory -coverage/ - -# Ignore documentation site scaffold -documentation/ - -# Ignore local testing script -test.js diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index fe28b0d0..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,76 +0,0 @@ -module.exports = { - 'env': { - 'es6': true, - 'node': true, - }, - 'extends': 'eslint:recommended', - 'parserOptions': { - 'ecmaVersion': 2018, - }, - 'rules': { - 'array-bracket-spacing': [ 'error', 'always' ], - 'arrow-parens': [ 'error', 'as-needed', { 'requireForBlockBody': true } ], - 'arrow-spacing': [ 'error', { - 'before': true, - 'after': true, - } ], - 'block-spacing': [ 'error' ], - 'comma-dangle': [ 'error', 'always-multiline' ], - 'comma-spacing': [ 'error', { - 'before': false, - 'after': true, - } ], - 'eol-last': [ 'error', 'unix' ], - 'eqeqeq': [ 'error' ], - 'func-call-spacing': [ 'error' ], - 'indent': [ 'error', 'tab' ], - 'key-spacing': [ 'error', { - 'beforeColon': false, - 'afterColon': true, - } ], - 'linebreak-style': [ 'error', 'unix' ], - 'no-console': [ 'warn' ], - 'no-mixed-spaces-and-tabs': [ 'error', 'smart-tabs' ], - 'no-multiple-empty-lines': [ 'error', { - 'max': 1, - } ], - 'no-var': [ 'error' ], - 'object-curly-newline': [ 'error', { - 'ObjectExpression': { - 'consistent': true, - 'minProperties': 2, - 'multiline': true, - }, - 'ObjectPattern': { - 'consistent': true, - 'multiline': true, - }, - 'ImportDeclaration': { - 'consistent': true, - 'multiline': true, - }, - 'ExportDeclaration': { - 'consistent': true, - 'minProperties': 2, - 'multiline': true, - }, - } ], - 'object-curly-spacing': [ 'error', 'always' ], - 'object-property-newline': [ 'error' ], - 'prefer-arrow-callback': [ 'error' ], - 'prefer-const': [ 'error' ], - 'quotes': [ 'error', 'single' ], - 'semi': [ 'error', 'always' ], - 'semi-spacing': [ 'error', { - 'before': false, - 'after': true, - } ], - 'space-before-function-paren': [ 'error', { - 'anonymous': 'never', - 'asyncArrow': 'always', - 'named': 'never', - } ], - 'space-in-parens': [ 'error', 'always' ], - 'yoda': [ 'error', 'never' ], - }, -}; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..f5ff69ed --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,93 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + +jobs: + # tsdown/rolldown require Node >=22.18 to run; vitest requires >=20. The + # published library itself only requires Node >=18 (see `engines` in + # package.json) — that promise is checked separately by `dist-smoke` below, + # against the pre-built output, without needing the build tooling to run. + test: + name: Lint, typecheck, build & unit test (Node ${{ matrix.node-version }}) + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [ 22, 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 + - 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 + + dist-smoke: + name: Published dist works on Node ${{ matrix.node-version }} + runs-on: ubuntu-latest + needs: test + strategy: + matrix: + node-version: [ 18, 20 ] + 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: 22 + 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..6f631309 --- /dev/null +++ b/.github/workflows/release.yml @@ -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 }} diff --git a/.gitignore b/.gitignore index b4170884..da4c79ef 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..340e342d --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "useTabs": true, + "singleQuote": true, + "semi": true, + "trailingComma": "all", + "printWidth": 90 +} diff --git a/.scratchpad/type-consumer/README.md b/.scratchpad/type-consumer/README.md new file mode 100644 index 00000000..a2cb9e84 --- /dev/null +++ b/.scratchpad/type-consumer/README.md @@ -0,0 +1,15 @@ +# 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). + +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 3 errors +``` diff --git a/.scratchpad/type-consumer/consumer.cts b/.scratchpad/type-consumer/consumer.cts new file mode 100644 index 00000000..f375d911 --- /dev/null +++ b/.scratchpad/type-consumer/consumer.cts @@ -0,0 +1,20 @@ +// 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 { + 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() ); diff --git a/.scratchpad/type-consumer/consumer.mts b/.scratchpad/type-consumer/consumer.mts new file mode 100644 index 00000000..3d7bcaa9 --- /dev/null +++ b/.scratchpad/type-consumer/consumer.mts @@ -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 { + const posts = await wp.posts(); + console.log( posts, url ); +} +run(); diff --git a/.scratchpad/type-consumer/negative.cts b/.scratchpad/type-consumer/negative.cts new file mode 100644 index 00000000..b7790e6d --- /dev/null +++ b/.scratchpad/type-consumer/negative.cts @@ -0,0 +1,6 @@ +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 } ); diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index eec9a381..00000000 --- a/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -language: node_js -script: "npm run test:ci" -node_js: - - "node" - - "lts/*" - - "10" - - "8" -# Opt-in to travis container infrastructure -sudo: false diff --git a/.wp-env.json b/.wp-env.json new file mode 100644 index 00000000..3ee33a35 --- /dev/null +++ b/.wp-env.json @@ -0,0 +1,15 @@ +{ + "port": 2747, + "testsEnvironment": false, + "config": { + "SCRIPT_DEBUG": true, + "WP_DEBUG_DISPLAY": false, + "WP_DEBUG": true, + "WP_DEVELOPMENT_MODE": "plugin" + }, + "plugins": [ + ".", + "https://downloads.wordpress.org/plugin/query-monitor.zip", + "https://github.com/WP-API/Basic-Auth/archive/refs/heads/master.zip" + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index d42793db..73fb999c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ # Changelog -## v2.0.0 [**alpha**] _Second Toughest in the Infants_ +## v2.0.0 [**unreleased**] _Second Toughest in the Infants_ - **BREAKING**: "Node-style" error-first callbacks (_e.g._ `.get( ( err, data ) => {} )`) are no longer supported. All transport request methods return Promises. -- **BREAKING**: The module exported as `wpapi` no longer includes HTTP methods. Install `superagent` as a peer dependency and `require( 'wpapi/superagent' )` in order to make HTTP requests. +- **BREAKING**: The minimum supported Node.js version is now v18; Internet Explorer is no longer supported. +- **BREAKING**: HTTP requests are made with the native `fetch` API. The superagent transport and the `wpapi/superagent` entrypoint introduced in the v2 alphas are removed; the default `wpapi` export makes HTTP requests out of the box again, with no additional dependencies. `wpapi/fetch` remains as an alias for the default export. +- **BREAKING**: Media uploads use native `FormData`/`Blob`. `.file()` accepts a file path (Node), a `Buffer` or `Blob` plus a file name, or a `File` object; streams are no longer supported. The multipart part's content type is no longer inferred from the file name (WordPress determines the type server-side from the name); pass a `Blob` or `File` with an explicit `type` to control it. - **BREAKING**: Autodiscovery now either succeeds or fails; a WPAPI instance configured with default routes will no longer be returned. diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index 2f8ebe84..00000000 --- a/Gruntfile.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * The Gruntfile in this project is not responsible for the code build or - * linting, and instead handles all tasks related to documentation generation - * and output. - */ -'use strict'; - -module.exports = function( grunt ) { - grunt.initConfig( { - pkg: grunt.file.readJSON( 'package.json' ), - } ); - - // Individual tasks are defined within build/grunt - grunt.loadTasks( 'build/grunt' ); - - grunt.registerTask( 'docs', [ - 'clean', - 'generate_readme_docs', - 'zip', - ] ); -}; diff --git a/README.md b/README.md index 3668a032..48cc2fde 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,9 @@ To get started, `npm install wpapi` or [download the browser build](https://wp-a ## Installation -`node-wpapi` works both on the server or in the browser. Node.js version 8.6 or higher (or version 8.2.1 with the `--harmony` flag) is required, and the latest LTS release is recommended. +`node-wpapi` works both on the server or in the browser. Node.js version 18 or higher is required, and the latest LTS release is recommended. -In the browser `node-wpapi` officially supports the latest two versions of all evergreen browsers, and Internet Explorer 11. +In the browser `node-wpapi` officially supports the latest two versions of all evergreen browsers. ### Install with NPM @@ -54,47 +54,35 @@ To use the library from Node, install it with [npm](http://npmjs.org): npm install --save wpapi ``` -Then, within your application's script files, `require` the module to gain access to it. As `wpapi` is both a query builder and a transport layer (_i.e._ a tool for getting and sending HTTP requests), we leave it up to you as the author of your application whether you need both parts of this functionality. You may use `wpapi` with [superagent](https://www.npmjs.com/package/superagent) if you wish to send and receive HTTP requests using this library, but you may also use only the query builder part of the library if you intend to submit your HTTP requests with `fetch`, `axios` or other tools. - -To import only the query builder (without the `.get()`, `.create()`, `.delete()`, `.update()` or `.then()` chaining methods): - -```javascript -var WPAPI = require( 'wpapi' ); -``` - -To import the superagent bundle, which contains the full suite of HTTP interaction methods: +Then, within your application's script files, `require` or `import` the module to gain access to it: ```js -var WPAPI = require( 'wpapi/superagent' ); +const WPAPI = require( 'wpapi' ); +// or +import WPAPI from 'wpapi'; ``` -This library is designed to work in the browser as well, via a build system such as Browserify or Webpack; just install the package and `require( 'wpapi' )` (or `'wpapi/superagent'`) from your application code. +HTTP requests are made with the `fetch` API built into Node 18+, browsers and other modern JavaScript runtimes, so no other dependencies are required. This library is designed to work in the browser as well, via a build system such as Vite or Webpack; just install the package and import it from your application code. ### Download the UMD Bundle Alternatively, you may download a [ZIP archive of the bundled library code](https://wp-api.github.io/node-wpapi/wpapi.zip). These files are UMD modules, which may be included directly on a page using a regular `