Skip to content
Merged
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
4 changes: 4 additions & 0 deletions docs/guides/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,9 @@ Build a release zip with `wptoolkit-build` (`wptoolkit-build init`, then `wptool
If your plugin might share a site with another WPToolkit plugin, scope your copy first — see
[scoping and coexistence](scoping-and-coexistence.md).

Commands the build runs (`npm run build`, Composer) are silent unless they fail. To watch them, add
`--verbose` (`wptoolkit-build package --verbose`), set `"verbose": true` in `wptoolkit.json`, or in a
`build.php` call `->verbose()` for every command or `->runVerbose('npm run build')` for one.

Next: [routes and security](routes-and-security.md), [data](data.md),
[admin and front end](admin-and-front-end.md).
4 changes: 3 additions & 1 deletion docs/phases/track-p-packager.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ php wptoolkit/bin/wptoolkit package # standalone install (no Composer)
| `wptoolkit scope <Prefix>` | Scopes the bundled toolkit (ADR-0014) |

Flags override config: `--out`, `--zip-name`, `--no-build`, `--no-composer`, `--no-scope`, `--pot`,
`--json` (machine-readable report for CI).
`--json` (machine-readable report for CI). `--verbose` (added 2026-10-12) prints each command's
output as it runs; `"verbose": true` and `Build::verbose()` / `runVerbose()` do the same from config
and from a build script.

**Configuration** is one optional `wptoolkit.json` at the project root. It's JSON so PHP reads it
without dependencies, and it has a JSON Schema so editors autocomplete it:
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/work-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,5 @@ One dated entry per working session: what landed, what was learned, what's next.

- **Landed:** the guard's `namespaces` option (ADR-0025): PSR-4 loading of a standalone plugin's own classes,
which ADR-0014 promised and 0.x's `Autoloader::init()` used to do. The E2E fixtures now rely on it.
- **Landed:** `Build::verbose()`, `runVerbose()`, `--verbose` and `"verbose": true`: command output streamed as it
runs (`StreamingCommandRunner`); a failure after streaming repeats only the last 20 lines.
8 changes: 6 additions & 2 deletions packages/build/bin/wptoolkit-build
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
/**
* wptoolkit-build — package a WordPress plugin or theme from wptoolkit.json (or defaults).
*
* vendor/bin/wptoolkit-build package [--project=DIR] [--zip=PATH] [--dry-run]
* vendor/bin/wptoolkit-build package [--project=DIR] [--zip=PATH] [--dry-run] [--verbose]
* vendor/bin/wptoolkit-build verify FILE.zip
* vendor/bin/wptoolkit-build doctor [--project=DIR]
* vendor/bin/wptoolkit-build init [--project=DIR]
Expand Down Expand Up @@ -57,6 +57,9 @@ try {
fwrite(STDOUT, "(dry run: commands, composer, scope and make-pot were not run)\n");
exit(0);
}
if (in_array('--verbose', $argv, true)) {
$build->verbose();
}
$build->zip($options['zip'] ?? $zip);
exit(0);

Expand Down Expand Up @@ -98,7 +101,8 @@ try {

default:
fwrite(STDOUT, "Usage:\n"
. " wptoolkit-build package [--project=DIR] [--zip=PATH] [--dry-run]\n"
. " wptoolkit-build package [--project=DIR] [--zip=PATH] [--dry-run] [--verbose]\n"
. " --verbose print the output of every command as it runs\n"
. " wptoolkit-build verify FILE.zip\n"
. " wptoolkit-build doctor [--project=DIR]\n"
. " wptoolkit-build init [--project=DIR]\n");
Expand Down
26 changes: 25 additions & 1 deletion packages/build/src/Build.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
*
* Phases: Prepare (in your project) → stage (copy to a temp directory) → Transform → prune
* (excludes) → Verify (always) → zip. Your working tree is never modified.
*
* Commands are silent unless they fail; `->verbose()` prints their output as they run, and
* `->runVerbose(...)` does so for chosen commands only.
*/
final class Build
{
Expand Down Expand Up @@ -62,6 +65,8 @@ final class Build

private string $composerBinary = 'composer';

private bool $verbose = false;

/** @var list<string|PatternSource> */
private array $excludes = [];

Expand Down Expand Up @@ -127,6 +132,25 @@ public function run(string ...$commands): self
return $this->step(new RunCommands(...$commands));
}

/**
* Like run(), but these commands' output is printed as they run even when the build is not verbose.
*/
public function runVerbose(string ...$commands): self
{
return $this->step(RunCommands::streamed(...$commands));
}

/**
* Print the output of every command the build runs (run(), composer(), scope(), make-pot) as it
* is produced, instead of only when a command fails. Useful in CI logs and for slow asset builds.
*/
public function verbose(bool $verbose = true): self
{
$this->verbose = $verbose;

return $this;
}

/**
* Reinstall Composer dependencies in staging; `noDev` drops dev packages.
*/
Expand Down Expand Up @@ -228,7 +252,7 @@ public function zip(string $target = 'dist/{slug}-{version}.zip'): BuildReport
$zip = $this->absolute(strtr($target, ['{slug}' => $this->project->slug, '{version}' => $version]));
$staging = sys_get_temp_dir() . '/wptoolkit-build-' . bin2hex(random_bytes(6)) . '/' . $this->project->slug;

$context = new BuildContext($this->project, $staging, $version, $this->runner, $this->output);
$context = new BuildContext($this->project, $staging, $version, $this->runner, $this->output, $this->verbose);
$context->say(sprintf('Building %s %s (%s)', $this->project->slug, $version, $this->project->type));

try {
Expand Down
43 changes: 38 additions & 5 deletions packages/build/src/BuildContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,30 +26,63 @@ public function __construct(
public readonly string $staging,
public readonly string $version,
public readonly CommandRunner $runner,
private readonly Closure $output
private readonly Closure $output,
public readonly bool $verbose = false
) {
}

/** How much of a streamed command's output a failure repeats: it was already printed once. */
private const FAILURE_TAIL = 20;

/**
* Run a shell command, failing the step on a non-zero exit code.
*
* @param bool|null $stream Print the command's output as it runs. Null follows `Build::verbose()`.
*/
public function exec(string $step, string $command, string $workingDirectory): string
public function exec(string $step, string $command, string $workingDirectory, ?bool $stream = null): string
{
$stream ??= $this->verbose;
$this->say(" $ {$command}");
$result = $this->runner->run($command, $workingDirectory);

$print = function (string $line): void {
$this->say(' | ' . $line);
};
if ($stream && $this->runner instanceof StreamingCommandRunner) {
$result = $this->runner->stream($command, $workingDirectory, $print);
} else {
$result = $this->runner->run($command, $workingDirectory);
if ($stream) {
// This runner can't stream: show the output now that the command has ended.
array_map($print, self::lines($result['output']));
}
}

if ($result['exitCode'] !== 0) {
$lines = self::lines($result['output']);
$shown = $stream ? array_slice($lines, -self::FAILURE_TAIL) : $lines;
throw BuildException::inStep($step, sprintf(
'"%s" exited with %d:%s%s',
'"%s" exited with %d:%s%s%s',
$command,
$result['exitCode'],
PHP_EOL,
trim($result['output'])
count($shown) < count($lines) ? sprintf('(last %d lines)%s', count($shown), PHP_EOL) : '',
implode(PHP_EOL, $shown)
));
}

return $result['output'];
}

/**
* @return list<string>
*/
private static function lines(string $output): array
{
$output = trim(str_replace("\r\n", "\n", $output));

return $output === '' ? [] : explode("\n", $output);
}

public function say(string $line): void
{
($this->output)($line);
Expand Down
3 changes: 3 additions & 0 deletions packages/build/src/JsonConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ public static function load(string $projectDirectory, string $file = 'wptoolkit.
$type = $config['type'] ?? (is_file($projectDirectory . '/style.css') ? 'theme' : 'plugin');
$build = $type === 'theme' ? Build::theme($projectDirectory) : Build::plugin($projectDirectory);

if (!empty($config['verbose'])) {
$build->verbose();
}
if (isset($config['version'])) {
$build->version(self::versionSource((string) $config['version']));
}
Expand Down
16 changes: 14 additions & 2 deletions packages/build/src/ProcessCommandRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,20 @@

namespace Codad5\WPToolkit\Build;

use Closure;

/**
* Runs commands through the system shell with proc_open, capturing stdout and stderr together.
*/
final class ProcessCommandRunner implements CommandRunner
final class ProcessCommandRunner implements StreamingCommandRunner
{
public function run(string $command, string $workingDirectory): array
{
return $this->stream($command, $workingDirectory, static function (string $line): void {
});
}

public function stream(string $command, string $workingDirectory, Closure $onLine): array
{
// stderr joins stdout in the shell, so one pipe carries both and can't deadlock.
$process = proc_open($command . ' 2>&1', [1 => ['pipe', 'w']], $pipes, $workingDirectory);
Expand All @@ -22,7 +30,11 @@ public function run(string $command, string $workingDirectory): array
return ['exitCode' => 127, 'output' => "Could not start: {$command}"];
}

$output = (string) stream_get_contents($pipes[1]);
$output = '';
while (($line = fgets($pipes[1])) !== false) {
$output .= $line;
$onLine(rtrim($line, "\r\n"));
}
fclose($pipes[1]);

return ['exitCode' => proc_close($process), 'output' => $output];
Expand Down
16 changes: 15 additions & 1 deletion packages/build/src/Steps/RunCommands.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,25 @@ final class RunCommands implements BuildStep
/** @var list<string> */
private readonly array $commands;

/** True: always print these commands' output as they run. Null: follow `Build::verbose()`. */
private ?bool $stream = null;

public function __construct(string ...$commands)
{
$this->commands = array_values($commands);
}

/**
* Commands whose output is printed as they run, whether or not the build is verbose.
*/
public static function streamed(string ...$commands): self
{
$step = new self(...$commands);
$step->stream = true;

return $step;
}

public function name(): string
{
return 'run';
Expand All @@ -38,7 +52,7 @@ public function phase(): Phase
public function run(BuildContext $context): void
{
foreach ($this->commands as $command) {
$context->exec($this->name(), $command, $context->project->root);
$context->exec($this->name(), $command, $context->project->root, $this->stream);
}
}
}
25 changes: 25 additions & 0 deletions packages/build/src/StreamingCommandRunner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

/**
* @license GPL-2.0-or-later
*/

declare(strict_types=1);

namespace Codad5\WPToolkit\Build;

use Closure;

/**
* A runner that can hand over a command's output while it is still running (`Build::verbose()`).
* A separate interface so existing `CommandRunner` implementations keep working: with one of those,
* verbose output is printed when the command ends.
*/
interface StreamingCommandRunner extends CommandRunner
{
/**
* @param Closure(string): void $onLine Called once per line of stdout or stderr, without the line ending.
* @return array{exitCode: int, output: string} The whole output is still returned.
*/
public function stream(string $command, string $workingDirectory, Closure $onLine): array;
}
Loading
Loading