{"_id":"@artisanpack-ui/hooks-js","name":"@artisanpack-ui/hooks-js","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@artisanpack-ui/hooks-js","version":"1.0.0","description":"WordPress-style actions and filters for JavaScript, with a React adapter.","license":"MIT","author":{"name":"Jacob Martella","email":"me@jacobmartella.com"},"repository":{"type":"git","url":"git+https://github.com/ArtisanPack-UI/hooks-js.git"},"bugs":{"url":"https://github.com/ArtisanPack-UI/hooks-js/issues"},"homepage":"https://github.com/ArtisanPack-UI/hooks-js#readme","type":"module","sideEffects":["./dist/index.cjs","./dist/index.mjs"],"main":"./dist/index.cjs","module":"./dist/index.mjs","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.cjs"},"./react":{"types":"./dist/react.d.ts","import":"./dist/react.mjs","require":"./dist/react.cjs"}},"engines":{"node":">=20.0.0"},"scripts":{"build":"tsup","clean":"rm -rf dist","lint":"eslint \"src/**/*.{ts,tsx}\"","format":"prettier --write \"src/**/*.{ts,tsx}\"","format:check":"prettier --check \"src/**/*.{ts,tsx}\"","type-check":"tsc --noEmit","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run clean && npm run build"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0"},"peerDependenciesMeta":{"react":{"optional":true}},"devDependencies":{"@eslint/js":"^9.39.4","@testing-library/jest-dom":"^6.9.1","@testing-library/react":"^16.3.2","@types/react":"^19.2.14","@types/react-dom":"^19.2.3","@vitejs/plugin-react":"^5.2.0","@vitest/coverage-v8":"^4.1.10","eslint":"^9.39.4","eslint-plugin-react-hooks":"^7.0.1","jsdom":"^29.0.1","prettier":"^3.8.1","react":"^19.2.4","react-dom":"^19.2.4","tsup":"^8.5.1","typescript":"^5.9.3","typescript-eslint":"^8.57.2","vitest":"^4.1.2"},"publishConfig":{"access":"public"},"_id":"@artisanpack-ui/hooks-js@1.0.0","gitHead":"f29382d9f1aa9ef03362fbef0f404613b93076af","_nodeVersion":"22.23.1","_npmVersion":"10.9.8","dist":{"integrity":"sha512-tMEFy3/tpjhOAlcIkkAzpHAfkNtVS2zA0hys1ITbHpu2cgne3bmD+F6iUbYH7DC3crDRi/bXmKsW10MxoFEEoA==","shasum":"6ee6587f143ae20dc675243be6e193f25ea1bd6f","tarball":"https://registry.npmjs.org/@artisanpack-ui/hooks-js/-/hooks-js-1.0.0.tgz","fileCount":18,"unpackedSize":188809,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@artisanpack-ui%2fhooks-js@1.0.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCJjHun3nYWPnu2K0t6Js3Wg8B59KXwAE5KfCqAALGncQIhAOcDtaabTJh+xC6rWAQFA+Jh5ck3V0HUeiajLD/JIqQM"}]},"_npmUser":{"name":"viewfromthebox","email":"viewfromthebox94@gmail.com"},"directories":{},"maintainers":[{"name":"viewfromthebox","email":"viewfromthebox94@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/hooks-js_1.0.0_1785192442679_0.6146785409722926"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-27T22:47:22.473Z","1.0.0":"2026-07-27T22:47:22.835Z","modified":"2026-07-27T22:47:23.213Z"},"maintainers":[{"name":"viewfromthebox","email":"viewfromthebox94@gmail.com"}],"description":"WordPress-style actions and filters for JavaScript, with a React adapter.","homepage":"https://github.com/ArtisanPack-UI/hooks-js#readme","repository":{"type":"git","url":"git+https://github.com/ArtisanPack-UI/hooks-js.git"},"author":{"name":"Jacob Martella","email":"me@jacobmartella.com"},"bugs":{"url":"https://github.com/ArtisanPack-UI/hooks-js/issues"},"license":"MIT","readme":"# @artisanpack-ui/hooks-js\n\nWordPress-style actions and filters for JavaScript, with a first-class React adapter.\n\nPart of the [ArtisanPack UI](https://github.com/ArtisanPack-UI) ecosystem. This package is the JavaScript counterpart to [`artisanpack-ui/hooks`](https://github.com/ArtisanPack-UI/hooks) for PHP — same naming convention (`ap.<domain>.<event>`, dot-notation lowerCamelCase), same semantics for priorities, dedup, and deprecation aliases.\n\n- **Framework-agnostic core** with zero runtime dependencies.\n- **Optional React adapter** on a dedicated subpath (`/react`) so non-React consumers pay nothing.\n- **Module-Federation-safe** — a shared `globalThis` singleton means duplicate copies still find each other's callbacks, and `singleton: true` in your bundler config works out of the box.\n- **Deprecation aliases** (`deprecateHook`) let you rename hooks without breaking existing subscribers.\n- **SSR-safe.** No `window` access on import; React hooks use `useSyncExternalStore` with a safe server snapshot.\n\nFor long-form docs — API reference, migration guide, testing recipes — see [`/docs`](./docs/home.md) or the linked pages below.\n\n## Table of contents\n\n- [Installation](#installation)\n- [Quickstart](#quickstart)\n- [API reference](#api-reference)\n  - [Actions](#actions)\n  - [Filters](#filters)\n  - [Deprecation aliases](#deprecation-aliases)\n  - [Introspection](#introspection)\n- [React adapter](#react-adapter)\n- [Module Federation](#module-federation)\n  - [`globalThis.ApHooks` escape hatch](#globalthisaphooks-escape-hatch)\n  - [Plugin `bootModule` recipe](#plugin-bootmodule-recipe)\n- [DevTools debug mode](#devtools-debug-mode)\n- [Hook naming convention](#hook-naming-convention)\n- [Migrating from `@wordpress/hooks`](#migrating-from-wordpresshooks)\n- [Cross-links](#cross-links)\n- [Development](#development)\n- [License](#license)\n\n## Installation\n\n```bash\nnpm install @artisanpack-ui/hooks-js\n```\n\nRequires Node.js 20+ for local development. React 18 or 19 is an optional peer dependency, needed only if you import from `@artisanpack-ui/hooks-js/react`.\n\n## Quickstart\n\n```ts\nimport { addAction, doAction, addFilter, applyFilters } from '@artisanpack-ui/hooks-js';\n\n// Actions — fire-and-forget event triggers\naddAction('order.placed', (order) => {\n  // Send email, enqueue a job, log, etc.\n});\n\ndoAction('order.placed', order);\n\n// Filters — value transformations\naddFilter('price.display', (price: string, currency: string) => `${currency} ${price}`);\n\nconst display = applyFilters('price.display', '49.00', 'USD'); // \"USD 49.00\"\n```\n\nReact consumers get a set of hooks and one component:\n\n```tsx\nimport { useFilter, useAction, HookSlot } from '@artisanpack-ui/hooks-js/react';\n\nfunction NavBar({ items }: { items: NavItem[] }) {\n  const filtered = useFilter<NavItem[]>('nav.items', items);\n  return <nav>{filtered.map((item) => <a href={item.href} key={item.href}>{item.label}</a>)}</nav>;\n}\n```\n\n## API reference\n\nThe core entry point exports:\n\n```ts\nimport {\n  // Actions\n  addAction, doAction, removeAction, removeAllActions, hasAction,\n  // Filters\n  addFilter, applyFilters, removeFilter, removeAllFilters, hasFilter,\n  // Introspection\n  hasHook, VERSION,\n  // Deprecations\n  deprecateHook, hasAliases, aliasesFor, resetDeprecationLogState,\n  // Types\n  type HookCallback,\n  type DeprecationLevel,\n} from '@artisanpack-ui/hooks-js';\n```\n\n`HookCallback` is `(...args: unknown[]) => unknown`.\n\n### Actions\n\n| Function                                                         | Returns   | Notes                                                       |\n| ---------------------------------------------------------------- | --------- | ----------------------------------------------------------- |\n| `addAction(hook, callback, priority = 10)`                       | `void`    | Registers a callback. See [[Priorities and Execution Order|priorities]].                            |\n| `doAction(hook, ...args)`                                        | `void`    | Fires every callback in priority order; return values ignored. |\n| `removeAction(hook, callback, priority = 10)`                    | `boolean` | Removes one specific `(callback, priority)` match.          |\n| `removeAllActions(hook, priority = false)`                       | `boolean` | Removes all callbacks; `priority` scopes to one bucket.     |\n| `hasAction(hook)`                                                | `boolean` | True if any action callback is registered for the hook.     |\n\nCallbacks registered while `doAction` is walking the list are **deferred to the next dispatch** — this matches the PHP twin and prevents surprise recursion.\n\nFull page: [`docs/actions.md`](./docs/actions.md).\n\n### Filters\n\n| Function                                                         | Returns  | Notes                                                        |\n| ---------------------------------------------------------------- | -------- | ------------------------------------------------------------ |\n| `addFilter(hook, callback, priority = 10)`                       | `void`   | Registers a callback.                                        |\n| `applyFilters<T>(hook, value, ...args)`                          | `T`      | Threads `value` through every callback; returns the final result. Returns `value` unchanged when no callbacks are registered (no allocation). |\n| `removeFilter(hook, callback, priority = 10)`                    | `boolean`| Removes one specific `(callback, priority)` match.           |\n| `removeAllFilters(hook, priority = false)`                       | `boolean`| Removes all callbacks; `priority` scopes to one bucket.      |\n| `hasFilter(hook)`                                                | `boolean`| True if any filter callback is registered for the hook.      |\n\nEvery filter callback receives the current value as its first argument and must return the (possibly modified) value.\n\nFull page: [`docs/filters.md`](./docs/filters.md).\n\n### Deprecation aliases\n\nRename a hook without breaking existing subscribers.\n\n```ts\nimport { deprecateHook } from '@artisanpack-ui/hooks-js';\n\ndeprecateHook('order.placed', 'order.created');\n\n// Both of the following now attach to (or fire) `order.created`:\naddAction('order.placed', callback);\ndoAction('order.created', order);\n```\n\n- `addAction`/`addFilter` on the old name silently attach to the canonical name.\n- `doAction`/`applyFilters` on either name fire every callback registered under either — deduped by callable identity (`===`).\n- Chains collapse: `a→b` then `b→c` resolves `a→c`. Cycles throw.\n- Deprecation notices log once per alias per session at the level set by `window.__AP_HOOKS_DEPRECATION_LEVEL__` (`off` / `debug` / `info` / `warn` / `error`, defaults to `info`). Call `resetDeprecationLogState()` in long-lived processes to re-arm.\n\nHelpers: `hasAliases()`, `aliasesFor(canonical)`.\n\nFull page: [`docs/hook-naming-and-deprecations.md`](./docs/hook-naming-and-deprecations.md).\n\n### Introspection\n\n- `hasAction(hook)` / `hasFilter(hook)` — bucket-specific check.\n- `hasHook(hook)` — true if any action *or* filter callback exists.\n- `VERSION` — the package's semver string.\n\n## React adapter\n\nShips from `@artisanpack-ui/hooks-js/react`. React (18 or 19) is an optional peer dependency.\n\n```tsx\nimport {\n  useFilter,           // read a filtered value, re-render on mutation\n  useAction,           // fire an action from an effect, deep-compare args\n  useHookedChildren,   // thread ReactNode children through a filter\n  HookSlot,            // component form of the filter-a-value pattern\n} from '@artisanpack-ui/hooks-js/react';\n```\n\nAll hooks are backed by `useSyncExternalStore` on a per-hook version counter that bumps on every registry mutation — including changes on any reverse-alias bucket. They are safe under `<StrictMode>` and SSR.\n\nFull pages: [`docs/react.md`](./docs/react.md), [`docs/react/use-filter.md`](./docs/react/use-filter.md), [`docs/react/use-action.md`](./docs/react/use-action.md), [`docs/react/use-hooked-children.md`](./docs/react/use-hooked-children.md), [`docs/react/hook-slot.md`](./docs/react/hook-slot.md), [`docs/react/ssr.md`](./docs/react/ssr.md).\n\n## Module Federation\n\nHooks are only useful when host and remote agree on a single registry. Configure your bundler to share the package as a singleton:\n\n**`@originjs/vite-plugin-federation`**\n\n```ts\nfederation({\n  name: 'host',\n  shared: {\n    '@artisanpack-ui/hooks-js':       { singleton: true, strictVersion: false },\n    '@artisanpack-ui/hooks-js/react': { singleton: true, strictVersion: false },\n  },\n});\n```\n\n**webpack / Rspack `ModuleFederationPlugin`**\n\n```js\nnew ModuleFederationPlugin({\n  name: 'host',\n  shared: {\n    '@artisanpack-ui/hooks-js':       { singleton: true, requiredVersion: false },\n    '@artisanpack-ui/hooks-js/react': { singleton: true, requiredVersion: false },\n  },\n});\n```\n\nEven without a shared config, duplicate copies find each other via a `Symbol.for('@artisanpack-ui/hooks-js/singleton')` slot on `globalThis` — but explicit singleton config is preferred.\n\n### `globalThis.ApHooks` escape hatch\n\nIf a remote is loaded *outside* your bundler graph (browser extension, runtime-injected `<script>`, plugin that cannot patch its federation config), it still needs a way to reach the host's registry. The package publishes its public API on `globalThis.ApHooks` on first import:\n\n```ts\nwindow.ApHooks.addAction('order.placed', (order) => sendReceipt(order));\nwindow.ApHooks.doAction('order.placed', order);\n```\n\n`window.ApHooks` and `import { addAction } from '@artisanpack-ui/hooks-js'` share the same underlying registry, even across duplicate copies of the package.\n\n### Plugin `bootModule` recipe\n\nRuntime-loaded plugins should register their hooks **before the first page mounts**. A dedicated `bootModule` makes the ordering explicit:\n\n```ts\n// plugin/src/boot.ts\nimport { addAction, addFilter } from '@artisanpack-ui/hooks-js';\n\nexport function bootModule(): void {\n  addAction('app.ready', () => { /* ... */ });\n  addFilter('nav.items', (items) => [...items, { label: 'My plugin', href: '/plugin' }]);\n}\n```\n\n```ts\n// host/src/bootstrap.ts\nimport { bootModule } from 'plugin/boot';\n\nasync function bootstrap(): Promise<void> {\n  bootModule();               // register hooks first\n  await import('./app');      // then mount the app\n}\nbootstrap();\n```\n\nFull page: [`docs/module-federation.md`](./docs/module-federation.md).\n\n## DevTools debug mode\n\nOpt into a per-dispatch console log by setting `window.__AP_HOOKS_DEBUG__ = true`:\n\n```\n[ApHooks] doAction(\"order.placed\") → 3 subscriber(s)\n  { hook: 'order.placed', kind: 'doAction', args: ['[Object]'], subscribers: 3 }\n```\n\nThe flag is read on every dispatch, so toggling it in DevTools takes effect immediately. `applyFilters` guards its argument-preview allocation behind the flag, so there is no cost when it's off. Leave it off in production.\n\nFull page: [`docs/devtools-debug.md`](./docs/devtools-debug.md).\n\n## Hook naming convention\n\n> Hook names should be namespaced with dot notation and lower camelCase segments — for example, `order.placed`, `user.registered`, `ap.icons.registerIconSets`.\n>\n> The `ap.<domain>.<event>` prefix is reserved for cross-package hooks that any ArtisanPack UI package (or downstream app) may listen to.\n\n(Verbatim from the [`artisanpack-ui/hooks` PHP README](https://github.com/ArtisanPack-UI/hooks#hook-naming-and-deprecations).)\n\nTwo of these are intentionally shared across the ecosystem today:\n\n- `ap.google.scopes` — filter for augmenting the requested Google OAuth scopes.\n- `ap.icons.registerIconSets` — filter for registering additional icon sets.\n\n## Migrating from `@wordpress/hooks`\n\nIf you're coming from `@wordpress/hooks`, the API surface will look familiar. Key differences:\n\n- **No namespace argument.** `addAction(hook, fn, priority)` — not `addAction(hook, ns, fn, priority)`. `removeAction` matches by `(callback, priority)`, not by string namespace.\n- **Priorities** default to `10` and follow the same \"lower runs first, FIFO within a priority\" rules. Negative priorities are allowed here (WP silently clamps).\n- **Alias handling is built in.** `deprecateHook('old', 'new')` replaces the manual \"dispatch both and warn\" pattern.\n- **React reactivity is first-class.** `useFilter` re-renders on registry mutations without needing a wrapper.\n- **SSR-safe.** No `window` access on import; server renders never subscribe.\n- **Globals are separate.** WP writes to `wp.hooks`; this package writes to `globalThis.ApHooks`. The two libraries do not see each other's callbacks.\n\nFull guide with a step-by-step migration checklist: [`docs/migration-from-wordpress-hooks.md`](./docs/migration-from-wordpress-hooks.md).\n\n## Cross-links\n\n- **PHP twin package**: [`artisanpack-ui/hooks`](https://github.com/ArtisanPack-UI/hooks) — the same primitives for Laravel, with Blade directives and Facades. If you're building a full-stack ArtisanPack UI app, you'll typically install both.\n- **Documentation site**: [`/docs/home.md`](./docs/home.md) — the tree of Markdown docs mirrored here.\n- **Issues and discussions**: <https://github.com/ArtisanPack-UI/hooks-js/issues>\n\n## Development\n\n```bash\nnpm install\nnpm run build           # tsup — produces dist/*.mjs, dist/*.cjs, dist/*.d.ts\nnpm test                # vitest run\nnpm run test:watch      # vitest in watch mode\nnpm run test:coverage   # v8 coverage report\nnpm run lint            # eslint\nnpm run type-check      # tsc --noEmit\nnpm run format          # prettier --write\n```\n\nSee [CONTRIBUTING.md](./CONTRIBUTING.md) for the full contribution workflow.\n\n## License\n\n[MIT](./LICENSE)\n","readmeFilename":"README.md","_rev":"1-a6d3c87fb2209f48d9b4897654175004"}