Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions node-packages/wp-tooling/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion node-packages/wp-tooling/docs/ai-orchestration.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ The requested `<category>/<slug>` 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
{
Expand Down
2 changes: 1 addition & 1 deletion node-packages/wp-tooling/docs/authoring-scaffolds.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion node-packages/wp-tooling/docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions node-packages/wp-tooling/scaffolds/lint/phpstan/scaffold.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (?)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion node-packages/wp-tooling/skills/setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions node-packages/wp-tooling/src/scaffolds/prompt-inputs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
12 changes: 9 additions & 3 deletions node-packages/wp-tooling/src/scaffolds/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string,string>} supplied - Caller-supplied input values.
Expand All @@ -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,
Expand Down
33 changes: 33 additions & 0 deletions node-packages/wp-tooling/tests/scaffolds/bundled-manifests.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'],
Expand Down
19 changes: 19 additions & 0 deletions node-packages/wp-tooling/tests/scaffolds/prompt-inputs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
21 changes: 21 additions & 0 deletions node-packages/wp-tooling/tests/scaffolds/registry.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Loading