diff --git a/node-packages/wp-tooling/CHANGELOG.md b/node-packages/wp-tooling/CHANGELOG.md index 2486894..d5b3f6d 100644 --- a/node-packages/wp-tooling/CHANGELOG.md +++ b/node-packages/wp-tooling/CHANGELOG.md @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Fixed + +- A required scaffold input given an empty or whitespace-only value (`--paths=`, or Enter at the prompt) counts as missing. Non-interactive runs fail with `EMISSINGINPUT` and the prompt asks again, instead of rendering an empty slug, namespace or path list. +- `lint/phpstan` asks for the PHP to analyse through a required `paths` input (comma-separated, e.g. `--paths='inc/, my-plugin.php'`) instead of hardcoding `src` and `includes`. PHPStan stops with `Path … does not exist` on a missing `paths` entry, and `(?)` only works in `excludePaths`, so `composer analyse` failed in every project without both directories, including the rtCamp plugin and theme skeletons, which use `inc/`. The excludes shrink to optional `vendor` and `node_modules`, which only matter when `paths` includes `.`. + ## [2.0.0] - 2026-10-01 ### Added diff --git a/node-packages/wp-tooling/docs/ai-orchestration.md b/node-packages/wp-tooling/docs/ai-orchestration.md index 2fa1dfa..131c3a1 100644 --- a/node-packages/wp-tooling/docs/ai-orchestration.md +++ b/node-packages/wp-tooling/docs/ai-orchestration.md @@ -100,7 +100,7 @@ The requested `/` is not in the merged catalogue. ### `EMISSINGINPUT` -`--non-interactive` mode and one or more required inputs were not supplied (after the manifest's `default` was applied). +`--non-interactive` mode and one or more required inputs were not supplied, or were blank (after the manifest's `default` was applied). ```json { diff --git a/node-packages/wp-tooling/docs/authoring-scaffolds.md b/node-packages/wp-tooling/docs/authoring-scaffolds.md index 04b48fa..7eeed70 100644 --- a/node-packages/wp-tooling/docs/authoring-scaffolds.md +++ b/node-packages/wp-tooling/docs/authoring-scaffolds.md @@ -126,7 +126,7 @@ The `inputs[]` array is the authoritative declaration. For each input the engine 1. Explicit `--key=value` passed to `add`. 2. `discover_from` (see below). 3. `default`. -4. Error `EMISSINGINPUT` if the field is `required: true`. +4. Error `EMISSINGINPUT` if the field is `required: true` and the value is missing or blank. An empty `--key=` or an Enter at the prompt is not a value. 5. Error `EINVALIDINPUT` if the resolved value falls outside a declared `enum`. An explicit value always wins over discovery, so AI/CLI callers are never overridden. diff --git a/node-packages/wp-tooling/docs/examples.md b/node-packages/wp-tooling/docs/examples.md index 48d0bfc..aa730e5 100644 --- a/node-packages/wp-tooling/docs/examples.md +++ b/node-packages/wp-tooling/docs/examples.md @@ -52,7 +52,7 @@ Confirm? npx wp-tooling add setup/editorconfig --non-interactive --json --cwd . npx wp-tooling add setup/psr4 --non-interactive --json --cwd . --namespace='Acme\InventoryManager' --base_path=includes npx wp-tooling add lint/phpcs/full --non-interactive --json --cwd . -npx wp-tooling add lint/phpstan --non-interactive --json --cwd . +npx wp-tooling add lint/phpstan --non-interactive --json --cwd . --paths=includes npx wp-tooling add lint/eslint --non-interactive --json --cwd . npx wp-tooling add setup/phpunit --non-interactive --json --cwd . --source_dir=includes npx wp-tooling add wp/cpt --non-interactive --json --cwd . --slug=product --singular=Product --plural=Products --namespace='Acme\InventoryManager' --base_path=includes diff --git a/node-packages/wp-tooling/scaffolds/lint/phpstan/scaffold.json b/node-packages/wp-tooling/scaffolds/lint/phpstan/scaffold.json index e947b8b..c1fc728 100644 --- a/node-packages/wp-tooling/scaffolds/lint/phpstan/scaffold.json +++ b/node-packages/wp-tooling/scaffolds/lint/phpstan/scaffold.json @@ -4,6 +4,13 @@ "name": "PHPStan setup", "description": "Installs PHPStan with rtCamp's shared baseline (includes `vendor/rtcamp/wp-phpstan/phpstan.neon.dist` alongside szepeviktor/phpstan-wordpress) and adds composer analyse scripts.", "source": "template", + "inputs": [ + { + "key": "paths", + "description": "Files and directories holding your own PHP, comma-separated, e.g. `inc/, my-plugin.php`. PHPStan stops on one that doesn't exist.", + "required": true + } + ], "files": [ { "src": "templates/phpstan.neon.dist.mustache", diff --git a/node-packages/wp-tooling/scaffolds/lint/phpstan/templates/phpstan.neon.dist.mustache b/node-packages/wp-tooling/scaffolds/lint/phpstan/templates/phpstan.neon.dist.mustache index 4d55c5a..2e18a0f 100644 --- a/node-packages/wp-tooling/scaffolds/lint/phpstan/templates/phpstan.neon.dist.mustache +++ b/node-packages/wp-tooling/scaffolds/lint/phpstan/templates/phpstan.neon.dist.mustache @@ -1,17 +1,13 @@ -# PHPStan config — extends rtCamp's shared baseline (rtcamp/wp-phpstan). -# Scaffolded by @rtcamp/wp-tooling. Add project-specific overrides under `parameters`. +# https://phpstan.org/config-reference +# Scaffolded by @rtcamp/wp-tooling. includes: - vendor/rtcamp/wp-phpstan/phpstan.neon.dist parameters: - paths: - - src - - includes + # Your own PHP. A path that doesn't exist stops PHPStan. + paths: [{{paths}}] + # Only matters if `paths` includes `.`. excludePaths: - # PHPStan rejects an excluded path that does not exist, so the ones a - # project may not have are marked optional with (?). - - vendor + - vendor (?) - node_modules (?) - - build (?) - - tests (?) diff --git a/node-packages/wp-tooling/scaffolds/setup/claude-skills/templates/setup-SKILL.md b/node-packages/wp-tooling/scaffolds/setup/claude-skills/templates/setup-SKILL.md index 2ce5358..b67ebcb 100644 --- a/node-packages/wp-tooling/scaffolds/setup/claude-skills/templates/setup-SKILL.md +++ b/node-packages/wp-tooling/scaffolds/setup/claude-skills/templates/setup-SKILL.md @@ -192,7 +192,7 @@ For lint and test setup scaffolds: ```bash npx wp-tooling add lint/phpcs/vip --non-interactive --json --cwd . -npx wp-tooling add lint/phpstan --non-interactive --json --cwd . +npx wp-tooling add lint/phpstan --non-interactive --json --cwd . --paths=includes npx wp-tooling add lint/eslint --non-interactive --json --cwd . npx wp-tooling add setup/phpunit --non-interactive --json --cwd . --source-dir=includes npx wp-tooling add setup/pa11y --non-interactive --json --cwd . --base-url=http://localhost:8888 diff --git a/node-packages/wp-tooling/skills/setup/SKILL.md b/node-packages/wp-tooling/skills/setup/SKILL.md index 2ce5358..b67ebcb 100644 --- a/node-packages/wp-tooling/skills/setup/SKILL.md +++ b/node-packages/wp-tooling/skills/setup/SKILL.md @@ -192,7 +192,7 @@ For lint and test setup scaffolds: ```bash npx wp-tooling add lint/phpcs/vip --non-interactive --json --cwd . -npx wp-tooling add lint/phpstan --non-interactive --json --cwd . +npx wp-tooling add lint/phpstan --non-interactive --json --cwd . --paths=includes npx wp-tooling add lint/eslint --non-interactive --json --cwd . npx wp-tooling add setup/phpunit --non-interactive --json --cwd . --source-dir=includes npx wp-tooling add setup/pa11y --non-interactive --json --cwd . --base-url=http://localhost:8888 diff --git a/node-packages/wp-tooling/src/scaffolds/prompt-inputs.js b/node-packages/wp-tooling/src/scaffolds/prompt-inputs.js index 550cf5f..3653f5e 100644 --- a/node-packages/wp-tooling/src/scaffolds/prompt-inputs.js +++ b/node-packages/wp-tooling/src/scaffolds/prompt-inputs.js @@ -43,6 +43,10 @@ async function promptMissingInputs({ const answer = await text({ message: `${message}${defaultValue !== undefined ? ` (default: ${defaultValue})` : ''}`, defaultValue, + // The engine rejects a blank required value, so ask again here. + validate: declared.required + ? (value) => (value ? undefined : 'A value is required.') + : undefined, }); next[key] = answer; } diff --git a/node-packages/wp-tooling/src/scaffolds/registry.js b/node-packages/wp-tooling/src/scaffolds/registry.js index 66dc867..576e003 100644 --- a/node-packages/wp-tooling/src/scaffolds/registry.js +++ b/node-packages/wp-tooling/src/scaffolds/registry.js @@ -1194,8 +1194,8 @@ function resolveDeclared(declared, supplied, discovery) { * discovery. * * Throws EMISSINGINPUT (with `missingDetails`) when required inputs are - * not supplied (after `default` is applied), then EINVALIDINPUT (with - * `invalid`) when a resolved value falls outside its declared `enum`. + * not supplied or blank (after `default` is applied), then EINVALIDINPUT + * (with `invalid`) when a resolved value falls outside its declared `enum`. * * @param {Object} scaffold - The scaffold record being executed. * @param {Object} supplied - Caller-supplied input values. @@ -1211,7 +1211,13 @@ function resolveInputs(scaffold, supplied, discovery = {}) { if (declared) { resolved = resolveDeclared(declared, supplied, discovery); for (const decl of declared) { - if (!(decl.key in resolved) && decl.required) { + // A blank value counts as missing: it would render an empty slug, + // namespace or path list, which no generated code means. + const value = resolved[decl.key]; + if ( + decl.required && + (value === undefined || String(value).trim() === '') + ) { missing.push(decl.key); missingDetails.push({ key: decl.key, diff --git a/node-packages/wp-tooling/tests/scaffolds/bundled-manifests.test.js b/node-packages/wp-tooling/tests/scaffolds/bundled-manifests.test.js index e075506..739de6c 100644 --- a/node-packages/wp-tooling/tests/scaffolds/bundled-manifests.test.js +++ b/node-packages/wp-tooling/tests/scaffolds/bundled-manifests.test.js @@ -8,6 +8,8 @@ * - wiring targetFile paths are normalised (no `..` segments), and a * target that still escapes the project is dropped with a warning * - lint/i18n binds a supplied or discovered text domain, never a guessed one + * - lint/phpstan analyses only the paths it is given, never guessed ones, + * since PHPStan stops on a listed path that does not exist * - wp-api/speculation renders into the registrable layout, reuses the * registrable wiring anchor, follows a non-`includes/` PSR-4 root, and * rejects an out-of-enum mode/eagerness @@ -752,6 +754,37 @@ describe('lint/i18n ruleset', () => { }); }); +describe('lint/phpstan paths', () => { + it('analyses exactly the supplied paths', async () => { + const target = makeTmpDir(); + await registry.execute( + 'lint/phpstan', + { paths: 'inc/, src/, acme-blog.php' }, + { cwd: target } + ); + const neon = fs.readFileSync( + path.join(target, 'phpstan.neon.dist'), + 'utf8' + ); + expect(neon).toContain(' paths: [inc/, src/, acme-blog.php]\n'); + }); + + it('requires paths rather than guessing them', async () => { + // The bug this closes: the config always listed `src` and `includes`, + // and PHPStan stops on a path that does not exist. + await expect( + registry.execute( + 'lint/phpstan', + {}, + { dryRun: true, cwd: makeTmpDir() } + ) + ).rejects.toMatchObject({ + code: 'EMISSINGINPUT', + missing: ['paths'], + }); + }); +}); + describe('setup/claude-skills accessibility distribution', () => { it.each([ ['default directory', {}, '.claude/skills'], diff --git a/node-packages/wp-tooling/tests/scaffolds/prompt-inputs.test.js b/node-packages/wp-tooling/tests/scaffolds/prompt-inputs.test.js index 5371b02..4fe3cd3 100644 --- a/node-packages/wp-tooling/tests/scaffolds/prompt-inputs.test.js +++ b/node-packages/wp-tooling/tests/scaffolds/prompt-inputs.test.js @@ -81,6 +81,25 @@ describe('promptMissingInputs', () => { }); expect(ui.__calls.text[0].defaultValue).toBeUndefined(); }); + + it('asks again on a blank answer only for a required input', async () => { + const scaffold = { + inputs: [ + { key: 'name', description: 'Command slug', required: true }, + { key: 'label', description: 'Menu label' }, + ], + }; + await promptMissingInputs({ + scaffold, + missing: ['name', 'label'], + missingDetails: [], + supplied: {}, + }); + const [required, optional] = ui.__calls.text; + expect(required.validate('')).toBe('A value is required.'); + expect(required.validate('export')).toBeUndefined(); + expect(optional.validate).toBeUndefined(); + }); }); describe('confirmRun', () => { diff --git a/node-packages/wp-tooling/tests/scaffolds/registry.test.js b/node-packages/wp-tooling/tests/scaffolds/registry.test.js index cc2b37a..466e779 100644 --- a/node-packages/wp-tooling/tests/scaffolds/registry.test.js +++ b/node-packages/wp-tooling/tests/scaffolds/registry.test.js @@ -495,6 +495,27 @@ describe('execute() error paths', () => { discover_from: null, }); }); + + it.each(['', ' '])( + 'treats a blank required input (%j) as missing', + async (blank) => { + await expect( + registry.execute('wp/cli', { name: blank }, { dryRun: true }) + ).rejects.toMatchObject({ + code: 'EMISSINGINPUT', + missing: ['name'], + }); + } + ); + + it('accepts a blank optional input', async () => { + const result = await registry.execute( + 'wp/cli', + { name: 'export', namespace: '' }, + { dryRun: true, cwd: makeTmpDir() } + ); + expect(result.engine.inputs.namespace).toBe(''); + }); }); describe('execute() passes scripts through to developer block', () => {