From 142c6868d651a948f5dc763a475d1a4894e06d73 Mon Sep 17 00:00:00 2001 From: Aaron Ware Date: Sun, 18 Oct 2026 10:17:14 -0400 Subject: [PATCH] fix(LINCHPIN-5636): Rename readme.txt with the rest of a scaffolded plugin linchpin/plugin-scaffold now ships a readme.txt on every channel, and the wporg overlay already did. The scaffold renamed the plugin header and README.md but not readme.txt, so a generated readme kept "Plugin Scaffold" and the scaffold's description, and under --php its Requires PHP disagreed with the header, which Plugin Check fails. readme.txt now takes --name, --description and, where it declares one, --php. The short description is replaced only when one exists, rather than the section heading that follows the header block. --- docs/plugin-scaffold.md | 6 ++--- src/core/plugin-scaffold.ts | 14 ++++++++++++ .../plugin-scaffold/overlays/wporg/readme.txt | 3 +++ test/fixtures/plugin-scaffold/readme.txt | 9 ++++++++ test/plugin-scaffold.test.js | 22 ++++++++++++++++--- 5 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 test/fixtures/plugin-scaffold/readme.txt diff --git a/docs/plugin-scaffold.md b/docs/plugin-scaffold.md index c1f4261..34df354 100644 --- a/docs/plugin-scaffold.md +++ b/docs/plugin-scaffold.md @@ -20,9 +20,9 @@ linchpin plugin scaffold acme --path ~/GitHub/acme --dry-run | Flag | Default | Meaning | | --- | --- | --- | -| `--name` | title-cased slug | Plugin Name and README title | -| `--description` | generated one-liner | Plugin header and README one-liner | -| `--php` | `8.3` | Requires PHP | +| `--name` | title-cased slug | Plugin Name, README title, and the `readme.txt` name | +| `--description` | generated one-liner | Plugin header, README one-liner, and the `readme.txt` short description | +| `--php` | `8.3` | Requires PHP, in the header and in `readme.txt` when it declares one | | `--channel` | `private` | `private`, `wporg`, or `self-hosted` | | `--with-blocks` | off | Nested `blocks/` workspace and an example block | | `--path` | `./` | Destination | diff --git a/src/core/plugin-scaffold.ts b/src/core/plugin-scaffold.ts index 0807164..fc59c67 100644 --- a/src/core/plugin-scaffold.ts +++ b/src/core/plugin-scaffold.ts @@ -332,6 +332,20 @@ function applyDisplayIdentity( readme = readme.replace(/^(# .+\n\n).+(\n)/, `$1${description}$2`); writeFileSync(readmePath, readme); + // Plugin Check fails a readme.txt whose Requires PHP differs from the header, + // and warns on a mismatched plugin name. The short description follows the + // header block, and is absent when the next line is already a section. + const readmeTxtPath = join(dest, 'readme.txt'); + if (existsSync(readmeTxtPath)) { + let readmeTxt = readFileSync(readmeTxtPath, 'utf8'); + readmeTxt = readmeTxt.replace(/^=== .+ ===$/m, `=== ${name} ===`); + readmeTxt = readmeTxt.replace(/^(=== .+ ===\n(?:.+\n)*\n)(?!==).+(\n)/, `$1${description}$2`); + if (php !== '8.3') { + readmeTxt = readmeTxt.replace(/(Requires PHP:\s*)8\.3/, (_match, label: string) => `${label}${php}`); + } + writeFileSync(readmeTxtPath, readmeTxt); + } + const composerPath = join(dest, 'composer.json'); const composer = JSON.parse(readFileSync(composerPath, 'utf8')) as { description?: string; diff --git a/test/fixtures/plugin-scaffold/overlays/wporg/readme.txt b/test/fixtures/plugin-scaffold/overlays/wporg/readme.txt index 1a59e0e..8938909 100644 --- a/test/fixtures/plugin-scaffold/overlays/wporg/readme.txt +++ b/test/fixtures/plugin-scaffold/overlays/wporg/readme.txt @@ -1,6 +1,9 @@ === Plugin Scaffold === +Requires PHP: 8.3 Stable tag: 0.1.0 +The Linchpin WordPress plugin standard. + == Description == The Linchpin WordPress plugin standard. diff --git a/test/fixtures/plugin-scaffold/readme.txt b/test/fixtures/plugin-scaffold/readme.txt new file mode 100644 index 0000000..9f4e819 --- /dev/null +++ b/test/fixtures/plugin-scaffold/readme.txt @@ -0,0 +1,9 @@ +=== Plugin Scaffold === +Contributors: linchpin_agency +Tested up to: 7.1 + +The Linchpin WordPress plugin standard. + +== Description == + +Replace this description. diff --git a/test/plugin-scaffold.test.js b/test/plugin-scaffold.test.js index 8f962cd..b6990a5 100644 --- a/test/plugin-scaffold.test.js +++ b/test/plugin-scaffold.test.js @@ -123,6 +123,12 @@ test('renders acme from the fixture', () => { ); assert.doesNotMatch(readme.replaceAll(BANNER, ''), /plugin-scaffold/); + const readmeTxt = fs.readFileSync(path.join(dest, 'readme.txt'), 'utf8'); + assert.match(readmeTxt, /^=== Acme ===$/m); + assert.match(readmeTxt, /^A test plugin\.$/m); + assert.match(readmeTxt, /^== Description ==$/m); + assert.doesNotMatch(readmeTxt, /Stable tag:/); + const leftovers = lib.leftoverIdentity(dest); assert.deepEqual(leftovers, []); }); @@ -139,12 +145,19 @@ test('with-blocks and wporg overlays', () => { FIXTURE, '--with-blocks', '--channel=wporg', + '--php', + '8.4', ]); assert.equal(result.code, 0, result.stderr); assert.equal(fs.existsSync(path.join(dest, 'includes/Controller/Blocks.php')), true); assert.equal(fs.existsSync(path.join(dest, 'blocks/src/example/block.json')), true); - assert.equal(fs.existsSync(path.join(dest, 'readme.txt')), true); + + const readmeTxt = fs.readFileSync(path.join(dest, 'readme.txt'), 'utf8'); + assert.match(readmeTxt, /^=== Acme ===$/m); + assert.match(readmeTxt, /^Stable tag: 0\.1\.0$/m); + assert.match(readmeTxt, /^Requires PHP: 8\.4$/m); + assert.match(readmeTxt, /^A WordPress plugin generated from the Linchpin plugin standard\.$/m); const pkg = JSON.parse(fs.readFileSync(path.join(dest, 'package.json'), 'utf8')); assert.equal(pkg.scripts['build:all'], 'npm run build && npm run build --prefix blocks'); @@ -153,7 +166,7 @@ test('with-blocks and wporg overlays', () => { assert.doesNotMatch(main, /Update URI:/); }); -test('self-hosted adds Update URI and does not add readme.txt', () => { +test('self-hosted adds Update URI and keeps the private readme.txt', () => { const dest = path.join(tempDir(), 'acme'); const result = runCli(process.cwd(), [ 'plugin', @@ -169,7 +182,10 @@ test('self-hosted adds Update URI and does not add readme.txt', () => { assert.equal(result.code, 0, result.stderr); const main = fs.readFileSync(path.join(dest, 'acme.php'), 'utf8'); assert.match(main, /Update URI:\s+https:\/\/api\.linchpin\.com\/updates\/acme/); - assert.equal(fs.existsSync(path.join(dest, 'readme.txt')), false); + + const readmeTxt = fs.readFileSync(path.join(dest, 'readme.txt'), 'utf8'); + assert.match(readmeTxt, /^=== Acme ===$/m); + assert.doesNotMatch(readmeTxt, /Stable tag:/); }); test('refuses a non-empty destination without --force', () => {