Skip to content

Commit 2642e84

Browse files
authored
Merge pull request #997 from Automattic/issue-992-composer-extra-plugin-autoload
Prepare local recipe plugins with Composer autoload
2 parents e14cbf2 + ccf6144 commit 2642e84

3 files changed

Lines changed: 163 additions & 5 deletions

File tree

packages/cli/src/recipe-sources.ts

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export interface RecipeSourceProvenance {
3939
maxExtractedFiles: number
4040
sha256Required: boolean
4141
}
42-
localPathCategory?: "recipe-relative" | "temporary-download"
42+
localPathCategory?: "recipe-relative" | "temporary-download" | "temporary-composer-autoload"
4343
}
4444

4545
interface PreparedExternalSource {
@@ -279,23 +279,72 @@ export async function prepareRecipeExtraPlugins(recipe: WorkspaceRecipe, recipeD
279279
const resolved = await prepareRecipeSource(plugin.source, recipeDirectory, slug, plugin.sha256)
280280
const pluginFile = await resolveRecipeExtraPluginFile(plugin, recipeDirectory)
281281
const loadAs = plugin.loadAs ?? "plugin"
282-
await assertPreparedPluginFileExists(resolved.source, pluginFile.slice(slug.length + 1), plugin.source)
282+
const prepared = await prepareComposerAutoloadForPlugin(resolved, slug, plugin.source)
283+
await assertPreparedPluginFileExists(prepared.source, pluginFile.slice(slug.length + 1), plugin.source)
283284
plugins.push({
284-
source: resolved.source,
285+
source: prepared.source,
285286
slug,
286287
target: pluginTarget(slug, loadAs),
287288
pluginFile,
288289
activate: plugin.activate !== false,
289290
loadAs,
290-
cleanupPaths: resolved.cleanupPaths,
291-
provenance: resolved.provenance,
291+
cleanupPaths: prepared.cleanupPaths,
292+
provenance: prepared.provenance,
292293
metadata: plugin.metadata ?? {},
293294
})
294295
}
295296

296297
return plugins
297298
}
298299

300+
async function prepareComposerAutoloadForPlugin(prepared: PreparedExternalSource, slug: string, sourceRef: string): Promise<PreparedExternalSource> {
301+
if (prepared.provenance.kind !== "local") {
302+
return prepared
303+
}
304+
305+
try {
306+
const composerJson = await stat(join(prepared.source, "composer.json"))
307+
if (!composerJson.isFile()) {
308+
return prepared
309+
}
310+
} catch {
311+
return prepared
312+
}
313+
314+
try {
315+
const autoload = await stat(join(prepared.source, "vendor", "autoload.php"))
316+
if (autoload.isFile()) {
317+
return prepared
318+
}
319+
} catch {
320+
// Prepare a temporary copy below.
321+
}
322+
323+
const stagingRoot = await mkdtemp(join(tmpdir(), `wp-codebox-plugin-${slug}-`))
324+
const stagedSource = join(stagingRoot, slug)
325+
await cp(prepared.source, stagedSource, { recursive: true })
326+
try {
327+
await execFileAsync("composer", ["install", "--no-dev", "--prefer-dist", "--no-interaction", "--no-progress", "--no-scripts", "--no-plugins"], {
328+
cwd: stagedSource,
329+
maxBuffer: 1024 * 1024 * 10,
330+
})
331+
} catch (error) {
332+
await rm(stagingRoot, { recursive: true, force: true })
333+
const detail = error instanceof Error && "stderr" in error && typeof error.stderr === "string" && error.stderr.trim() ? error.stderr.trim() : error instanceof Error ? error.message : String(error)
334+
throw new Error(`Recipe extra plugin source requires Composer autoload but could not be prepared: ${sourceRef}: ${detail}`)
335+
}
336+
337+
return {
338+
...prepared,
339+
source: stagedSource,
340+
cleanupPaths: [...prepared.cleanupPaths, stagingRoot],
341+
provenance: {
342+
...prepared.provenance,
343+
localPathCategory: "temporary-composer-autoload",
344+
},
345+
}
346+
}
347+
299348
export async function prepareRecipeDependencyOverlays(recipe: WorkspaceRecipe, recipeDirectory: string, extraPlugins: PreparedExtraPlugin[]): Promise<PreparedDependencyOverlay[]> {
300349
const overlays: PreparedDependencyOverlay[] = []
301350
for (const [index, overlay] of (recipe.inputs?.dependency_overlays ?? []).entries()) {
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import assert from "node:assert/strict"
2+
import { spawnSync } from "node:child_process"
3+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
4+
import { dirname, resolve } from "node:path"
5+
import { fileURLToPath } from "node:url"
6+
7+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..")
8+
const cli = resolve(root, "packages/cli/dist/index.js")
9+
const artifacts = resolve(root, "artifacts/recipe-run-composer-autoload-extra-plugin-smoke")
10+
const pluginSource = resolve(artifacts, "source-plugin")
11+
const recipePath = resolve(artifacts, "recipe.json")
12+
13+
rmSync(artifacts, { recursive: true, force: true })
14+
mkdirSync(resolve(pluginSource, "src"), { recursive: true })
15+
16+
writeFileSync(resolve(pluginSource, "composer.json"), `${JSON.stringify({
17+
name: "wp-codebox/composer-autoload-smoke",
18+
autoload: { classmap: ["src/"] },
19+
config: { "allow-plugins": false },
20+
}, null, 2)}\n`)
21+
22+
writeFileSync(resolve(pluginSource, "composer-autoload-smoke.php"), `<?php
23+
/**
24+
* Plugin Name: WP Codebox Composer Autoload Smoke
25+
*/
26+
27+
defined( 'ABSPATH' ) || exit;
28+
29+
$autoload = __DIR__ . '/vendor/autoload.php';
30+
if ( is_file( $autoload ) ) {
31+
require_once $autoload;
32+
}
33+
34+
register_activation_hook( __FILE__, static function (): void {
35+
if ( ! class_exists( \\WpCodeboxComposerSmoke\\Fixture::class ) ) {
36+
throw new RuntimeException( 'Composer classmap fixture was not autoloaded.' );
37+
}
38+
39+
update_option( 'wp_codebox_composer_smoke_value', \\WpCodeboxComposerSmoke\\Fixture::value() );
40+
} );
41+
`)
42+
43+
writeFileSync(resolve(pluginSource, "src", "Fixture.php"), `<?php
44+
45+
namespace WpCodeboxComposerSmoke;
46+
47+
final class Fixture {
48+
public static function value(): int {
49+
return 992;
50+
}
51+
}
52+
`)
53+
54+
writeFileSync(recipePath, `${JSON.stringify({
55+
schema: "wp-codebox/workspace-recipe/v1",
56+
runtime: {
57+
backend: "wordpress-playground",
58+
name: "recipe-run-composer-autoload-extra-plugin-smoke",
59+
wp: "7.0",
60+
blueprint: { steps: [] },
61+
},
62+
inputs: {
63+
extra_plugins: [
64+
{
65+
source: pluginSource,
66+
slug: "composer-autoload-smoke",
67+
pluginFile: "composer-autoload-smoke/composer-autoload-smoke.php",
68+
},
69+
],
70+
},
71+
workflow: {
72+
steps: [
73+
{
74+
command: "wordpress.run-php",
75+
args: [
76+
"code=if (!class_exists('WpCodeboxComposerSmoke\\\\Fixture')) { throw new RuntimeException('autoloaded class missing after plugin boot'); } echo wp_json_encode(array('value' => get_option('wp_codebox_composer_smoke_value'), 'active' => is_plugin_active('composer-autoload-smoke/composer-autoload-smoke.php')));",
77+
],
78+
},
79+
],
80+
},
81+
}, null, 2)}\n`)
82+
83+
assert.equal(existsSync(resolve(pluginSource, "vendor", "autoload.php")), false, "fixture source should start without Composer vendor/autoload.php")
84+
85+
const result = spawnSync(process.execPath, [
86+
cli,
87+
"recipe-run",
88+
"--recipe",
89+
recipePath,
90+
"--artifacts",
91+
artifacts,
92+
"--json",
93+
], { cwd: root, encoding: "utf8" })
94+
95+
assert.equal(result.status, 0, result.stderr || result.stdout)
96+
const output = JSON.parse(result.stdout)
97+
assert.equal(output.success, true)
98+
99+
const workflowExecution = output.executions.find((execution: { command: string; recipePhase?: string }) => execution.command === "wordpress.run-php" && execution.recipePhase === "steps")
100+
assert.ok(workflowExecution)
101+
102+
const workflowResult = JSON.parse(workflowExecution.stdout)
103+
assert.equal(workflowResult.value, "992")
104+
assert.equal(workflowResult.active, true)
105+
assert.equal(existsSync(resolve(pluginSource, "vendor", "autoload.php")), false, "recipe-run must not mutate the caller plugin checkout")
106+
assert.equal(existsSync(resolve(pluginSource, "composer.lock")), false, "recipe-run must not write Composer lockfiles into the caller plugin checkout")
107+
108+
console.log("recipe run Composer autoload extra plugin smoke passed")

scripts/smoke-manifest.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export const smokeGroups = {
7070
tsxSmoke("wordpress-state-contract-smoke"),
7171
tsxSmoke("playground-command-errors-smoke"),
7272
tsxSmoke("composer-backed-source-hydration-smoke"),
73+
tsxSmoke("recipe-run-composer-autoload-extra-plugin-smoke"),
7374
],
7475
},
7576
agent: {

0 commit comments

Comments
 (0)