|
| 1 | +# Newspack Plugin: Agent Instructions |
| 2 | + |
| 3 | +This file covers what is specific to `newspack-plugin`. Shared conventions (Docker commands, `n` script, coding standards, git rules, etc.) are in the root `newspack-workspace/AGENTS.md`. |
| 4 | + |
| 5 | +## Linting Commands |
| 6 | + |
| 7 | +```bash |
| 8 | +npm run lint # JS + SCSS only (see gotchas) |
| 9 | +npm run lint:js # JavaScript/TypeScript linting |
| 10 | +npm run lint:scss # SCSS linting |
| 11 | +npm run lint:php # PHP linting (PHPCS) |
| 12 | +npm run fix:js # Auto-fix JS issues |
| 13 | +npm run fix:php # Auto-fix PHP issues (PHPCBF) |
| 14 | +``` |
| 15 | + |
| 16 | +## Common Gotchas |
| 17 | + |
| 18 | +- `npm run lint` runs JS + SCSS only. PHP linting requires a separate `npm run lint:php`. |
| 19 | +- After adding a new PHP file, run `composer dump-autoload` to update the classmap (Composer uses `classmap`, not PSR-4). |
| 20 | +- Individual JS test files cannot run independently. Always run `npm test` for the full suite. |
| 21 | +- Never import `react-router-dom` directly in source code. Use the proxy: `import Router from '../../packages/components/src/proxied-imports/router'`. Tests may import `react-router-dom` directly. |
| 22 | +- New standalone webpack entry points must import `src/shared/js/public-path.js` first. |
| 23 | +- Plugin integration classes in `includes/plugins/` use the root `Newspack` namespace despite living in subdirectories. |
| 24 | + |
| 25 | +## PHP Backend |
| 26 | + |
| 27 | +### Bootstrap & Autoloading |
| 28 | + |
| 29 | +- **`newspack.php`**: Main plugin file, defines constants (`NEWSPACK_ABSPATH`, `NEWSPACK_PLUGIN_FILE`, etc.), requires Composer autoloader and Action Scheduler. |
| 30 | +- **`includes/class-newspack.php`**: Singleton main class. Manually `include_once`s files in a specific order via `includes()`, then hooks `init()` methods. |
| 31 | +- **Autoloading**: Composer `classmap` strategy (not PSR-4). After adding a new PHP file, run `composer dump-autoload` to update the classmap. |
| 32 | + |
| 33 | +### Class Initialization Patterns |
| 34 | + |
| 35 | +New classes should follow the **static `init()` pattern** (dominant, used by 100+ classes): |
| 36 | + |
| 37 | +```php |
| 38 | +namespace Newspack; |
| 39 | + |
| 40 | +class My_Feature { |
| 41 | + public static function init() { |
| 42 | + add_action( 'init', [ __CLASS__, 'register_things' ] ); |
| 43 | + } |
| 44 | + // ... static methods ... |
| 45 | +} |
| 46 | +My_Feature::init(); |
| 47 | +``` |
| 48 | + |
| 49 | +Two other patterns exist but are legacy or special-purpose: |
| 50 | +- `new ClassName()` at file bottom (rare, ~7 classes like `API`, `Profile`) |
| 51 | +- `Newspack::instance()` singleton (only the main class) |
| 52 | + |
| 53 | +Classes that should not be extended are marked `final`. |
| 54 | + |
| 55 | +### Namespace Map |
| 56 | + |
| 57 | +| Namespace | Directory | |
| 58 | +|-----------|-----------| |
| 59 | +| `Newspack` | `includes/` (root, most classes) | |
| 60 | +| `Newspack\API` | `includes/api/` | |
| 61 | +| `Newspack\CLI` | `includes/cli/` | |
| 62 | +| `Newspack\Data_Events` | `includes/data-events/` | |
| 63 | +| `Newspack\Data_Events\Connectors` | `includes/data-events/connectors/` | |
| 64 | +| `Newspack\Reader_Activation` | `includes/reader-activation/` | |
| 65 | +| `Newspack\Reader_Activation\Sync` | `includes/reader-activation/sync/` | |
| 66 | +| `Newspack\Reader_Activation\Integrations` | `includes/reader-activation/integrations/` | |
| 67 | +| `Newspack\Wizards` | `includes/wizards/` | |
| 68 | +| `Newspack\Wizards\Newspack` | `includes/wizards/newspack/` | |
| 69 | +| `Newspack\Wizards\Traits` | `includes/wizards/traits/` | |
| 70 | +| `Newspack\Optional_Modules` | `includes/optional-modules/` | |
| 71 | +| `Newspack\Content_Gate` | `includes/content-gate/` | |
| 72 | +| `Newspack\Collections` | `includes/collections/` | |
| 73 | + |
| 74 | +### REST API |
| 75 | + |
| 76 | +Two namespace constants, defined in `includes/util.php`: |
| 77 | +- `NEWSPACK_API_NAMESPACE` = `newspack/v1` (primary) |
| 78 | +- `NEWSPACK_API_NAMESPACE_V2` = `newspack/v2` |
| 79 | + |
| 80 | +Three patterns for registering routes (in order of prevalence): |
| 81 | + |
| 82 | +1. **Wizard Section routes** (most common): Extend `Wizard_Section`, implement `register_rest_routes()` (auto-hooked to `rest_api_init`). Route pattern: `wizard/{wizard_slug}/{section}`. Permission: `$this->api_permissions_check()`. |
| 83 | + |
| 84 | +2. **Wizard routes**: Extend `Wizard`, implement `register_api_endpoints()`. Route pattern: `wizard/{slug}/...`. Permission: `$this->api_permissions_check()`. |
| 85 | + |
| 86 | +3. **Standalone controllers**: In `includes/api/`, extend `WP_REST_Controller`. Only 2 exist (`Plugins_Controller`, `Wizards_Controller`). |
| 87 | + |
| 88 | +Common sanitize callbacks from `util.php`: `Newspack\newspack_clean()`, `Newspack\newspack_string_to_bool()`. |
| 89 | + |
| 90 | +### Wizard System (PHP side) |
| 91 | + |
| 92 | +Two levels of abstraction: |
| 93 | + |
| 94 | +- **`Wizard`** (abstract, `includes/wizards/class-wizard.php`): Override `$slug`, `$capability`, `get_name()`. Renders an empty `<div>` for React hydration. Provides `api_permissions_check()`, completion tracking, admin menu registration. |
| 95 | + |
| 96 | +- **`Wizard_Section`** (abstract, `includes/wizards/class-wizard-section.php`): Modular sections within a wizard. Set `$wizard_slug`, implement `register_rest_routes()`. Used by 9+ section classes under `includes/wizards/newspack/` (Emails, Pixels, Social, etc.). |
| 97 | + |
| 98 | +For the frontend counterpart, see **Frontend > Wizard System** below. |
| 99 | + |
| 100 | +### Plugin Integrations |
| 101 | + |
| 102 | +Simple integrations live in single files: `includes/plugins/class-{plugin}.php`. Complex integrations span subdirectories: `includes/plugins/woocommerce/`, `includes/plugins/co-authors-plus/`, `includes/plugins/woocommerce-subscriptions/`, etc. |
| 103 | + |
| 104 | +Some integrations have corresponding Configuration Managers in `includes/configuration_managers/`. |
| 105 | + |
| 106 | +### Optional Modules |
| 107 | + |
| 108 | +Feature flags managed by `includes/optional-modules/class-optional-modules.php`. Check the class for the current module list. Enable/disable via `wp newspack optional-modules enable|disable|list`. |
| 109 | + |
| 110 | +### Settings & Data Storage |
| 111 | + |
| 112 | +| Mechanism | Convention | Examples | |
| 113 | +|-----------|-----------|----------| |
| 114 | +| `wp_options` | Prefixed per subsystem | `newspack_reader_activation_*`, `newspack_donation_*` | |
| 115 | +| Custom Post Types | Short prefix | `newspack_rr_email`, `np_content_gate` | |
| 116 | +| User meta | `np_` prefix | `np_reader`, `np_reader_email_verified` | |
| 117 | +| Feature flag constants | In `wp-config.php` | `NEWSPACK_CONTENT_GATES`, `NEWSPACK_LOG_LEVEL` | |
| 118 | + |
| 119 | +### Logging |
| 120 | + |
| 121 | +`Newspack\Logger` provides: |
| 122 | +- `Logger::log( $payload, $header, $type )`: gated by `NEWSPACK_LOG_LEVEL` constant (0 = off, 1 = basic, 2 = verbose). |
| 123 | +- `Logger::newspack_log( $code, $message, $data, $type )`: fires `newspack_log` action (consumed by Newspack Manager). |
| 124 | + |
| 125 | +Subsystems use a `LOGGER_HEADER` constant (e.g., `Data_Events::LOGGER_HEADER = 'NEWSPACK-DATA-EVENTS'`). |
| 126 | + |
| 127 | +### WP-CLI Commands |
| 128 | + |
| 129 | +All under the `newspack` namespace, defined in `includes/cli/`. Run `wp newspack --help` to list available subcommands. |
| 130 | + |
| 131 | +### PHP Testing |
| 132 | + |
| 133 | +```bash |
| 134 | +npm run lint:php # PHP linting (PHPCS) |
| 135 | +npm run fix:php # Auto-fix PHP issues (PHPCBF) |
| 136 | +``` |
| 137 | + |
| 138 | +- Tests live in `tests/unit-tests/`, extend `WP_UnitTestCase`. |
| 139 | +- Bootstrap: `tests/class-newspack-unit-tests-bootstrap.php`. Mocks in `tests/mocks/`. |
| 140 | +- Available `@group` annotations: `byline-block`, `corrections`, `Access_Rules`, `WooCommerce_Subscriptions_Integration`. |
| 141 | +- Run tests via `n test-php` from the repo directory (see parent AGENTS.md for flags). |
| 142 | + |
| 143 | +## Frontend (JS/React/TypeScript) |
| 144 | + |
| 145 | +### Wizard System (two coexisting architectures) |
| 146 | + |
| 147 | +For the PHP backend counterpart, see **PHP Backend > Wizard System** above. |
| 148 | + |
| 149 | +**Modern pattern (use for new code):** |
| 150 | +- Single entry point: `src/wizards/index.tsx` uses `React.lazy()` + `Suspense` to load views by `?page=` URL param. |
| 151 | +- Views are function components (`.tsx` preferred). |
| 152 | +- Data fetching: `useWizardApiFetch(slug)` hook from `src/wizards/hooks/`. |
| 153 | +- Composition helpers: `WizardsTab`, `WizardSection`, `WizardsActionCard` from `src/wizards/`. |
| 154 | +- Used by: Dashboard, Settings, Audience pages. |
| 155 | + |
| 156 | +**Legacy pattern (found in older wizards):** |
| 157 | +- Standalone webpack entry per wizard: `src/wizards/{name}/index.js`. |
| 158 | +- Uses `withWizard(Component, requiredPlugins)` HOC. |
| 159 | +- Data fetching via `wizardApiFetch` prop (from HOC). |
| 160 | +- Mounts via `createRoot()` + `render()`. |
| 161 | +- Used by: Setup, Newsletters, Advertising. |
| 162 | + |
| 163 | +### State Management |
| 164 | + |
| 165 | +`@wordpress/data` custom store, namespace `newspack/wizards` (`WIZARD_STORE_NAMESPACE`): |
| 166 | +- Key selectors: `getWizardAPIData(slug)`, `getWizardData(slug)`, `isLoading()`. |
| 167 | +- Key actions: `wizardApiFetch`, `saveWizardSettings`. |
| 168 | +- Auto-fetches data from `/newspack/v1/wizard/{slug}` via resolver. |
| 169 | +- Helper: `useWizardData(wizardName)` from `packages/components/src/wizard/store/utils.js`. |
| 170 | + |
| 171 | +Reader activation frontend uses a separate localStorage-based store (`src/reader-activation/store.js`), not `@wordpress/data`. |
| 172 | + |
| 173 | +### TypeScript Conventions |
| 174 | + |
| 175 | +Mixed JS/TS codebase (~30% TypeScript). Newer wizard views are `.tsx`; older wizards, blocks, and most components remain `.js`. |
| 176 | + |
| 177 | +- Config extends `newspack-scripts/config/tsconfig.json` (strict mode). |
| 178 | +- Type declarations: ambient `.d.ts` files with global types (no imports needed). Located at `src/wizards/types/` and feature-level `types.d.ts`. |
| 179 | +- Window globals typed via `declare global { interface Window { ... } }`. |
| 180 | + |
| 181 | +### Import Conventions |
| 182 | + |
| 183 | +**Order** (each group separated by a blank line with a JSDoc comment header): |
| 184 | +1. `/** External dependencies */` (classnames, lodash, etc.) |
| 185 | +2. `/** WordPress dependencies */` (@wordpress/\*) |
| 186 | +3. `/** Internal dependencies */` (relative paths) |
| 187 | + |
| 188 | +**Component library**: Import from `packages/components/src` via relative paths (no webpack alias): |
| 189 | +```js |
| 190 | +import { Button, ActionCard } from '../../packages/components/src'; |
| 191 | +``` |
| 192 | + |
| 193 | +**Router**: In source code, always import through the proxy, never directly from `react-router-dom` (tests may import directly): |
| 194 | +```js |
| 195 | +import Router from '../../packages/components/src/proxied-imports/router'; |
| 196 | +const { HashRouter, Route, Switch } = Router; |
| 197 | +``` |
| 198 | + |
| 199 | +**Colors in JS**: `import colors from '../../packages/colors/colors.module.scss';` |
| 200 | + |
| 201 | +### Webpack Entry Points |
| 202 | + |
| 203 | +Base config from `newspack-scripts`, which extends `@wordpress/scripts`. |
| 204 | + |
| 205 | +**Auto-discovered entries:** |
| 206 | +- Wizards: `src/wizards/*/index.{js,tsx}` (~7 standalone entries) |
| 207 | +- Other scripts: `src/other-scripts/*/index.js` (~11 entries) |
| 208 | + |
| 209 | +**Hardcoded entries (~30)**: reader-activation scripts, content-gate scripts, my-account variants, admin/editor scripts, blocks, collections, newspack-ui, bylines, and more. All declared in `webpack.config.js`. |
| 210 | + |
| 211 | +**Code splitting**: Hashed chunk filenames, commons split chunk. Public path set dynamically via `src/shared/js/public-path.js` from `window.newspack_urls.public_path`. Any standalone entry point must import this file first. |
| 212 | + |
| 213 | +**Ad blocker workaround**: `advertising/` wizard bundled as `billboard.js`. |
| 214 | + |
| 215 | +### Gutenberg Blocks |
| 216 | + |
| 217 | +Blocks in `src/blocks/` with `block.json` metadata. Central registration in `src/blocks/index.js`. |
| 218 | + |
| 219 | +- Each block exports `{ metadata, name, settings }`. |
| 220 | +- Conditional registration based on `window.newspack_blocks` feature flags (`has_reader_activation`, `corrections_enabled`, `collections_enabled`, `has_memberships`, `is_block_theme`). |
| 221 | +- Icons from `packages/icons/` with foreground color from `packages/colors/`. See `packages/icons/DEVELOPMENT.md` for the icon selection hierarchy (prefer `@wordpress/icons` first, then Newspack icons) and React/PHP usage patterns. |
| 222 | +- Some blocks have separate webpack entries for frontend `view.js` scripts. |
| 223 | + |
| 224 | +### SCSS & Color System |
| 225 | + |
| 226 | +- Design tokens in `packages/colors/colors.module.scss` (primary, secondary, tertiary, quaternary, neutral + semantic colors, each with 000-1000 scale). |
| 227 | +- See `packages/colors/DEVELOPMENT.md` for the color usage decision tree: backend admin uses WordPress colors (with `primary-600` accent override), block icons must use `primary-400`, frontend Newspack UI uses `newspack-colors`, theme elements use the theme palette. |
| 228 | +- BEM-ish naming with `newspack-` prefix (e.g., `.newspack-wizard__header`, `.newspack-card`). |
| 229 | +- Tachyons CSS utility library available for utility classes. |
| 230 | +- Shared mixins in `src/shared/scss/_mixins.scss`. |
| 231 | + |
| 232 | +### JS Testing |
| 233 | + |
| 234 | +```bash |
| 235 | +npm test # IMPORTANT: Always run the full suite. Individual test files cannot run independently. |
| 236 | +npm run tsc # TypeScript type checking (watch mode, no emit) |
| 237 | +``` |
| 238 | + |
| 239 | +- Jest via `newspack-scripts test` (wraps `@wordpress/scripts`). |
| 240 | +- Test files colocated with source using `.test.js` suffix. |
| 241 | +- Libraries: `@testing-library/react` (`render`, `fireEvent`, `waitFor`, `screen`). |
| 242 | +- Mocking: `jest.mock()` for `@wordpress/data`, `@wordpress/api-fetch`; direct manipulation of `window.*` globals. |
| 243 | + |
| 244 | +## Recipes |
| 245 | + |
| 246 | +### Add a new wizard section |
| 247 | + |
| 248 | +1. Create a PHP class in `includes/wizards/newspack/` extending `Wizard_Section`. |
| 249 | +2. Set `$wizard_slug` to match the parent wizard, implement `register_rest_routes()`. |
| 250 | +3. `include_once` the file in `includes/class-newspack.php` (order matters). |
| 251 | +4. Run `composer dump-autoload`. |
| 252 | +5. Create a React component in `src/wizards/` (use modern pattern: function component, `.tsx`, `useWizardApiFetch`). |
| 253 | +6. Add a `React.lazy()` import in `src/wizards/index.tsx` mapped to the `?page=` param. |
| 254 | + |
| 255 | +### Add a new block |
| 256 | + |
| 257 | +1. Create a directory in `src/blocks/<block-name>/` with `block.json`, `edit.js`, `index.js`. |
| 258 | +2. Export `{ metadata, name, settings }` from `index.js`. |
| 259 | +3. Register the block in `src/blocks/index.js` (add feature flag condition if needed via `window.newspack_blocks`). |
| 260 | +4. If the block needs a PHP render callback, register it in `includes/class-blocks.php`. |
| 261 | +5. If the block needs a frontend script, add a `view.js` and a hardcoded webpack entry in `webpack.config.js`. |
| 262 | + |
| 263 | +### Add a new plugin integration |
| 264 | + |
| 265 | +1. Create `includes/plugins/class-{plugin-name}.php` using the root `Newspack` namespace. |
| 266 | +2. Follow the static `init()` pattern (see Class Initialization Patterns above). |
| 267 | +3. `include_once` the file in `includes/class-newspack.php`. |
| 268 | +4. Run `composer dump-autoload`. |
| 269 | +5. Optionally add a Configuration Manager in `includes/configuration_managers/` for setup UI. |
0 commit comments