From 085dbab99c602e3f0dc93bf731cbb0b368f9407d Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 30 May 2022 11:44:49 -0300 Subject: [PATCH 01/26] Adds illuminate/support as a dependency --- composer.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index fd77496..fd7255f 100644 --- a/composer.json +++ b/composer.json @@ -17,10 +17,12 @@ ], "require": { "php": "^8.0|^8.1", - "spatie/laravel-package-tools": "^1.9.2", - "illuminate/contracts": "^8.73|^9.0" + "illuminate/contracts": "^8.73|^9.0", + "illuminate/support": "^8.47|^9.0", + "spatie/laravel-package-tools": "^1.9.2" }, "require-dev": { + "guzzlehttp/guzzle": "^7.4", "nunomaduro/collision": "^5.10|^6.0", "nunomaduro/larastan": "^1.0", "orchestra/testbench": "^6.24", From 897d7513cb7c2538c61b6c4c4e9950f21961c68f Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 30 May 2022 11:45:04 -0300 Subject: [PATCH 02/26] Adds the importmap:outdated command --- src/Commands/OutdatedCommand.php | 40 ++++++++++ src/ImportmapLaravelServiceProvider.php | 1 + src/Npm.php | 94 ++++++++++++++++++++++ src/OutdatedPackage.php | 15 ++++ tests/NpmTest.php | 102 ++++++++++++++++++++++++ 5 files changed, 252 insertions(+) create mode 100644 src/Commands/OutdatedCommand.php create mode 100644 src/Npm.php create mode 100644 src/OutdatedPackage.php create mode 100644 tests/NpmTest.php diff --git a/src/Commands/OutdatedCommand.php b/src/Commands/OutdatedCommand.php new file mode 100644 index 0000000..03ddbb1 --- /dev/null +++ b/src/Commands/OutdatedCommand.php @@ -0,0 +1,40 @@ +outdatedPackages(); + + if ($outdatedPackages->isEmpty()) { + $this->info("No outdated packages found."); + + return Command::SUCCESS; + } + + $this->table( + ['Package', 'Current', 'Latest'], + $outdatedPackages + ->map(fn (OutdatedPackage $package) => [$package->name, $package->currentVersion, $package->latestVersion ?: $package->error]) + ->all(), + ); + + $packageLabel = Str::plural('package', $outdatedPackages->count()); + + $this->newLine(); + $this->error(sprintf('%d outdated %s found.', $outdatedPackages->count(), $packageLabel)); + + return Command::FAILURE; + } +} diff --git a/src/ImportmapLaravelServiceProvider.php b/src/ImportmapLaravelServiceProvider.php index dacad4e..b7222cf 100644 --- a/src/ImportmapLaravelServiceProvider.php +++ b/src/ImportmapLaravelServiceProvider.php @@ -26,6 +26,7 @@ public function configurePackage(Package $package): void ->hasCommand(Commands\JsonCommand::class) ->hasCommand(Commands\PinCommand::class) ->hasCommand(Commands\UnpinCommand::class) + ->hasCommand(Commands\OutdatedCommand::class) ; } diff --git a/src/Npm.php b/src/Npm.php new file mode 100644 index 0000000..ef7ea4e --- /dev/null +++ b/src/Npm.php @@ -0,0 +1,94 @@ +packagesWithVersion()->reduce(function (Collection $outdatedPackages, string $url) { + $package = $this->extractVendorName($url); + + if (! $package) { + return $outdatedPackages; + } + + if (! ($response = $this->getPackage($package))) { + $package->error = "Response error"; + } elseif ($response["error"] ?? false) { + $package->error = $response["error"]; + } else { + $latestVersion = $this->findLatestVersion($response); + + if (! $this->outdated($package->currentVersion, $latestVersion)) { + return $outdatedPackages; + } + + $package->latestVersion = $latestVersion; + } + + return $outdatedPackages->add($package); + }, collect()); + } + + private function packagesWithVersion(): Collection + { + return collect($this->importmap->asArray(fn ($url) => $url)["imports"]); + } + + private function extractVendorName(string $url) + { + $matches = null; + preg_match('/^.*(?<=npm:|npm\/|skypack\.dev\/|unpkg\.com\/)(.*)(?=@\d+\.\d+\.\d+)@(\d+\.\d+\.\d+(?:[^\/\s"]*)).*$/', $url, $matches); + + if (count($matches) !== 3) { + return null; + } + + return new OutdatedPackage(name: $matches[1], currentVersion: $matches[2]); + } + + private function getPackage(OutdatedPackage $package) + { + $response = Http::get($this->baseUrl . "/" . $package->name); + + if (! $response->ok()) { + return null; + } + + return $response->json(); + } + + private function findLatestVersion(array $json) + { + $latestVersion = data_get($json, "dist-tags.latest"); + + if ($latestVersion) { + return $latestVersion; + } + + if (! isset($json["versions"])) { + return; + } + + return collect($json["versions"]) + ->keys() + ->sort(fn ($versionA, $versionB) => version_compare($versionB, $versionA)) + ->values() + ->first(); + } + + private function outdated(string $currentVersion, string $latestVersion) + { + return version_compare($currentVersion, $latestVersion) === -1; + } +} diff --git a/src/OutdatedPackage.php b/src/OutdatedPackage.php new file mode 100644 index 0000000..2a42d7a --- /dev/null +++ b/src/OutdatedPackage.php @@ -0,0 +1,15 @@ +importmap = new Importmap(); + $this->npm = new Npm($this->importmap); + } + + /** @test */ + public function no_oudated_packages() + { + $this->importmap->pin("md5", "https://cdn.skypack.dev/md5@2.2.0"); + + Http::fake(fn () => Http::response([ + "dist-tags" => [ + "latest" => "2.2.0", + ], + ])); + + $this->assertCount(0, $this->npm->outdatedPackages()); + } + + /** @test */ + public function handles_error_when_fails_to_fetch_latest_version_of_package() + { + $this->importmap->pin("md5", "https://cdn.skypack.dev/md5@2.2.0"); + + Http::fake(fn () => Http::response([], 404)); + + $this->assertCount(1, $packages = $this->npm->outdatedPackages()); + $this->assertEquals("md5", $packages->first()->name); + $this->assertEquals("2.2.0", $packages->first()->currentVersion); + $this->assertNull($packages->first()->latestVersion); + $this->assertEquals("Response error", $packages->first()->error); + } + + /** @test */ + public function handles_error_when_returns_ok_but_response_json_contains_error() + { + $this->importmap->pin("md5", "https://cdn.skypack.dev/md5@2.2.0"); + + Http::fake(fn () => Http::response([ + "error" => "Something went wrong", + ], 200)); + + $this->assertCount(1, $packages = $this->npm->outdatedPackages()); + $this->assertEquals("md5", $packages->first()->name); + $this->assertEquals("2.2.0", $packages->first()->currentVersion); + $this->assertNull($packages->first()->latestVersion); + $this->assertEquals("Something went wrong", $packages->first()->error); + } + + /** @test */ + public function finds_outdated_package() + { + $this->importmap->pin("md5", "https://cdn.skypack.dev/md5@2.2.0"); + + Http::fake(fn () => Http::response([ + "dist-tags" => [ + "latest" => "2.2.1", + ], + ])); + + $this->assertCount(1, $packages = $this->npm->outdatedPackages()); + $this->assertEquals("md5", $packages->first()->name); + $this->assertEquals("2.2.0", $packages->first()->currentVersion); + $this->assertEquals("2.2.1", $packages->first()->latestVersion); + $this->assertNull($packages->first()->error); + } + + /** @test */ + public function finds_outdated_package_comparing_versions() + { + $this->importmap->pin("md5", "https://cdn.skypack.dev/md5@2.2.0"); + + Http::fake(fn () => Http::response([ + "versions" => [ + "2.0.0" => [], + "2.2.2" => [], + "1.2.0" => [], + "1.7.0" => [], + ], + ])); + + $this->assertCount(1, $packages = $this->npm->outdatedPackages()); + $this->assertEquals("md5", $packages->first()->name); + $this->assertEquals("2.2.0", $packages->first()->currentVersion); + $this->assertEquals("2.2.2", $packages->first()->latestVersion); + $this->assertNull($packages->first()->error); + } +} From d392eff086695389a0421bd0153a43159fbe3bb5 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 30 May 2022 12:45:01 -0300 Subject: [PATCH 03/26] Adds the importmap:audit command --- src/Commands/AuditCommand.php | 51 +++++++++++++++++++++++++ src/ImportmapLaravelServiceProvider.php | 1 + src/Npm.php | 39 +++++++++++++++++++ src/VulnerablePackage.php | 14 +++++++ tests/NpmTest.php | 33 ++++++++++++++++ 5 files changed, 138 insertions(+) create mode 100644 src/Commands/AuditCommand.php create mode 100644 src/VulnerablePackage.php diff --git a/src/Commands/AuditCommand.php b/src/Commands/AuditCommand.php new file mode 100644 index 0000000..78bd67e --- /dev/null +++ b/src/Commands/AuditCommand.php @@ -0,0 +1,51 @@ +vulnerablePackages(); + + if ($vulnerablePackages->isEmpty()) { + $this->info("No vulnerable packages found."); + + return self::SUCCESS; + } + + $this->table( + ['Package', 'Severity', 'Vundlerable Versions', 'Vulnerability'], + $vulnerablePackages + ->map(fn (VulnerablePackage $package) => [$package->name, $package->severity, $package->vulnerableVersions, $package->vulnerability]) + ->all() + ); + + $this->newLine(); + + $summary = $vulnerablePackages + ->groupBy('severity') + ->map(fn ($vulns) => $vulns->count()) + ->sortDesc() + ->map(fn ($count, $severity) => "$count {$severity}") + ->join(", "); + + $this->error(sprintf( + "%d %s found: %s", + $vulnerablePackages->count(), + Str::plural('vulnerability', $vulnerablePackages->count()), + $summary, + )); + + return self::FAILURE; + } +} diff --git a/src/ImportmapLaravelServiceProvider.php b/src/ImportmapLaravelServiceProvider.php index b7222cf..0fd25f3 100644 --- a/src/ImportmapLaravelServiceProvider.php +++ b/src/ImportmapLaravelServiceProvider.php @@ -27,6 +27,7 @@ public function configurePackage(Package $package): void ->hasCommand(Commands\PinCommand::class) ->hasCommand(Commands\UnpinCommand::class) ->hasCommand(Commands\OutdatedCommand::class) + ->hasCommand(Commands\AuditCommand::class) ; } diff --git a/src/Npm.php b/src/Npm.php index ef7ea4e..faf695a 100644 --- a/src/Npm.php +++ b/src/Npm.php @@ -40,6 +40,33 @@ public function outdatedPackages(): Collection }, collect()); } + public function vulnerablePackages(): Collection + { + $data = $this->packagesWithVersion() + ->map(fn ($url) => $this->extractVendorName($url)) + ->mapWithKeys(fn (OutdatedPackage $package) => [ + $package->name => [$package->currentVersion], + ]) + ->all(); + + return $this->getAudit($data) + ->collect() + ->flatMap(function (array $vulnerabilities, string $package) { + return collect($vulnerabilities) + ->map(fn (array $vulnerability) => new VulnerablePackage( + name: $package, + severity: $vulnerability['severity'], + vulnerableVersions: $vulnerability['vulnerable_versions'], + vulnerability: $vulnerability['title'], + )); + }) + ->sortBy([ + ['name', 'asc'], + ['severity', 'asc'], + ]) + ->values(); + } + private function packagesWithVersion(): Collection { return collect($this->importmap->asArray(fn ($url) => $url)["imports"]); @@ -91,4 +118,16 @@ private function outdated(string $currentVersion, string $latestVersion) { return version_compare($currentVersion, $latestVersion) === -1; } + + private function getAudit(array $packages) + { + $response = Http::asJson() + ->post($this->baseUrl . "/-/npm/v1/security/advisories/bulk", $packages); + + if (! $response->ok()) { + return collect(); + } + + return $response->collect(); + } } diff --git a/src/VulnerablePackage.php b/src/VulnerablePackage.php new file mode 100644 index 0000000..2894391 --- /dev/null +++ b/src/VulnerablePackage.php @@ -0,0 +1,14 @@ +assertEquals("2.2.2", $packages->first()->latestVersion); $this->assertNull($packages->first()->error); } + + /** @test */ + public function finds_no_audit_vulnerabilities() + { + $this->importmap->pin("is-svg", "https://cdn.skypack.dev/is-svg@3.0.0"); + + Http::fake(fn () => Http::response([ + "is-svg" => [ + [ + "title" => "Regular Expression Denial of Service (ReDoS)", + "severity" => "high", + "vulnerable_versions" => ">=2.1.0 <4.2.2", + ], + [ + "title" => "ReDOS in IS-SVG", + "severity" => "high", + "vulnerable_versions" => ">=2.1.0 <4.3.0", + ], + ], + ])); + + $this->assertCount(2, $vulnerabilities = $this->npm->vulnerablePackages()); + + $this->assertEquals("is-svg", $vulnerabilities->first()->name); + $this->assertEquals("Regular Expression Denial of Service (ReDoS)", $vulnerabilities->first()->vulnerability); + $this->assertEquals("high", $vulnerabilities->first()->severity); + $this->assertEquals(">=2.1.0 <4.2.2", $vulnerabilities->first()->vulnerableVersions); + + $this->assertEquals("is-svg", $vulnerabilities->last()->name); + $this->assertEquals("ReDOS in IS-SVG", $vulnerabilities->last()->vulnerability); + $this->assertEquals("high", $vulnerabilities->last()->severity); + $this->assertEquals(">=2.1.0 <4.3.0", $vulnerabilities->last()->vulnerableVersions); + } } From 0378f410a609d3b0b8bb3ff5db07ca8b89151b3b Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 30 May 2022 12:56:47 -0300 Subject: [PATCH 04/26] Convert test to pest --- tests/NpmTest.php | 213 ++++++++++++++++++++++------------------------ 1 file changed, 100 insertions(+), 113 deletions(-) diff --git a/tests/NpmTest.php b/tests/NpmTest.php index 92f9850..f352b52 100644 --- a/tests/NpmTest.php +++ b/tests/NpmTest.php @@ -3,109 +3,97 @@ namespace Tonysm\ImportmapLaravel; use Illuminate\Support\Facades\Http; -use Tonysm\ImportmapLaravel\Tests\TestCase; -class NpmTest extends TestCase -{ - private Npm $npm; - - protected function setUp(): void - { - parent::setUp(); - - $this->importmap = new Importmap(); - $this->npm = new Npm($this->importmap); - } - - /** @test */ - public function no_oudated_packages() - { - $this->importmap->pin("md5", "https://cdn.skypack.dev/md5@2.2.0"); - - Http::fake(fn () => Http::response([ - "dist-tags" => [ - "latest" => "2.2.0", - ], - ])); - - $this->assertCount(0, $this->npm->outdatedPackages()); - } - - /** @test */ - public function handles_error_when_fails_to_fetch_latest_version_of_package() - { - $this->importmap->pin("md5", "https://cdn.skypack.dev/md5@2.2.0"); - - Http::fake(fn () => Http::response([], 404)); - - $this->assertCount(1, $packages = $this->npm->outdatedPackages()); - $this->assertEquals("md5", $packages->first()->name); - $this->assertEquals("2.2.0", $packages->first()->currentVersion); - $this->assertNull($packages->first()->latestVersion); - $this->assertEquals("Response error", $packages->first()->error); - } - - /** @test */ - public function handles_error_when_returns_ok_but_response_json_contains_error() - { - $this->importmap->pin("md5", "https://cdn.skypack.dev/md5@2.2.0"); - - Http::fake(fn () => Http::response([ - "error" => "Something went wrong", - ], 200)); - - $this->assertCount(1, $packages = $this->npm->outdatedPackages()); - $this->assertEquals("md5", $packages->first()->name); - $this->assertEquals("2.2.0", $packages->first()->currentVersion); - $this->assertNull($packages->first()->latestVersion); - $this->assertEquals("Something went wrong", $packages->first()->error); - } - - /** @test */ - public function finds_outdated_package() - { - $this->importmap->pin("md5", "https://cdn.skypack.dev/md5@2.2.0"); - - Http::fake(fn () => Http::response([ - "dist-tags" => [ - "latest" => "2.2.1", - ], - ])); - - $this->assertCount(1, $packages = $this->npm->outdatedPackages()); - $this->assertEquals("md5", $packages->first()->name); - $this->assertEquals("2.2.0", $packages->first()->currentVersion); - $this->assertEquals("2.2.1", $packages->first()->latestVersion); - $this->assertNull($packages->first()->error); - } - - /** @test */ - public function finds_outdated_package_comparing_versions() - { - $this->importmap->pin("md5", "https://cdn.skypack.dev/md5@2.2.0"); - - Http::fake(fn () => Http::response([ - "versions" => [ - "2.0.0" => [], - "2.2.2" => [], - "1.2.0" => [], - "1.7.0" => [], - ], - ])); - - $this->assertCount(1, $packages = $this->npm->outdatedPackages()); - $this->assertEquals("md5", $packages->first()->name); - $this->assertEquals("2.2.0", $packages->first()->currentVersion); - $this->assertEquals("2.2.2", $packages->first()->latestVersion); - $this->assertNull($packages->first()->error); - } - - /** @test */ - public function finds_no_audit_vulnerabilities() - { - $this->importmap->pin("is-svg", "https://cdn.skypack.dev/is-svg@3.0.0"); - - Http::fake(fn () => Http::response([ +beforeEach(function () { + $this->importmap = new Importmap(); + $this->npm = new Npm($this->importmap); +}); + +it("finds no outdated packages", function () { + $this->importmap->pin("md5", "https://cdn.skypack.dev/md5@2.2.0"); + + Http::fake(fn () => Http::response([ + "dist-tags" => [ + "latest" => "2.2.0", + ], + ])); + + expect($this->npm->outdatedPackages()->count())->toEqual(0); +}); + +it("handles error when fails to fetch latest version of package", function () { + $this->importmap->pin("md5", "https://cdn.skypack.dev/md5@2.2.0"); + + Http::fake(fn () => Http::response([], 404)); + + expect($packages = $this->npm->outdatedPackages())->toHaveCount(1); + expect($packages->first()->name)->toEqual("md5"); + expect($packages->first()->currentVersion)->toEqual("2.2.0"); + expect($packages->first()->latestVersion)->toBeNull(); + expect($packages->first()->error)->toEqual("Response error"); +}); + +it("handles error when returns ok but response json contains error", function () { + $this->importmap->pin("md5", "https://cdn.skypack.dev/md5@2.2.0"); + + Http::fake(fn () => Http::response([ + "error" => "Something went wrong", + ], 200)); + + expect($packages = $this->npm->outdatedPackages())->toHaveCount(1); + expect($packages->first()->name)->toEqual("md5"); + expect($packages->first()->currentVersion)->toEqual("2.2.0"); + expect($packages->first()->latestVersion)->toBeNull(); + expect($packages->first()->error)->toEqual("Something went wrong"); +}); + +it("finds outdated packages", function () { + $this->importmap->pin("md5", "https://cdn.skypack.dev/md5@2.2.0"); + + Http::fake(fn () => Http::response([ + "dist-tags" => [ + "latest" => "2.2.1", + ], + ])); + + expect($packages = $this->npm->outdatedPackages())->toHaveCount(1); + expect($packages->first()->name)->toEqual("md5"); + expect($packages->first()->currentVersion)->toEqual("2.2.0"); + expect($packages->first()->latestVersion)->toEqual("2.2.1"); + expect($packages->first()->error)->toBeNull(); +}); + +it("finds outdated packages comparing versions", function () { + $this->importmap->pin("md5", "https://cdn.skypack.dev/md5@2.2.0"); + + Http::fake(fn () => Http::response([ + "versions" => [ + "2.0.0" => [], + "2.2.2" => [], + "1.2.0" => [], + "1.7.0" => [], + ], + ])); + + expect($packages = $this->npm->outdatedPackages())->toHaveCount(1); + expect($packages->first()->name)->toEqual("md5"); + expect($packages->first()->currentVersion)->toEqual("2.2.0"); + expect($packages->first()->latestVersion)->toEqual("2.2.2"); + expect($packages->first()->error)->toBeNull(); +}); + +it("finds no audit vulnerabilities", function () { + $this->importmap->pin("is-svg", "https://cdn.skypack.dev/is-svg@3.0.0"); + + Http::fake(fn () => Http::response([])); + + expect($this->npm->vulnerablePackages())->toHaveCount(0); +}); + +it("finds audit vulnerabilities", function () { + $this->importmap->pin("is-svg", "https://cdn.skypack.dev/is-svg@3.0.0"); + + Http::fake(fn () => Http::response([ "is-svg" => [ [ "title" => "Regular Expression Denial of Service (ReDoS)", @@ -120,16 +108,15 @@ public function finds_no_audit_vulnerabilities() ], ])); - $this->assertCount(2, $vulnerabilities = $this->npm->vulnerablePackages()); + expect($vulnerabilities = $this->npm->vulnerablePackages())->toHaveCount(2); - $this->assertEquals("is-svg", $vulnerabilities->first()->name); - $this->assertEquals("Regular Expression Denial of Service (ReDoS)", $vulnerabilities->first()->vulnerability); - $this->assertEquals("high", $vulnerabilities->first()->severity); - $this->assertEquals(">=2.1.0 <4.2.2", $vulnerabilities->first()->vulnerableVersions); + expect($vulnerabilities->first()->name)->toEqual("is-svg"); + expect($vulnerabilities->first()->vulnerability)->toEqual("Regular Expression Denial of Service (ReDoS)"); + expect($vulnerabilities->first()->severity)->toEqual("high"); + expect($vulnerabilities->first()->vulnerableVersions)->toEqual(">=2.1.0 <4.2.2"); - $this->assertEquals("is-svg", $vulnerabilities->last()->name); - $this->assertEquals("ReDOS in IS-SVG", $vulnerabilities->last()->vulnerability); - $this->assertEquals("high", $vulnerabilities->last()->severity); - $this->assertEquals(">=2.1.0 <4.3.0", $vulnerabilities->last()->vulnerableVersions); - } -} + expect($vulnerabilities->last()->name)->toEqual("is-svg"); + expect($vulnerabilities->last()->vulnerability)->toEqual("ReDOS in IS-SVG"); + expect($vulnerabilities->last()->severity)->toEqual("high"); + expect($vulnerabilities->last()->vulnerableVersions)->toEqual(">=2.1.0 <4.3.0"); +}); From cf0aaa869f3980f25587cf6edfcac84607172b63 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 30 May 2022 13:00:06 -0300 Subject: [PATCH 05/26] Fix typo --- src/Commands/AuditCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/AuditCommand.php b/src/Commands/AuditCommand.php index 78bd67e..44f928f 100644 --- a/src/Commands/AuditCommand.php +++ b/src/Commands/AuditCommand.php @@ -24,7 +24,7 @@ public function handle(Npm $npm): int } $this->table( - ['Package', 'Severity', 'Vundlerable Versions', 'Vulnerability'], + ['Package', 'Severity', 'Vulnerable Versions', 'Vulnerability'], $vulnerablePackages ->map(fn (VulnerablePackage $package) => [$package->name, $package->severity, $package->vulnerableVersions, $package->vulnerability]) ->all() From f8872f24617fdb677856f927faa3bdbf8d74b7a7 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 30 May 2022 13:00:49 -0300 Subject: [PATCH 06/26] Inline pluralized label --- src/Commands/OutdatedCommand.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Commands/OutdatedCommand.php b/src/Commands/OutdatedCommand.php index 03ddbb1..e87c3e2 100644 --- a/src/Commands/OutdatedCommand.php +++ b/src/Commands/OutdatedCommand.php @@ -30,10 +30,13 @@ public function handle(Npm $npm): int ->all(), ); - $packageLabel = Str::plural('package', $outdatedPackages->count()); - $this->newLine(); - $this->error(sprintf('%d outdated %s found.', $outdatedPackages->count(), $packageLabel)); + + $this->error(sprintf( + '%d outdated %s found.', + $outdatedPackages->count(), + Str::plural('package', $outdatedPackages->count()), + )); return Command::FAILURE; } From 79d2387e51e050c16cac5236e050912c767ff641 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 30 May 2022 13:04:52 -0300 Subject: [PATCH 07/26] Fix extract version should return a simple package version object --- src/Npm.php | 25 +++++++++++++++---------- src/PackageVersion.php | 12 ++++++++++++ 2 files changed, 27 insertions(+), 10 deletions(-) create mode 100644 src/PackageVersion.php diff --git a/src/Npm.php b/src/Npm.php index faf695a..613cc8f 100644 --- a/src/Npm.php +++ b/src/Npm.php @@ -17,26 +17,31 @@ public function outdatedPackages(): Collection { return $this->packagesWithVersion()->reduce(function (Collection $outdatedPackages, string $url) { $package = $this->extractVendorName($url); + $latestVersion = null; + $error = null; if (! $package) { return $outdatedPackages; } if (! ($response = $this->getPackage($package))) { - $package->error = "Response error"; + $error = "Response error"; } elseif ($response["error"] ?? false) { - $package->error = $response["error"]; + $error = $response["error"]; } else { $latestVersion = $this->findLatestVersion($response); - if (! $this->outdated($package->currentVersion, $latestVersion)) { + if (! $this->outdated($package->version, $latestVersion)) { return $outdatedPackages; } - - $package->latestVersion = $latestVersion; } - return $outdatedPackages->add($package); + return $outdatedPackages->add(new OutdatedPackage( + name: $package->name, + currentVersion: $package->version, + latestVersion: $latestVersion, + error: $error, + )); }, collect()); } @@ -44,8 +49,8 @@ public function vulnerablePackages(): Collection { $data = $this->packagesWithVersion() ->map(fn ($url) => $this->extractVendorName($url)) - ->mapWithKeys(fn (OutdatedPackage $package) => [ - $package->name => [$package->currentVersion], + ->mapWithKeys(fn (PackageVersion $package) => [ + $package->name => [$package->version], ]) ->all(); @@ -81,10 +86,10 @@ private function extractVendorName(string $url) return null; } - return new OutdatedPackage(name: $matches[1], currentVersion: $matches[2]); + return new PackageVersion(name: $matches[1], version: $matches[2]); } - private function getPackage(OutdatedPackage $package) + private function getPackage(PackageVersion $package) { $response = Http::get($this->baseUrl . "/" . $package->name); diff --git a/src/PackageVersion.php b/src/PackageVersion.php new file mode 100644 index 0000000..030a4dc --- /dev/null +++ b/src/PackageVersion.php @@ -0,0 +1,12 @@ + Date: Mon, 30 May 2022 14:32:44 -0300 Subject: [PATCH 08/26] Split outdated and audit test cases and scan the importmap route file with regex instead of using json Some vendor dependencies are downloaded, so in order to make sure the commands work with those, we need to scan the importmap.php file instead of relying on the Importmap::json() helper. --- src/Npm.php | 41 ++++++-- tests/NpmAuditTest.php | 54 ++++++++++ tests/NpmOutdatedTest.php | 66 ++++++++++++ tests/NpmTest.php | 122 ---------------------- tests/fixtures/npm/audit-importmap.php | 6 ++ tests/fixtures/npm/outdated-importmap.php | 6 ++ 6 files changed, 163 insertions(+), 132 deletions(-) create mode 100644 tests/NpmAuditTest.php create mode 100644 tests/NpmOutdatedTest.php delete mode 100644 tests/NpmTest.php create mode 100644 tests/fixtures/npm/audit-importmap.php create mode 100644 tests/fixtures/npm/outdated-importmap.php diff --git a/src/Npm.php b/src/Npm.php index 613cc8f..a0ea6c5 100644 --- a/src/Npm.php +++ b/src/Npm.php @@ -3,20 +3,21 @@ namespace Tonysm\ImportmapLaravel; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Http; class Npm { private string $baseUrl = "https://registry.npmjs.org"; - public function __construct(private Importmap $importmap) + public function __construct(private string $configPath) { } public function outdatedPackages(): Collection { - return $this->packagesWithVersion()->reduce(function (Collection $outdatedPackages, string $url) { - $package = $this->extractVendorName($url); + return $this->packagesWithVersion() + ->reduce(function (Collection $outdatedPackages, PackageVersion $package) { $latestVersion = null; $error = null; @@ -48,7 +49,6 @@ public function outdatedPackages(): Collection public function vulnerablePackages(): Collection { $data = $this->packagesWithVersion() - ->map(fn ($url) => $this->extractVendorName($url)) ->mapWithKeys(fn (PackageVersion $package) => [ $package->name => [$package->version], ]) @@ -74,19 +74,40 @@ public function vulnerablePackages(): Collection private function packagesWithVersion(): Collection { - return collect($this->importmap->asArray(fn ($url) => $url)["imports"]); + $content = File::get($this->configPath); + + $matches = $this->findPackagesFromCdnMatches($content) + ->merge($this->findPackagesFromLocalMatches($content)); + + return $matches; } - private function extractVendorName(string $url) + private function findPackagesFromCdnMatches(string $content) { - $matches = null; - preg_match('/^.*(?<=npm:|npm\/|skypack\.dev\/|unpkg\.com\/)(.*)(?=@\d+\.\d+\.\d+)@(\d+\.\d+\.\d+(?:[^\/\s"]*)).*$/', $url, $matches); + preg_match_all('/^Importmap\:\:pin\(.*(?<=npm:|npm\/|skypack\.dev\/|unpkg\.com\/)(.*)(?=@\d+\.\d+\.\d+)@(\d+\.\d+\.\d+(?:[^\/\s"]*)).*\)\;$/m', $content, $matches); if (count($matches) !== 3) { - return null; + return collect(); + } + + return collect($matches[1]) + ->zip($matches[2]) + ->map(fn ($items) => new PackageVersion(name: $items[0], version: $items[1])) + ->values(); + } + + private function findPackagesFromLocalMatches(string $content) + { + preg_match_all('/^Importmap::pin\("([^"]*)".*\)\; \/\/.*@(\d+\.\d+\.\d+(?:[^\s]*)).*$/m', $content, $matches); + + if (count($matches) !== 3) { + return collect(); } - return new PackageVersion(name: $matches[1], version: $matches[2]); + return collect($matches[1]) + ->zip($matches[2]) + ->map(fn ($items) => new PackageVersion(name: $items[0], version: $items[1])) + ->values(); } private function getPackage(PackageVersion $package) diff --git a/tests/NpmAuditTest.php b/tests/NpmAuditTest.php new file mode 100644 index 0000000..f9e6c16 --- /dev/null +++ b/tests/NpmAuditTest.php @@ -0,0 +1,54 @@ +npm = new Npm(configPath: __DIR__ . '/fixtures/npm/audit-importmap.php'); +}); + +it("finds no audit vulnerabilities", function () { + Http::fake(fn () => Http::response([])); + + expect($this->npm->vulnerablePackages())->toHaveCount(0); + + Http::assertSent(fn ($request) => ( + $request['is-svg'] === ["3.0.0"] && + $request['lodash'] === ["4.17.12"] + )); +}); + +it("finds audit vulnerabilities", function () { + Http::fake(fn () => Http::response([ + "is-svg" => [ + [ + "title" => "Regular Expression Denial of Service (ReDoS)", + "severity" => "high", + "vulnerable_versions" => ">=2.1.0 <4.2.2", + ], + [ + "title" => "ReDOS in IS-SVG", + "severity" => "high", + "vulnerable_versions" => ">=2.1.0 <4.3.0", + ], + ], + ])); + + expect($vulnerabilities = $this->npm->vulnerablePackages())->toHaveCount(2); + + expect($vulnerabilities->first()->name)->toEqual("is-svg"); + expect($vulnerabilities->first()->vulnerability)->toEqual("Regular Expression Denial of Service (ReDoS)"); + expect($vulnerabilities->first()->severity)->toEqual("high"); + expect($vulnerabilities->first()->vulnerableVersions)->toEqual(">=2.1.0 <4.2.2"); + + expect($vulnerabilities->last()->name)->toEqual("is-svg"); + expect($vulnerabilities->last()->vulnerability)->toEqual("ReDOS in IS-SVG"); + expect($vulnerabilities->last()->severity)->toEqual("high"); + expect($vulnerabilities->last()->vulnerableVersions)->toEqual(">=2.1.0 <4.3.0"); + + Http::assertSent(fn ($request) => ( + $request['is-svg'] === ["3.0.0"] && + $request['lodash'] === ["4.17.12"] + )); +}); diff --git a/tests/NpmOutdatedTest.php b/tests/NpmOutdatedTest.php new file mode 100644 index 0000000..a116fe1 --- /dev/null +++ b/tests/NpmOutdatedTest.php @@ -0,0 +1,66 @@ +npm = new Npm(configPath: __DIR__ . "/fixtures/npm/outdated-importmap.php"); +}); + +it("finds no outdated packages", function () { + Http::fakeSequence() + ->push(["dist-tags" => ["latest" => "3.0.0"]]) + ->push(["dist-tags" => ["latest" => "4.0.0"]]); + + expect($this->npm->outdatedPackages()->count())->toEqual(0); +}); + +it("handles error when fails to fetch latest version of package", function () { + Http::fakeSequence() + ->push([], 404) + ->push(["dist-tags" => ["latest" => "4.0.0"]]); + + expect($packages = $this->npm->outdatedPackages())->toHaveCount(1); + expect($packages->first()->name)->toEqual("is-svg"); + expect($packages->first()->currentVersion)->toEqual("3.0.0"); + expect($packages->first()->latestVersion)->toBeNull(); + expect($packages->first()->error)->toEqual("Response error"); +}); + +it("handles error when returns ok but response json contains error", function () { + Http::fakeSequence() + ->push(["error" => "Something went wrong"]) + ->push(["dist-tags" => ["latest" => "4.0.0"]]); + + expect($packages = $this->npm->outdatedPackages())->toHaveCount(1); + expect($packages->first()->name)->toEqual("is-svg"); + expect($packages->first()->currentVersion)->toEqual("3.0.0"); + expect($packages->first()->latestVersion)->toBeNull(); + expect($packages->first()->error)->toEqual("Something went wrong"); +}); + +it("finds outdated packages", function () { + Http::fakeSequence() + ->push(["dist-tags" => ["latest" => "4.0.0"]]) + ->push([ + "versions" => [ + "2.0.0" => [], + "5.0.0" => [], + "1.2.0" => [], + "1.7.0" => [], + ], + ]); + + expect($packages = $this->npm->outdatedPackages())->toHaveCount(2); + + expect($packages->first()->name)->toEqual("is-svg"); + expect($packages->first()->currentVersion)->toEqual("3.0.0"); + expect($packages->first()->latestVersion)->toEqual("4.0.0"); + expect($packages->first()->error)->toBeNull(); + + expect($packages->last()->name)->toEqual("lodash"); + expect($packages->last()->currentVersion)->toEqual("4.0.0"); + expect($packages->last()->latestVersion)->toEqual("5.0.0"); + expect($packages->last()->error)->toBeNull(); +}); diff --git a/tests/NpmTest.php b/tests/NpmTest.php deleted file mode 100644 index f352b52..0000000 --- a/tests/NpmTest.php +++ /dev/null @@ -1,122 +0,0 @@ -importmap = new Importmap(); - $this->npm = new Npm($this->importmap); -}); - -it("finds no outdated packages", function () { - $this->importmap->pin("md5", "https://cdn.skypack.dev/md5@2.2.0"); - - Http::fake(fn () => Http::response([ - "dist-tags" => [ - "latest" => "2.2.0", - ], - ])); - - expect($this->npm->outdatedPackages()->count())->toEqual(0); -}); - -it("handles error when fails to fetch latest version of package", function () { - $this->importmap->pin("md5", "https://cdn.skypack.dev/md5@2.2.0"); - - Http::fake(fn () => Http::response([], 404)); - - expect($packages = $this->npm->outdatedPackages())->toHaveCount(1); - expect($packages->first()->name)->toEqual("md5"); - expect($packages->first()->currentVersion)->toEqual("2.2.0"); - expect($packages->first()->latestVersion)->toBeNull(); - expect($packages->first()->error)->toEqual("Response error"); -}); - -it("handles error when returns ok but response json contains error", function () { - $this->importmap->pin("md5", "https://cdn.skypack.dev/md5@2.2.0"); - - Http::fake(fn () => Http::response([ - "error" => "Something went wrong", - ], 200)); - - expect($packages = $this->npm->outdatedPackages())->toHaveCount(1); - expect($packages->first()->name)->toEqual("md5"); - expect($packages->first()->currentVersion)->toEqual("2.2.0"); - expect($packages->first()->latestVersion)->toBeNull(); - expect($packages->first()->error)->toEqual("Something went wrong"); -}); - -it("finds outdated packages", function () { - $this->importmap->pin("md5", "https://cdn.skypack.dev/md5@2.2.0"); - - Http::fake(fn () => Http::response([ - "dist-tags" => [ - "latest" => "2.2.1", - ], - ])); - - expect($packages = $this->npm->outdatedPackages())->toHaveCount(1); - expect($packages->first()->name)->toEqual("md5"); - expect($packages->first()->currentVersion)->toEqual("2.2.0"); - expect($packages->first()->latestVersion)->toEqual("2.2.1"); - expect($packages->first()->error)->toBeNull(); -}); - -it("finds outdated packages comparing versions", function () { - $this->importmap->pin("md5", "https://cdn.skypack.dev/md5@2.2.0"); - - Http::fake(fn () => Http::response([ - "versions" => [ - "2.0.0" => [], - "2.2.2" => [], - "1.2.0" => [], - "1.7.0" => [], - ], - ])); - - expect($packages = $this->npm->outdatedPackages())->toHaveCount(1); - expect($packages->first()->name)->toEqual("md5"); - expect($packages->first()->currentVersion)->toEqual("2.2.0"); - expect($packages->first()->latestVersion)->toEqual("2.2.2"); - expect($packages->first()->error)->toBeNull(); -}); - -it("finds no audit vulnerabilities", function () { - $this->importmap->pin("is-svg", "https://cdn.skypack.dev/is-svg@3.0.0"); - - Http::fake(fn () => Http::response([])); - - expect($this->npm->vulnerablePackages())->toHaveCount(0); -}); - -it("finds audit vulnerabilities", function () { - $this->importmap->pin("is-svg", "https://cdn.skypack.dev/is-svg@3.0.0"); - - Http::fake(fn () => Http::response([ - "is-svg" => [ - [ - "title" => "Regular Expression Denial of Service (ReDoS)", - "severity" => "high", - "vulnerable_versions" => ">=2.1.0 <4.2.2", - ], - [ - "title" => "ReDOS in IS-SVG", - "severity" => "high", - "vulnerable_versions" => ">=2.1.0 <4.3.0", - ], - ], - ])); - - expect($vulnerabilities = $this->npm->vulnerablePackages())->toHaveCount(2); - - expect($vulnerabilities->first()->name)->toEqual("is-svg"); - expect($vulnerabilities->first()->vulnerability)->toEqual("Regular Expression Denial of Service (ReDoS)"); - expect($vulnerabilities->first()->severity)->toEqual("high"); - expect($vulnerabilities->first()->vulnerableVersions)->toEqual(">=2.1.0 <4.2.2"); - - expect($vulnerabilities->last()->name)->toEqual("is-svg"); - expect($vulnerabilities->last()->vulnerability)->toEqual("ReDOS in IS-SVG"); - expect($vulnerabilities->last()->severity)->toEqual("high"); - expect($vulnerabilities->last()->vulnerableVersions)->toEqual(">=2.1.0 <4.3.0"); -}); diff --git a/tests/fixtures/npm/audit-importmap.php b/tests/fixtures/npm/audit-importmap.php new file mode 100644 index 0000000..8748684 --- /dev/null +++ b/tests/fixtures/npm/audit-importmap.php @@ -0,0 +1,6 @@ + Date: Mon, 30 May 2022 14:36:46 -0300 Subject: [PATCH 09/26] Ensure defaults to base_path() to find the importmap.php --- src/Npm.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Npm.php b/src/Npm.php index a0ea6c5..3bd567b 100644 --- a/src/Npm.php +++ b/src/Npm.php @@ -10,8 +10,9 @@ class Npm { private string $baseUrl = "https://registry.npmjs.org"; - public function __construct(private string $configPath) + public function __construct(private ?string $configPath = null) { + $this->configPath ??= base_path("routes/importmap.php"); } public function outdatedPackages(): Collection From c9c5b618fd19d506290f78261e1bac94c9633183 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 30 May 2022 14:37:57 -0300 Subject: [PATCH 10/26] Remove code path that can never happen --- src/Npm.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Npm.php b/src/Npm.php index 3bd567b..0d94261 100644 --- a/src/Npm.php +++ b/src/Npm.php @@ -22,10 +22,6 @@ public function outdatedPackages(): Collection $latestVersion = null; $error = null; - if (! $package) { - return $outdatedPackages; - } - if (! ($response = $this->getPackage($package))) { $error = "Response error"; } elseif ($response["error"] ?? false) { From ea20b407ba68bf879941e0161da6063fc1a07276 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 30 May 2022 14:41:44 -0300 Subject: [PATCH 11/26] Fix coverage composer script --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index fd7255f..5dbd90a 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,7 @@ "scripts": { "analyse": "vendor/bin/phpstan analyse", "test": "vendor/bin/pest", - "test-coverage": "vendor/bin/pest coverage" + "test-coverage": "vendor/bin/pest --coverage" }, "config": { "sort-packages": true, From 8d47b381dec188e6ac49b8f4c81480c036045f79 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 30 May 2022 15:00:04 -0300 Subject: [PATCH 12/26] Bump es-module-shims version to 1.5.5 --- resources/views/tags.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/tags.blade.php b/resources/views/tags.blade.php index 4dce2f1..b662d41 100644 --- a/resources/views/tags.blade.php +++ b/resources/views/tags.blade.php @@ -8,7 +8,7 @@ @if (config('importmap.use_shim')) @if ($nonce) @endif - + @endif From a95158129ee6d96dd064e2d083884940a278c0a9 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 30 May 2022 15:23:57 -0300 Subject: [PATCH 13/26] Use self:: instead of Command:: --- src/Commands/OutdatedCommand.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Commands/OutdatedCommand.php b/src/Commands/OutdatedCommand.php index e87c3e2..5e0dc3d 100644 --- a/src/Commands/OutdatedCommand.php +++ b/src/Commands/OutdatedCommand.php @@ -20,7 +20,7 @@ public function handle(Npm $npm): int if ($outdatedPackages->isEmpty()) { $this->info("No outdated packages found."); - return Command::SUCCESS; + return self::SUCCESS; } $this->table( @@ -38,6 +38,6 @@ public function handle(Npm $npm): int Str::plural('package', $outdatedPackages->count()), )); - return Command::FAILURE; + return self::FAILURE; } } From d124dd7387e1b1c03ea6956edc4c4f9bfa1f2f16 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 30 May 2022 15:25:55 -0300 Subject: [PATCH 14/26] Fix indent --- src/Npm.php | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/Npm.php b/src/Npm.php index 0d94261..163dc1c 100644 --- a/src/Npm.php +++ b/src/Npm.php @@ -18,29 +18,29 @@ public function __construct(private ?string $configPath = null) public function outdatedPackages(): Collection { return $this->packagesWithVersion() - ->reduce(function (Collection $outdatedPackages, PackageVersion $package) { - $latestVersion = null; - $error = null; - - if (! ($response = $this->getPackage($package))) { - $error = "Response error"; - } elseif ($response["error"] ?? false) { - $error = $response["error"]; - } else { - $latestVersion = $this->findLatestVersion($response); - - if (! $this->outdated($package->version, $latestVersion)) { - return $outdatedPackages; + ->reduce(function (Collection $outdatedPackages, PackageVersion $package) { + $latestVersion = null; + $error = null; + + if (! ($response = $this->getPackage($package))) { + $error = "Response error"; + } elseif ($response["error"] ?? false) { + $error = $response["error"]; + } else { + $latestVersion = $this->findLatestVersion($response); + + if (! $this->outdated($package->version, $latestVersion)) { + return $outdatedPackages; + } } - } - - return $outdatedPackages->add(new OutdatedPackage( - name: $package->name, - currentVersion: $package->version, - latestVersion: $latestVersion, - error: $error, - )); - }, collect()); + + return $outdatedPackages->add(new OutdatedPackage( + name: $package->name, + currentVersion: $package->version, + latestVersion: $latestVersion, + error: $error, + )); + }, collect()); } public function vulnerablePackages(): Collection From 9ba8b4da4c1497705b43e56e53c4baa94085419a Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 30 May 2022 15:43:19 -0300 Subject: [PATCH 15/26] Fix tests for tags component --- tests/TagsComponentTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/TagsComponentTest.php b/tests/TagsComponentTest.php index bf039fa..bf998b9 100644 --- a/tests/TagsComponentTest.php +++ b/tests/TagsComponentTest.php @@ -21,12 +21,12 @@ $this->blade('') ->assertSee('', escape: false) ->assertDontSee('', escape: false); + ->assertSee('', escape: false); }); it('uses given CSP nonce', function () { $this->blade('') ->assertSee('', escape: false) ->assertSee('', escape: false) - ->assertSee('', escape: false); + ->assertSee('', escape: false); }); From 5255ae45b9279322d3247bfcbb63b1ce2ad5e53d Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 30 May 2022 16:12:06 -0300 Subject: [PATCH 16/26] Drop old Laravel/PHP versions --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 5dbd90a..de3881b 100644 --- a/composer.json +++ b/composer.json @@ -17,15 +17,15 @@ ], "require": { "php": "^8.0|^8.1", - "illuminate/contracts": "^8.73|^9.0", - "illuminate/support": "^8.47|^9.0", + "illuminate/contracts": "^9.0", + "illuminate/support": "^9.0", "spatie/laravel-package-tools": "^1.9.2" }, "require-dev": { "guzzlehttp/guzzle": "^7.4", "nunomaduro/collision": "^5.10|^6.0", "nunomaduro/larastan": "^1.0", - "orchestra/testbench": "^6.24", + "orchestra/testbench": "^7.0", "pestphp/pest": "^1.21", "pestphp/pest-plugin-laravel": "^1.2", "phpstan/extension-installer": "^1.1", From 23e7ed8f76f92cfba14b1b1b226d9c1c7daa9a6e Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 30 May 2022 16:12:32 -0300 Subject: [PATCH 17/26] Tweaks the outdated tests so it doesnt depend on the order of packages --- src/Npm.php | 8 +++---- tests/NpmOutdatedTest.php | 45 +++++++++++++++++++++++---------------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/src/Npm.php b/src/Npm.php index 163dc1c..b6e3149 100644 --- a/src/Npm.php +++ b/src/Npm.php @@ -73,10 +73,10 @@ private function packagesWithVersion(): Collection { $content = File::get($this->configPath); - $matches = $this->findPackagesFromCdnMatches($content) - ->merge($this->findPackagesFromLocalMatches($content)); - - return $matches; + return $this->findPackagesFromCdnMatches($content) + ->merge($this->findPackagesFromLocalMatches($content)) + ->unique('name') + ->values(); } private function findPackagesFromCdnMatches(string $content) diff --git a/tests/NpmOutdatedTest.php b/tests/NpmOutdatedTest.php index a116fe1..3525886 100644 --- a/tests/NpmOutdatedTest.php +++ b/tests/NpmOutdatedTest.php @@ -6,6 +6,8 @@ beforeEach(function () { $this->npm = new Npm(configPath: __DIR__ . "/fixtures/npm/outdated-importmap.php"); + + Http::preventStrayRequests(); }); it("finds no outdated packages", function () { @@ -17,9 +19,10 @@ }); it("handles error when fails to fetch latest version of package", function () { - Http::fakeSequence() - ->push([], 404) - ->push(["dist-tags" => ["latest" => "4.0.0"]]); + Http::fake([ + 'https://registry.npmjs.org/is-svg' => Http::response([], 404), + 'https://registry.npmjs.org/lodash' => Http::response(["dist-tags" => ["latest" => "4.0.0"]]), + ]); expect($packages = $this->npm->outdatedPackages())->toHaveCount(1); expect($packages->first()->name)->toEqual("is-svg"); @@ -29,9 +32,10 @@ }); it("handles error when returns ok but response json contains error", function () { - Http::fakeSequence() - ->push(["error" => "Something went wrong"]) - ->push(["dist-tags" => ["latest" => "4.0.0"]]); + Http::fake([ + 'https://registry.npmjs.org/is-svg' => Http::response(["error" => "Something went wrong"]), + 'https://registry.npmjs.org/lodash' => Http::response(["dist-tags" => ["latest" => "4.0.0"]]), + ]); expect($packages = $this->npm->outdatedPackages())->toHaveCount(1); expect($packages->first()->name)->toEqual("is-svg"); @@ -41,26 +45,31 @@ }); it("finds outdated packages", function () { - Http::fakeSequence() - ->push(["dist-tags" => ["latest" => "4.0.0"]]) - ->push([ + Http::fake([ + 'https://registry.npmjs.org/is-svg' => Http::response(["dist-tags" => ["latest" => "4.0.0"]]), + 'https://registry.npmjs.org/lodash' => Http::response([ "versions" => [ "2.0.0" => [], "5.0.0" => [], "1.2.0" => [], "1.7.0" => [], ], - ]); + ]), + ]); expect($packages = $this->npm->outdatedPackages())->toHaveCount(2); - expect($packages->first()->name)->toEqual("is-svg"); - expect($packages->first()->currentVersion)->toEqual("3.0.0"); - expect($packages->first()->latestVersion)->toEqual("4.0.0"); - expect($packages->first()->error)->toBeNull(); + $svgPackage = $packages->firstWhere('name', 'is-svg'); + + expect($svgPackage->name)->toEqual("is-svg"); + expect($svgPackage->currentVersion)->toEqual("3.0.0"); + expect($svgPackage->latestVersion)->toEqual("4.0.0"); + expect($svgPackage->error)->toBeNull(); + + $lodashPackage = $packages->firstWhere('name', 'lodash'); - expect($packages->last()->name)->toEqual("lodash"); - expect($packages->last()->currentVersion)->toEqual("4.0.0"); - expect($packages->last()->latestVersion)->toEqual("5.0.0"); - expect($packages->last()->error)->toBeNull(); + expect($lodashPackage->name)->toEqual("lodash"); + expect($lodashPackage->currentVersion)->toEqual("4.0.0"); + expect($lodashPackage->latestVersion)->toEqual("5.0.0"); + expect($lodashPackage->error)->toBeNull(); }); From 0ce2996eb60f759d1b932f94c1d78ea5e4e59648 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 30 May 2022 16:14:22 -0300 Subject: [PATCH 18/26] Add Http::preventStrayRequests --- tests/NpmAuditTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/NpmAuditTest.php b/tests/NpmAuditTest.php index f9e6c16..5ffd314 100644 --- a/tests/NpmAuditTest.php +++ b/tests/NpmAuditTest.php @@ -6,6 +6,8 @@ beforeEach(function () { $this->npm = new Npm(configPath: __DIR__ . '/fixtures/npm/audit-importmap.php'); + + Http::preventStrayRequests(); }); it("finds no audit vulnerabilities", function () { From a83bbd5b951c9403d4e32870faa225fc648c1c62 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 30 May 2022 16:17:19 -0300 Subject: [PATCH 19/26] Tweaks workflows --- .github/workflows/run-tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index a12cfc5..645d62f 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -13,12 +13,12 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest, macos-latest, windows-latest] - php: [8.0, 8.1] - laravel: [8.*] + php: [8.1] + laravel: [9.*] stability: [prefer-lowest, prefer-stable] include: - - laravel: 8.* - testbench: ^6.24 + - laravel: 9.* + testbench: ^7.0 name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} From e7b1fcd57b47d9e07546e7c4fe4a23b5e0ff75d1 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 30 May 2022 16:21:33 -0300 Subject: [PATCH 20/26] Tweak workflow to require min Laravel 9.12 (for tests reasons) --- .github/workflows/run-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 645d62f..a14c20f 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -14,10 +14,10 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] php: [8.1] - laravel: [9.*] + laravel: [9.12.*] stability: [prefer-lowest, prefer-stable] include: - - laravel: 9.* + - laravel: 9.12.* testbench: ^7.0 name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} From c62da67e3f305e1f3d164ab32914b3d038f63b2d Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 30 May 2022 16:31:35 -0300 Subject: [PATCH 21/26] Bump PHP version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index de3881b..9e7c72f 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ } ], "require": { - "php": "^8.0|^8.1", + "php": "^8.1", "illuminate/contracts": "^9.0", "illuminate/support": "^9.0", "spatie/laravel-package-tools": "^1.9.2" From 34ef5c854ab7dac5f518f5ca0b5f7143a177435c Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 30 May 2022 16:32:59 -0300 Subject: [PATCH 22/26] Tweak Http Client payload assertion --- tests/NpmAuditTest.php | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/NpmAuditTest.php b/tests/NpmAuditTest.php index 5ffd314..6af129f 100644 --- a/tests/NpmAuditTest.php +++ b/tests/NpmAuditTest.php @@ -2,6 +2,7 @@ namespace Tonysm\ImportmapLaravel; +use Illuminate\Http\Client\Request; use Illuminate\Support\Facades\Http; beforeEach(function () { @@ -15,9 +16,11 @@ expect($this->npm->vulnerablePackages())->toHaveCount(0); - Http::assertSent(fn ($request) => ( - $request['is-svg'] === ["3.0.0"] && - $request['lodash'] === ["4.17.12"] + Http::assertSent(fn (Request $request) => ( + $request->data() == [ + "is-svg" => ["3.0.0"], + "lodash" => ["4.17.12"], + ] )); }); @@ -49,8 +52,10 @@ expect($vulnerabilities->last()->severity)->toEqual("high"); expect($vulnerabilities->last()->vulnerableVersions)->toEqual(">=2.1.0 <4.3.0"); - Http::assertSent(fn ($request) => ( - $request['is-svg'] === ["3.0.0"] && - $request['lodash'] === ["4.17.12"] + Http::assertSent(fn (Request $request) => ( + $request->data() == [ + "is-svg" => ["3.0.0"], + "lodash" => ["4.17.12"], + ] )); }); From e20a973a0b44d4976fd11b335841bea7e7aa84f5 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 30 May 2022 16:40:29 -0300 Subject: [PATCH 23/26] Use DS instead of forward slash --- tests/NpmAuditTest.php | 2 +- tests/NpmOutdatedTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/NpmAuditTest.php b/tests/NpmAuditTest.php index 6af129f..7200535 100644 --- a/tests/NpmAuditTest.php +++ b/tests/NpmAuditTest.php @@ -6,7 +6,7 @@ use Illuminate\Support\Facades\Http; beforeEach(function () { - $this->npm = new Npm(configPath: __DIR__ . '/fixtures/npm/audit-importmap.php'); + $this->npm = new Npm(configPath: __DIR__ . DIRECTORY_SEPARATOR . join(DIRECTORY_SEPARATOR, ["fixtures", "npm", "audit-importmap.php"])); Http::preventStrayRequests(); }); diff --git a/tests/NpmOutdatedTest.php b/tests/NpmOutdatedTest.php index 3525886..88fe727 100644 --- a/tests/NpmOutdatedTest.php +++ b/tests/NpmOutdatedTest.php @@ -5,7 +5,7 @@ use Illuminate\Support\Facades\Http; beforeEach(function () { - $this->npm = new Npm(configPath: __DIR__ . "/fixtures/npm/outdated-importmap.php"); + $this->npm = new Npm(configPath: __DIR__ . DIRECTORY_SEPARATOR . join(DIRECTORY_SEPARATOR, ["fixtures", "npm", "outdated-importmap.php"])); Http::preventStrayRequests(); }); From cf2cdc004d8d21753fcf10cc9d123e6ece0c1873 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 30 May 2022 18:07:01 -0300 Subject: [PATCH 24/26] Fix regex on Windows machines --- src/Npm.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Npm.php b/src/Npm.php index b6e3149..7e680de 100644 --- a/src/Npm.php +++ b/src/Npm.php @@ -81,7 +81,7 @@ private function packagesWithVersion(): Collection private function findPackagesFromCdnMatches(string $content) { - preg_match_all('/^Importmap\:\:pin\(.*(?<=npm:|npm\/|skypack\.dev\/|unpkg\.com\/)(.*)(?=@\d+\.\d+\.\d+)@(\d+\.\d+\.\d+(?:[^\/\s"]*)).*\)\;$/m', $content, $matches); + preg_match_all('/^Importmap\:\:pin\(.*(?<=npm:|npm\/|skypack\.dev\/|unpkg\.com\/)(.*)(?=@\d+\.\d+\.\d+)@(\d+\.\d+\.\d+(?:[^\/\s"]*)).*\)\;\r?$/m', $content, $matches); if (count($matches) !== 3) { return collect(); @@ -95,7 +95,7 @@ private function findPackagesFromCdnMatches(string $content) private function findPackagesFromLocalMatches(string $content) { - preg_match_all('/^Importmap::pin\("([^"]*)".*\)\; \/\/.*@(\d+\.\d+\.\d+(?:[^\s]*)).*$/m', $content, $matches); + preg_match_all('/^Importmap::pin\("([^"]*)".*\)\; \/\/.*@(\d+\.\d+\.\d+(?:[^\s]*)).*\r?$/m', $content, $matches); if (count($matches) !== 3) { return collect(); From 9b70d2a3a0d09b5388f2813c802396177ccd324b Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 30 May 2022 18:27:21 -0300 Subject: [PATCH 25/26] Docs --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index 1f96b8d..f554d48 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,30 @@ Which will add the correct `links` tags to your head tag in the HTML document, l ``` +### Dependency Maintenance Commands + +Maintaining a healthy dependency list can be tricky. Here's a couple of commands to help you with this task. + +#### Outdated Dependencies + +To keep your dependencies up-to-date, make sure you run the `importmap:outdated` command from time to time: + +```bash +php artisan importmap:outdated +``` + +This command will scan your `config/importmap.php` file, find your current versions, then use the NPM registry API to look for the latest version of the packages you're using. It also handles locally served vendor libs that you added using the `--download` flag from the `importmap:pin` command. + +#### Auditing Dependencies + +If you want to a security audit on your dependecies to see if you're using a version that's been breached, run the `importmap:audit` command from time to time. Better yet, add that command to your CI build: + +```bash +php artisan importmap:audit +``` + +This will also scan your `config/importmap.php` file, find your current versions, then use the NPM registry API to look for vulnerabilities on your packages. It also handles locally serverd vendor libs that you added using the `--download` flag from the `importmap:pin` command. + ## Known Problems ### Browser Console Errors From 72a773378a0f898cc76d7587a71b443dd29bd725 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 30 May 2022 18:29:59 -0300 Subject: [PATCH 26/26] Tweaks docs headings --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f554d48..e0b61cf 100644 --- a/README.md +++ b/README.md @@ -194,11 +194,11 @@ Which will add the correct `links` tags to your head tag in the HTML document, l ``` -### Dependency Maintenance Commands +## Dependency Maintenance Commands Maintaining a healthy dependency list can be tricky. Here's a couple of commands to help you with this task. -#### Outdated Dependencies +### Outdated Dependencies To keep your dependencies up-to-date, make sure you run the `importmap:outdated` command from time to time: @@ -208,7 +208,7 @@ php artisan importmap:outdated This command will scan your `config/importmap.php` file, find your current versions, then use the NPM registry API to look for the latest version of the packages you're using. It also handles locally served vendor libs that you added using the `--download` flag from the `importmap:pin` command. -#### Auditing Dependencies +### Auditing Dependencies If you want to a security audit on your dependecies to see if you're using a version that's been breached, run the `importmap:audit` command from time to time. Better yet, add that command to your CI build: