Skip to content

Commit e4b4e63

Browse files
authored
Merge pull request #2057 from Automattic/fix/2055-overlay-package-layout
Preserve wrapped Composer overlay package layouts
2 parents 59ba87f + 4cefe6b commit e4b4e63

2 files changed

Lines changed: 60 additions & 15 deletions

File tree

packages/cli/src/recipe-sources.ts

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -469,10 +469,11 @@ async function prepareRecipeDependencyOverlay(overlay: WorkspaceRecipeDependency
469469
// consumer's autoloader resolves, keeping the single vendor-path mount intact.
470470
// Never mutate the caller's checkout: stage a copy first when the hydrated
471471
// source is still the original directory.
472-
const preparedSource = await reconcileOverlayAutoloadLayout(hydratedSource, source, stagingRoot, consumer.source, overlay.package)
472+
const prepared = await reconcileOverlayAutoloadLayout(hydratedSource, source, stagingRoot, consumer.source, overlay.package)
473+
const preparedSource = prepared.source
473474
if (reference) {
474475
await preserveComposerDependencyReference(consumer, overlay.package, reference, stagedConsumers)
475-
await preserveComposerPackageReference(preparedSource, overlay.package, reference)
476+
await preserveComposerPackageReference(prepared.packageRoot, overlay.package, reference)
476477
}
477478
const target = `${consumer.target}/vendor/${composerPackageVendorPath(overlay.package)}`
478479
const digest = await directoryContentDigest(preparedSource)
@@ -503,36 +504,48 @@ async function prepareRecipeDependencyOverlay(overlay: WorkspaceRecipeDependency
503504
}
504505

505506
/**
506-
* Move the overlay's PSR-4 source directories to the paths the consumer's
507-
* committed autoloader resolves for the same package. The overlay is mounted at
508-
* the consumer's `vendor/<package>` path and loaded via the consumer's PSR-4
509-
* map; when the override ships a different source layout than the consumer
510-
* recorded (e.g. `src/` vs `php-transformer/src/`), the mounted classes must be
511-
* relocated to the consumer's recorded layout or they are never autoloaded.
507+
* Reconcile the overlay with the paths the consumer's committed autoloader
508+
* resolves for the same package. A common wrapper prefix relocates the complete
509+
* hydrated package so package-local bootstrap state stays coherent; other PSR-4
510+
* layout changes retain the existing source-directory relocation.
512511
*/
513-
async function reconcileOverlayAutoloadLayout(hydratedSource: string, originalSource: string, stagingRoot: string, consumerSource: string, packageName: string): Promise<string> {
512+
async function reconcileOverlayAutoloadLayout(hydratedSource: string, originalSource: string, stagingRoot: string, consumerSource: string, packageName: string): Promise<{ source: string; packageRoot: string }> {
514513
const consumerPsr4 = await composerPackagePsr4FromInstalled(join(consumerSource, "vendor", "composer", "installed.json"), packageName)
515514
const overridePsr4 = await composerPackagePsr4FromInstalled(join(hydratedSource, "vendor", "composer", "installed.json"), packageName)
516515
?? await composerPackagePsr4FromComposerJson(join(hydratedSource, "composer.json"))
517516
if (!consumerPsr4 || !overridePsr4) {
518-
return hydratedSource
517+
return { source: hydratedSource, packageRoot: hydratedSource }
519518
}
520519

521520
const moves = new Map<string, string>()
521+
const layouts: Array<[string, string]> = []
522522
for (const [namespace, consumerDir] of Object.entries(consumerPsr4)) {
523523
const overrideDir = overridePsr4[namespace]
524524
if (undefined === overrideDir) {
525525
continue
526526
}
527527
const from = normalizeOverlayRelativeDir(overrideDir)
528528
const to = normalizeOverlayRelativeDir(consumerDir)
529-
if ("" === from || "" === to || from === to) {
529+
if ("" === from || "" === to) {
530+
continue
531+
}
532+
layouts.push([from, to])
533+
if (from === to) {
530534
continue
531535
}
532536
moves.set(from, to)
533537
}
534538
if (0 === moves.size) {
535-
return hydratedSource
539+
return { source: hydratedSource, packageRoot: hydratedSource }
540+
}
541+
542+
const wrapper = commonOverlayWrapperPrefix(layouts)
543+
if (wrapper) {
544+
const effectiveSource = join(stagingRoot, "reconciled-source")
545+
const packageRoot = join(effectiveSource, wrapper)
546+
await mkdir(dirname(packageRoot), { recursive: true })
547+
await cp(hydratedSource, packageRoot, { recursive: true })
548+
return { source: effectiveSource, packageRoot }
536549
}
537550

538551
// Copy into the overlay staging root before moving directories so the caller's
@@ -553,7 +566,23 @@ async function reconcileOverlayAutoloadLayout(hydratedSource: string, originalSo
553566
await mkdir(dirname(toPath), { recursive: true })
554567
await rename(fromPath, toPath)
555568
}
556-
return effectiveSource
569+
return { source: effectiveSource, packageRoot: effectiveSource }
570+
}
571+
572+
function commonOverlayWrapperPrefix(changes: Array<[string, string]>): string | undefined {
573+
let wrapper: string | undefined
574+
for (const [from, to] of changes) {
575+
const suffix = `/${from}`
576+
if (!to.endsWith(suffix)) {
577+
return undefined
578+
}
579+
const candidate = to.slice(0, -suffix.length)
580+
if (!candidate || normalizeOverlayRelativeDir(candidate) !== candidate || (wrapper && wrapper !== candidate)) {
581+
return undefined
582+
}
583+
wrapper = candidate
584+
}
585+
return wrapper
557586
}
558587

559588
/** @return namespace-prefix -> normalized relative source dir, or undefined. */

scripts/composer-package-overlay-autoload-layout-smoke.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import assert from "node:assert/strict"
2+
import { execFile } from "node:child_process"
23
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"
34
import { tmpdir } from "node:os"
45
import { join } from "node:path"
6+
import { promisify } from "node:util"
57
import { prepareRecipeDependencyOverlays } from "../packages/cli/src/recipe-sources.js"
68
import type { PreparedExtraPlugin } from "../packages/cli/src/recipe-sources.js"
79

@@ -13,6 +15,7 @@ import type { PreparedExtraPlugin } from "../packages/cli/src/recipe-sources.js"
1315
// released version keeps running.
1416

1517
const root = await mkdtemp(join(tmpdir(), "wp-codebox-overlay-autoload-layout-"))
18+
const execFileAsync = promisify(execFile)
1619

1720
async function exists(path: string): Promise<boolean> {
1821
try {
@@ -28,11 +31,14 @@ async function exists(path: string): Promise<boolean> {
2831
const overrideSource = join(root, "blocks-engine-php-transformer")
2932
await mkdir(join(overrideSource, "src", "ArtifactCompiler"), { recursive: true })
3033
await writeFile(join(overrideSource, "src", "ArtifactCompiler", "ArtifactCompiler.php"), "<?php\nnamespace Automattic\\BlocksEngine\\PhpTransformer\\ArtifactCompiler;\nfinal class ArtifactCompiler {}\n")
34+
await writeFile(join(overrideSource, "php-transformer.php"), "<?php\nrequire __DIR__ . '/vendor/autoload.php';\nif (!class_exists(Automattic\\BlocksEngine\\PhpTransformer\\ArtifactCompiler\\ArtifactCompiler::class)) { throw new RuntimeException('classmap bootstrap failed'); }\n")
3135
await writeFile(join(overrideSource, "composer.json"), JSON.stringify({
3236
name: "automattic/blocks-engine-php-transformer",
3337
autoload: { "psr-4": { "Automattic\\BlocksEngine\\PhpTransformer\\": "src/" } },
3438
}, null, 2))
3539
await mkdir(join(overrideSource, "vendor", "composer"), { recursive: true })
40+
await writeFile(join(overrideSource, "vendor", "autoload.php"), "<?php\nforeach (require __DIR__ . '/composer/autoload_classmap.php' as $path) { require $path; }\n")
41+
await writeFile(join(overrideSource, "vendor", "composer", "autoload_classmap.php"), "<?php\n$vendorDir = dirname(__DIR__);\n$baseDir = dirname($vendorDir);\nreturn ['Automattic\\\\BlocksEngine\\\\PhpTransformer\\\\ArtifactCompiler\\\\ArtifactCompiler' => $baseDir . '/src/ArtifactCompiler/ArtifactCompiler.php'];\n")
3642
await writeFile(join(overrideSource, "vendor", "composer", "installed.json"), JSON.stringify({ packages: [
3743
{ name: "automattic/blocks-engine-php-transformer", autoload: { "psr-4": { "Automattic\\BlocksEngine\\PhpTransformer\\": "src/" } } },
3844
] }, null, 2))
@@ -70,14 +76,24 @@ const dependencyOverlays = await prepareRecipeDependencyOverlays({
7076
try {
7177
assert.equal(dependencyOverlays.length, 1)
7278
const staged = dependencyOverlays[0].source
73-
// The class must be relocated to the consumer's recorded PSR-4 path so the
74-
// consumer autoloader resolves it after the vendor-path mount.
79+
// A shared wrapper layout moves the complete hydrated package, keeping its
80+
// bootstrap, package-local autoloader, and classmap base directory coherent.
7581
assert.equal(await exists(join(staged, "php-transformer", "src", "ArtifactCompiler", "ArtifactCompiler.php")), true, "override class relocated to consumer PSR-4 layout")
82+
assert.equal(await exists(join(staged, "php-transformer", "php-transformer.php")), true, "package bootstrap moves with its source")
83+
assert.equal(await exists(join(staged, "php-transformer", "vendor", "autoload.php")), true, "package-local autoloader moves with its source")
84+
assert.equal(await exists(join(staged, "php-transformer", "vendor", "composer", "autoload_classmap.php")), true, "package-local classmap moves with its source")
7685
assert.equal(await exists(join(staged, "src", "ArtifactCompiler", "ArtifactCompiler.php")), false, "override source layout no longer shadows the consumer path")
86+
assert.equal(await exists(join(staged, "php-transformer.php")), false, "bootstrap is not stranded at the old package root")
87+
assert.equal(await exists(join(staged, "vendor", "autoload.php")), false, "package-local autoloader is not stranded at the old package root")
88+
const classmap = await readFile(join(staged, "php-transformer", "vendor", "composer", "autoload_classmap.php"), "utf8")
89+
assert.match(classmap, /\$baseDir \. '\/src\/ArtifactCompiler\/ArtifactCompiler\.php'/, "classmap remains relative to the relocated complete package")
90+
await execFileAsync("php", [join(staged, "php-transformer", "php-transformer.php")])
7791
assert.equal(dependencyOverlays[0].target, "/wordpress/wp-content/plugins/consumer-plugin/vendor/automattic/blocks-engine-php-transformer")
7892
// The override checkout itself is never mutated.
7993
assert.equal(await exists(join(overrideSource, "php-transformer")), false, "override checkout is not restructured in place")
8094
assert.equal(await exists(join(overrideSource, "src", "ArtifactCompiler", "ArtifactCompiler.php")), true, "override checkout keeps its own layout")
95+
assert.equal(await exists(join(overrideSource, "php-transformer.php")), true, "override checkout keeps its bootstrap")
96+
assert.equal(await exists(join(overrideSource, "vendor", "autoload.php")), true, "override checkout keeps its package-local autoloader")
8197
} finally {
8298
await Promise.all([...dependencyOverlays, ...consumers].flatMap((overlay) => overlay.cleanupPaths).map((path) => rm(path, { recursive: true, force: true })))
8399
await rm(root, { recursive: true, force: true })

0 commit comments

Comments
 (0)