diff --git a/docs/recipe-contract.md b/docs/recipe-contract.md index b9ceabdc2..dc013969d 100644 --- a/docs/recipe-contract.md +++ b/docs/recipe-contract.md @@ -204,7 +204,9 @@ the mounted plugin slug. Supported `loadAs` values: - `plugin`: mount below `/wordpress/wp-content/plugins/` and activate when - `activate` is not `false`. + `activate` is not `false`. Composer autoloaders are preloaded only for these + active plugin inputs; an inactive plugin is mounted without executing its + `vendor/autoload.php`. - `mu-plugin`: mount below `/wordpress/wp-content/mu-plugins/wp-codebox-runtime/` and load through WP Codebox's generated MU-plugin loader. Use this for sandbox runtime diff --git a/package.json b/package.json index 74779eb76..31d59b08d 100644 --- a/package.json +++ b/package.json @@ -234,6 +234,7 @@ "test:composer-package-overlay-revision": "tsx scripts/composer-backed-source-hydration-smoke.ts", "test:composer-package-overlay-autoload-layout": "tsx scripts/composer-package-overlay-autoload-layout-smoke.ts", "test:composer-installed-versions-loader-order": "tsx scripts/composer-installed-versions-loader-order-smoke.ts", + "test:recipe-extra-plugin-composer-autoloaders": "tsx tests/recipe-extra-plugin-composer-autoloaders.test.ts", "test:runtime-preset-registry": "tsx tests/runtime-preset-registry.test.ts", "test:generic-ability-runtime-run": "tsx tests/generic-ability-runtime-run.test.ts", "test:provider-runtime-contracts": "tsx tests/provider-runtime-contracts.test.ts", diff --git a/packages/cli/src/recipe-sources.ts b/packages/cli/src/recipe-sources.ts index 5058374ad..7ed060c70 100644 --- a/packages/cli/src/recipe-sources.ts +++ b/packages/cli/src/recipe-sources.ts @@ -1796,7 +1796,7 @@ echo wp_json_encode(array('command' => 'install-mu-plugins', 'plugins' => $plugi export function installPluginComposerAutoloadersCode(extraPlugins: PreparedExtraPlugin[]): string | null { const plugins = extraPlugins - .filter((plugin) => plugin.loadAs === "plugin") + .filter((plugin) => plugin.loadAs === "plugin" && plugin.activate) .map((plugin) => plugin.pluginFile) if (plugins.length === 0) { @@ -1804,6 +1804,7 @@ export function installPluginComposerAutoloadersCode(extraPlugins: PreparedExtra } return `$plugins = ${JSON.stringify(plugins)}; +$autoloaders = array(); if (!is_dir(WPMU_PLUGIN_DIR) && !mkdir(WPMU_PLUGIN_DIR, 0777, true) && !is_dir(WPMU_PLUGIN_DIR)) { throw new RuntimeException('Could not create mu-plugins directory.'); } @@ -1828,16 +1829,20 @@ foreach ($plugins as $plugin) { } $package_autoload = WP_PLUGIN_DIR . '/' . $plugin_dir . '/vendor/autoload_packages.php'; if (is_file($package_autoload)) { - $lines[] = "require_once WP_PLUGIN_DIR . '/" . str_replace("'", "\\'", $plugin_dir) . "/vendor/autoload_packages.php';"; + $autoload_path = $plugin_dir . '/vendor/autoload_packages.php'; + $autoloaders[] = $autoload_path; + $lines[] = "require_once WP_PLUGIN_DIR . '/" . str_replace("'", "\\'", $autoload_path) . "';"; continue; } $autoload = WP_PLUGIN_DIR . '/' . $plugin_dir . '/vendor/autoload.php'; if (is_file($autoload)) { - $lines[] = "require_once WP_PLUGIN_DIR . '/" . str_replace("'", "\\'", $plugin_dir) . "/vendor/autoload.php';"; + $autoload_path = $plugin_dir . '/vendor/autoload.php'; + $autoloaders[] = $autoload_path; + $lines[] = "require_once WP_PLUGIN_DIR . '/" . str_replace("'", "\\'", $autoload_path) . "';"; } } if (false === file_put_contents($loader, implode("\n", $lines) . "\n")) { throw new RuntimeException('Could not write WP Codebox Composer autoloader loader.'); } -echo wp_json_encode(array('command' => 'install-composer-autoloaders', 'plugins' => $plugins, 'loader' => $loader), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);` +echo wp_json_encode(array('command' => 'install-composer-autoloaders', 'plugins' => $plugins, 'autoloaders' => $autoloaders, 'loader' => $loader), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);` } diff --git a/tests/recipe-extra-plugin-composer-autoloaders.test.ts b/tests/recipe-extra-plugin-composer-autoloaders.test.ts new file mode 100644 index 000000000..81802e17a --- /dev/null +++ b/tests/recipe-extra-plugin-composer-autoloaders.test.ts @@ -0,0 +1,78 @@ +import assert from "node:assert/strict" +import { execFile as execFileCallback } from "node:child_process" +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises" +import { tmpdir } from "node:os" +import { join } from "node:path" +import { promisify } from "node:util" + +import { installPluginComposerAutoloadersCode, type PreparedExtraPlugin } from "../packages/cli/src/recipe-sources.js" + +const execFile = promisify(execFileCallback) +const root = await mkdtemp(join(tmpdir(), "wp-codebox-extra-plugin-autoloaders-")) +const pluginsDir = join(root, "plugins") +const muPluginsDir = join(root, "mu-plugins") + +function plugin(slug: string, activate: boolean): PreparedExtraPlugin { + return { + source: join(pluginsDir, slug), + slug, + target: `/wordpress/wp-content/plugins/${slug}`, + pluginFile: `${slug}/${slug}.php`, + activate, + loadAs: "plugin", + cleanupPaths: [], + provenance: { kind: "local", original: join(pluginsDir, slug) }, + } +} + +try { + await mkdir(join(pluginsDir, "active-provider", "vendor"), { recursive: true }) + await mkdir(join(pluginsDir, "inactive-plugin", "vendor"), { recursive: true }) + await mkdir(join(pluginsDir, "inactive-plugin", "tests"), { recursive: true }) + await mkdir(muPluginsDir, { recursive: true }) + + await writeFile(join(pluginsDir, "active-provider", "vendor", "autoload.php"), " function_exists('wp_codebox_active_provider_loaded'), + 'inactive_dev_sentinel_loaded' => function_exists('wp_codebox_inactive_plugin_dev_sentinel'), +)); +` + const { stdout } = await execFile("php", ["-r", php]) + const [evidenceJson, resultJson] = stdout.split("\n---RESULT---\n") + assert.deepEqual(JSON.parse(evidenceJson), { + command: "install-composer-autoloaders", + plugins: ["active-provider/active-provider.php"], + autoloaders: ["active-provider/vendor/autoload.php"], + loader: join(muPluginsDir, "wp-codebox-composer-autoloaders.php"), + }, "setup evidence names each intentionally preloaded autoloader") + assert.deepEqual(JSON.parse(resultJson), { + active_provider_loaded: true, + inactive_dev_sentinel_loaded: false, + }) + + console.log("recipe extra plugin composer autoloaders ok") +} finally { + await rm(root, { recursive: true, force: true }) +}