From 445ce7162eb21a43185fe2eebb572b64109fe50b Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Wed, 26 Aug 2026 08:33:32 -0400 Subject: [PATCH 1/3] perf: write generated Composer asset manifests directly without first copying their source files. --- CHANGELOG.md | 1 + src/Solver/Solver.php | 14 +++++-------- tests/Solver/SolverTest.php | 40 ++++++++++++++++++++++++++++++------- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8e33d6..c6dcdef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - feat!: require Bun `^1.4.0`, npm `>=10.9.8`, pnpm `^11.23.0`, or Yarn `^4.18.0` and remove legacy manager support. - fix: run manager commands in the configured root directory without changing the PHP working directory, and prevent manager probes and npm dependency cleanup when manager execution is disabled. - feat!: add secure frontend audits with CVE reporting, CI formats, and strict npm, pnpm, Yarn, and Bun validation. +- perf: write generated Composer asset manifests directly without first copying their source files. ## 0.2.0 January 24, 2026 diff --git a/src/Solver/Solver.php b/src/Solver/Solver.php index 8850642..f99267c 100644 --- a/src/Solver/Solver.php +++ b/src/Solver/Solver.php @@ -173,7 +173,7 @@ private function getAssets(Composer $composer, string $assetDir, array $packages * @param string $assetDir The asset directory. * @param string $filename The filename of asset package. * - * @throws Exception if the asset package cannot be copied or written. + * @throws Exception if the asset package cannot be read or written. * * @return array{0: string, 1: string} The package name and absolute generated manifest path. */ @@ -194,17 +194,13 @@ private function getMockPackagePath(PackageInterface $package, string $assetDir, ); } - if (!$this->fs->copy($filename, $newFilename)) { - throw new RuntimeException( - sprintf('Unable to copy asset manifest "%s".', $filename), - ); - } + $sourceJsonFile = new JsonFile($filename); - $jsonFile = new JsonFile($newFilename); + $packageValue = AssetUtil::formatPackage($package, $packageName, (array) $sourceJsonFile->read()); - $packageValue = AssetUtil::formatPackage($package, $packageName, (array) $jsonFile->read()); + $targetJsonFile = new JsonFile($newFilename); - $jsonFile->write($packageValue); + $targetJsonFile->write($packageValue); return [$packageName, $newFilename]; } diff --git a/tests/Solver/SolverTest.php b/tests/Solver/SolverTest.php index a4318ff..2c72298 100644 --- a/tests/Solver/SolverTest.php +++ b/tests/Solver/SolverTest.php @@ -84,23 +84,49 @@ public function testCanonicalizePathStopsAtFilesystemRootWhenExistenceCheckFails ); } - public function testGetMockPackagePathRejectsCopyFailure(): void + public function testGetMockPackagePathDoesNotCopySourceBeforeWritingFormattedManifest(): void { - $assetDir = $this->cwd . '/copy-failure-assets'; + $assetDir = $this->cwd . '/direct-write-assets'; $source = $this->cwd . '/source-package.json'; $target = $assetDir . '/foo/bar/source-package.json'; + $sourceContent = <<<'JSON' + { + "name": "source-package", + "version": "1.2.3", + "scripts": { + "build": "ignored" + }, + "dependencies": { + "dependency": "^1.0" + } + } + JSON; $package = $this->createMock(PackageInterface::class); $package->method('getName')->willReturn('foo/bar'); - file_put_contents($source, '{}'); + file_put_contents($source, $sourceContent); $fs = $this->getMockBuilder(Filesystem::class)->onlyMethods(['copy'])->getMock(); - $fs->expects(self::once())->method('copy')->with($source, $target)->willReturn(false); + $fs->expects(self::never())->method('copy'); $solver = new Solver($this->manager, $this->config, $fs, $this->composerFallback); - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage(sprintf('Unable to copy asset manifest "%s".', $source)); + $result = $this->invokeSolverMethodOn($solver, 'getMockPackagePath', $package, $assetDir, $source); + + self::assertSame(['@composer-asset/foo--bar', $target], $result); + self::assertSame($sourceContent, file_get_contents($source)); + self::assertFileExists($target); + self::assertSame( + <<<'JSON' + { + "name": "@composer-asset/foo--bar", + "version": "1.2.3", + "dependencies": { + "dependency": "^1.0" + } + } - $this->invokeSolverMethodOn($solver, 'getMockPackagePath', $package, $assetDir, $source); + JSON, + file_get_contents($target), + ); } public function testGetMockPackagePathWrapsDirectoryCreationFailure(): void From 27f4a6104b243b67eddea716986ca59ec8b45ff6 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Wed, 26 Aug 2026 08:53:44 -0400 Subject: [PATCH 2/3] Fix Quality ci. --- tests/Solver/SolverTest.php | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/tests/Solver/SolverTest.php b/tests/Solver/SolverTest.php index 2c72298..da65a90 100644 --- a/tests/Solver/SolverTest.php +++ b/tests/Solver/SolverTest.php @@ -86,23 +86,27 @@ public function testCanonicalizePathStopsAtFilesystemRootWhenExistenceCheckFails public function testGetMockPackagePathDoesNotCopySourceBeforeWritingFormattedManifest(): void { - $assetDir = $this->cwd . '/direct-write-assets'; - $source = $this->cwd . '/source-package.json'; - $target = $assetDir . '/foo/bar/source-package.json'; - $sourceContent = <<<'JSON' + $assetDir = "{$this->cwd}/direct-write-assets"; + $source = "{$this->cwd}/source-package.json"; + + $target = "{$assetDir}/foo/bar/source-package.json"; + + $sourceContent = <<createMock(PackageInterface::class); $package->method('getName')->willReturn('foo/bar'); + file_put_contents($source, $sourceContent); $fs = $this->getMockBuilder(Filesystem::class)->onlyMethods(['copy'])->getMock(); From 0e4914ea192c2e00025270614628fef381828c91 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Wed, 26 Aug 2026 09:31:38 -0400 Subject: [PATCH 3/3] Fix Workflow ci. --- src/Solver/Solver.php | 15 +++++++++-- tests/Solver/SolverTest.php | 33 ++++++++++++++++++----- tests/Support/InternalMockerExtension.php | 4 +++ 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/Solver/Solver.php b/src/Solver/Solver.php index f99267c..0330468 100644 --- a/src/Solver/Solver.php +++ b/src/Solver/Solver.php @@ -27,6 +27,7 @@ use function is_dir; use function is_file; use function is_string; +use function json_decode; use function realpath; use function rtrim; use function sprintf; @@ -194,9 +195,19 @@ private function getMockPackagePath(PackageInterface $package, string $assetDir, ); } - $sourceJsonFile = new JsonFile($filename); + $sourceContent = file_get_contents($filename); - $packageValue = AssetUtil::formatPackage($package, $packageName, (array) $sourceJsonFile->read()); + if (false === $sourceContent) { + throw new RuntimeException( + sprintf('Unable to read asset manifest "%s".', $filename), + ); + } + + $packageValue = AssetUtil::formatPackage( + $package, + $packageName, + (array) json_decode($sourceContent, false, flags: JSON_THROW_ON_ERROR), + ); $targetJsonFile = new JsonFile($newFilename); diff --git a/tests/Solver/SolverTest.php b/tests/Solver/SolverTest.php index da65a90..c26b2f3 100644 --- a/tests/Solver/SolverTest.php +++ b/tests/Solver/SolverTest.php @@ -95,12 +95,12 @@ public function testGetMockPackagePathDoesNotCopySourceBeforeWritingFormattedMan { "name": "source-package", "version": "1.2.3", + "engines": {}, + "bundleDependencies": [], "scripts": { "build": "ignored" }, - "dependencies": { - "dependency": "^1.0" - } + "dependencies": {} } JSON; @@ -123,9 +123,9 @@ public function testGetMockPackagePathDoesNotCopySourceBeforeWritingFormattedMan { "name": "@composer-asset/foo--bar", "version": "1.2.3", - "dependencies": { - "dependency": "^1.0" - } + "engines": {}, + "bundleDependencies": [], + "dependencies": {} } JSON, @@ -133,6 +133,27 @@ public function testGetMockPackagePathDoesNotCopySourceBeforeWritingFormattedMan ); } + public function testGetMockPackagePathRejectsUnreadableSourceManifest(): void + { + $assetDir = "{$this->cwd}/unreadable-source-assets"; + $source = "{$this->cwd}/source-package.json"; + + $package = $this->createMock(PackageInterface::class); + $package->method('getName')->willReturn('foo/bar'); + + MockerState::addCondition( + 'Foxy\\Solver', + 'file_get_contents', + [$source, false, null, 0, null], + false, + ); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage(sprintf('Unable to read asset manifest "%s".', $source)); + + $this->invokeSolverMethod('getMockPackagePath', $package, $assetDir, $source); + } + public function testGetMockPackagePathWrapsDirectoryCreationFailure(): void { $assetDir = $this->cwd . '/directory-failure-assets'; diff --git a/tests/Support/InternalMockerExtension.php b/tests/Support/InternalMockerExtension.php index d61775f..137023e 100644 --- a/tests/Support/InternalMockerExtension.php +++ b/tests/Support/InternalMockerExtension.php @@ -60,6 +60,10 @@ public static function load(): void 'namespace' => 'Foxy\\Solver', 'name' => 'file_exists', ], + [ + 'namespace' => 'Foxy\\Solver', + 'name' => 'file_get_contents', + ], [ 'namespace' => 'Foxy\\Solver', 'name' => 'file_put_contents',