@wporg/plugin-check 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/plugin-check.mjs +231 -0
  2. package/package.json +33 -0
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFileSync } from 'node:child_process';
4
+ import { createWriteStream } from 'node:fs';
5
+ import {
6
+ mkdir,
7
+ mkdtemp,
8
+ readFile,
9
+ rm,
10
+ stat,
11
+ writeFile,
12
+ } from 'node:fs/promises';
13
+ import { tmpdir } from 'node:os';
14
+ import { basename, join, resolve } from 'node:path';
15
+ import { pipeline } from 'node:stream/promises';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ export const PCP_CHECK_PHP = `<?php
19
+ ob_start();
20
+ try {
21
+ WP_CLI::run_command(
22
+ array( 'plugin', 'check', PLUGIN_SLUG ),
23
+ array(
24
+ 'categories' => 'plugin_repo',
25
+ 'format' => 'json',
26
+ 'error-severity' => '7',
27
+ 'warning-severity' => '6',
28
+ 'include-low-severity-errors' => true,
29
+ 'exclude-checks' => 'prefixing',
30
+ )
31
+ );
32
+ } catch ( Exception $e ) {}
33
+ $output = ob_get_clean();
34
+ $output = preg_replace( '/<br\\s*\\/?>\\s*<b>Warning<\\/b>.*?<\\/b><br\\s*\\/?>\\s*/s', '', $output );
35
+ file_put_contents( '/output/results.txt', trim( $output ) );
36
+ `;
37
+
38
+ export function buildBlueprint( slug ) {
39
+ const steps = [];
40
+
41
+ // Skip installing PCP separately when checking PCP itself — it's already mounted.
42
+ if ( slug !== 'plugin-check' ) {
43
+ steps.push( {
44
+ step: 'installPlugin',
45
+ pluginData: {
46
+ resource: 'wordpress.org/plugins',
47
+ slug: 'plugin-check',
48
+ },
49
+ } );
50
+ }
51
+
52
+ steps.push(
53
+ { step: 'wp-cli', command: 'wp plugin activate plugin-check' },
54
+ { step: 'wp-cli', command: 'wp eval-file /pcp-check.php' }
55
+ );
56
+
57
+ return { steps };
58
+ }
59
+
60
+ export function getSlugFromZip( zipPath ) {
61
+ const output = execFileSync( 'unzip', [ '-l', zipPath ], {
62
+ encoding: 'utf8',
63
+ } );
64
+ for ( const line of output.split( '\n' ) ) {
65
+ const match = line.match( /\s+(\S+)\/$/ );
66
+ if ( match && ! match[ 1 ].includes( '/' ) ) {
67
+ return match[ 1 ];
68
+ }
69
+ }
70
+ throw new Error( `Could not determine plugin slug from zip: ${ zipPath }` );
71
+ }
72
+
73
+ function extractZip( zipPath, destDir ) {
74
+ execFileSync( 'unzip', [ '-o', '-q', zipPath, '-d', destDir ] );
75
+ }
76
+
77
+ export function detectInputType( input ) {
78
+ if ( input.startsWith( 'http://' ) || input.startsWith( 'https://' ) ) {
79
+ return 'remote-zip';
80
+ }
81
+ if ( input.toLowerCase().endsWith( '.zip' ) ) {
82
+ return 'local-zip';
83
+ }
84
+ return 'directory';
85
+ }
86
+
87
+ async function downloadFile( url, destPath ) {
88
+ const response = await fetch( url );
89
+ if ( ! response.ok ) {
90
+ throw new Error(
91
+ `Failed to download ${ url }: ${ response.status } ${ response.statusText }`
92
+ );
93
+ }
94
+ await pipeline( response.body, createWriteStream( destPath ) );
95
+ }
96
+
97
+ function resolvePlaygroundBin() {
98
+ const __dirname = fileURLToPath( new URL( '.', import.meta.url ) );
99
+ const localBin = join(
100
+ __dirname,
101
+ '..',
102
+ 'node_modules',
103
+ '.bin',
104
+ 'wp-playground-cli'
105
+ );
106
+
107
+ try {
108
+ execFileSync( localBin, [ '--version' ], { stdio: 'ignore' } );
109
+ return localBin;
110
+ } catch {
111
+ // Fall back to npx when the local bin isn't available (e.g. global install).
112
+ return 'wp-playground-cli';
113
+ }
114
+ }
115
+
116
+ async function main() {
117
+ const input = process.argv[ 2 ];
118
+
119
+ if ( ! input || input === '--help' || input === '-h' ) {
120
+ console.log( `Usage: plugin-check <plugin-path|plugin.zip|url>
121
+
122
+ Run WordPress Plugin Check against a plugin.
123
+
124
+ Arguments:
125
+ plugin-path Path to a local plugin directory
126
+ plugin.zip Path to a local plugin zip file
127
+ url URL to a remote plugin zip file
128
+
129
+ Examples:
130
+ plugin-check ./my-plugin
131
+ plugin-check ./my-plugin.zip
132
+ plugin-check https://example.com/my-plugin.zip` );
133
+ process.exit( 0 );
134
+ }
135
+
136
+ const inputType = detectInputType( input );
137
+ const tmpDir = await mkdtemp( join( tmpdir(), 'pcp-' ) );
138
+
139
+ try {
140
+ let pluginPath, slug;
141
+
142
+ if ( inputType === 'directory' ) {
143
+ pluginPath = resolve( input );
144
+ const stats = await stat( pluginPath );
145
+ if ( ! stats.isDirectory() ) {
146
+ console.error( `Error: ${ pluginPath } is not a directory.` );
147
+ process.exit( 1 );
148
+ }
149
+ slug = basename( pluginPath );
150
+ } else {
151
+ let zipPath;
152
+ if ( inputType === 'local-zip' ) {
153
+ zipPath = resolve( input );
154
+ await stat( zipPath ); // Throws if missing.
155
+ } else {
156
+ console.error( `Downloading ${ input }...` );
157
+ zipPath = join( tmpDir, 'plugin.zip' );
158
+ await downloadFile( input, zipPath );
159
+ }
160
+ slug = getSlugFromZip( zipPath );
161
+ const extractDir = join( tmpDir, 'plugin' );
162
+ extractZip( zipPath, extractDir );
163
+ pluginPath = join( extractDir, slug );
164
+ }
165
+
166
+ // Write temp files.
167
+ await writeFile(
168
+ join( tmpDir, 'blueprint.json' ),
169
+ JSON.stringify( buildBlueprint( slug ) )
170
+ );
171
+ await writeFile( join( tmpDir, 'pcp-check.php' ), PCP_CHECK_PHP );
172
+ await mkdir( join( tmpDir, 'output' ), { recursive: true } );
173
+
174
+ // Build Playground CLI args.
175
+ const args = [
176
+ 'run-blueprint',
177
+ '--define',
178
+ 'PLUGIN_SLUG',
179
+ slug,
180
+ '--mount',
181
+ `${ pluginPath }:/wordpress/wp-content/plugins/${ slug }`,
182
+ '--mount',
183
+ `${ tmpDir }/pcp-check.php:/pcp-check.php`,
184
+ '--mount',
185
+ `${ tmpDir }/output:/output`,
186
+ '--blueprint',
187
+ `${ tmpDir }/blueprint.json`,
188
+ '--quiet',
189
+ ];
190
+
191
+ // Run Playground CLI.
192
+ execFileSync( resolvePlaygroundBin(), args, {
193
+ stdio: [ 'ignore', 'inherit', 'inherit' ],
194
+ } );
195
+
196
+ // Read results.
197
+ let results = '';
198
+ try {
199
+ results = await readFile(
200
+ join( tmpDir, 'output', 'results.txt' ),
201
+ 'utf8'
202
+ );
203
+ } catch {
204
+ // No results file = plugin passed or PCP crashed.
205
+ }
206
+
207
+ if ( ! results.trim() ) {
208
+ console.log( 'Plugin Check passed — no issues found.' );
209
+ process.exit( 0 );
210
+ }
211
+
212
+ process.stdout.write( results + '\n' );
213
+
214
+ const hasErrors =
215
+ results.includes( '"type":"ERROR"' ) ||
216
+ results.includes( '"type": "ERROR"' );
217
+ process.exit( hasErrors ? 1 : 0 );
218
+ } finally {
219
+ await rm( tmpDir, { recursive: true, force: true } );
220
+ }
221
+ }
222
+
223
+ // Only run when executed directly, not when imported for testing.
224
+ const isDirectRun =
225
+ process.argv[ 1 ] &&
226
+ resolve( process.argv[ 1 ] ) ===
227
+ resolve( fileURLToPath( import.meta.url ) );
228
+
229
+ if ( isDirectRun ) {
230
+ main();
231
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@wporg/plugin-check",
3
+ "version": "0.1.0",
4
+ "description": "Run WordPress Plugin Check locally via Playground CLI.",
5
+ "type": "module",
6
+ "license": "GPL-2.0-or-later",
7
+ "bin": {
8
+ "plugin-check": "bin/plugin-check.mjs"
9
+ },
10
+ "files": [
11
+ "bin/plugin-check.mjs"
12
+ ],
13
+ "scripts": {
14
+ "test": "node --test test/*.test.mjs"
15
+ },
16
+ "keywords": [
17
+ "wordpress",
18
+ "plugin-check",
19
+ "playground",
20
+ "wp-cli",
21
+ "linter",
22
+ "static-analysis"
23
+ ],
24
+ "engines": {
25
+ "node": ">=20"
26
+ },
27
+ "dependencies": {
28
+ "@wp-playground/cli": "^3.1.13"
29
+ },
30
+ "devDependencies": {
31
+ "@wordpress/scripts": "^31.7.0"
32
+ }
33
+ }