From e8ecd6d769c113f287c39e8c235200f6a514da8e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 25 Sep 2026 14:56:11 +0000 Subject: [PATCH 1/2] feat: support Laravel 13 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Require PHP 8.3 and Laravel 11–13, and update Wink for the authentication contract, facade aliases, and public upload disk. Co-authored-by: Yeta Zulu --- .github/workflows/tests.yml | 23 +++ .gitignore | 1 + UPGRADE.md | 25 +++ composer.json | 17 +- config/wink.php | 8 +- phpunit.xml.dist | 16 ++ readme.md | 20 ++- src/Console/MigrateCommand.php | 3 +- .../Controllers/ForgotPasswordController.php | 3 +- src/Http/routes.php | 1 + src/WinkAuthor.php | 10 ++ src/WinkPost.php | 2 +- src/WinkServiceProvider.php | 52 +++--- tests/AuthenticationTest.php | 71 +++++++++ tests/DomainRoutingTest.php | 22 +++ tests/PackageTest.php | 56 +++++++ tests/PublishingTest.php | 150 ++++++++++++++++++ tests/TestCase.php | 93 +++++++++++ 18 files changed, 545 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/tests.yml create mode 100644 phpunit.xml.dist create mode 100644 tests/AuthenticationTest.php create mode 100644 tests/DomainRoutingTest.php create mode 100644 tests/PackageTest.php create mode 100644 tests/PublishingTest.php create mode 100644 tests/TestCase.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..61513f9a --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,23 @@ +name: tests + +on: + push: + pull_request: + +jobs: + laravel-13: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: shivammathur/setup-php@v2 + with: + php-version: "8.3" + extensions: curl, mbstring, sqlite, xml, zip + coverage: none + + - name: Install dependencies + run: composer update --prefer-dist --no-interaction --no-progress + + - name: Run tests + run: vendor/bin/phpunit diff --git a/.gitignore b/.gitignore index 41fe538b..7b7712ac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /node_modules /vendor /.idea +/.phpunit.cache composer.lock npm-debug.log yarn-error.log diff --git a/UPGRADE.md b/UPGRADE.md index 48425dee..d00bf872 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -12,3 +12,28 @@ php artisan vendor:publish --tag=wink-assets --force ``` In addition to this, make sure you render your posts and pages using `$post->content` and `$page->content` instead of `->body`. + +## Laravel 11, 12, and 13 + +This release requires PHP 8.3+ and Laravel 11, 12, or 13. + +`Wink\WinkAuthor` implements `getAuthPasswordName()` so the Laravel 11 authentication contract can resolve the password column. Route files import the `Route` facade directly, because a fresh Laravel 11+ application does not register global facade aliases. + +### Published configuration + +Laravel 11 and newer store the `local` filesystem disk in `storage/app/private`. Wink now defaults to the `public` disk so `php artisan storage:link` can serve uploads. If you already published `config/wink.php`, update these values: + +```php +'storage_disk' => env('WINK_STORAGE_DISK', 'public'), + +'storage_path' => env('WINK_STORAGE_PATH', 'wink/images'), +``` + +Files uploaded with the previous defaults on Laravel 10 (`local` disk, path `public/wink/images`) already live in `storage/app/public/wink/images`, which is the same directory as the new defaults. If a Laravel 11+ app stored uploads under `storage/app/private/public/wink/images`, move that directory to `storage/app/public/wink/images`. + +After updating, run: + +```sh +php artisan wink:migrate +php artisan vendor:publish --tag=wink-assets --force +``` diff --git a/composer.json b/composer.json index c10f4256..bc4f2a39 100644 --- a/composer.json +++ b/composer.json @@ -10,14 +10,24 @@ } ], "require": { - "php": "^7.3||^7.4||^8.0", - "laravel/framework": "^7.0||^8.0||^9.0|^10.0" + "php": "^8.3", + "laravel/framework": "^11.0||^12.0||^13.0", + "league/commonmark": "^2.7" + }, + "require-dev": { + "orchestra/testbench": "^11.5", + "phpunit/phpunit": "^11.5.50||^12.5.8" }, "autoload": { "psr-4": { "Wink\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "Wink\\Tests\\": "tests/" + } + }, "extra": { "branch-alias": { "dev-master": "1.x-dev" @@ -28,6 +38,9 @@ ] } }, + "scripts": { + "test": "phpunit" + }, "config": { "sort-packages": true }, diff --git a/config/wink.php b/config/wink.php index b75fbd6b..720a7b95 100644 --- a/config/wink.php +++ b/config/wink.php @@ -24,11 +24,15 @@ | any of the disks defined in your config/filesystems.php file. You may | also configure the path where the files should be stored. | + | The public disk keeps uploads under storage/app/public so that + | `php artisan storage:link` can serve them. Laravel 11 and newer point + | the local disk at storage/app/private, which is not web-accessible. + | */ - 'storage_disk' => env('WINK_STORAGE_DISK', 'local'), + 'storage_disk' => env('WINK_STORAGE_DISK', 'public'), - 'storage_path' => env('WINK_STORAGE_PATH', 'public/wink/images'), + 'storage_path' => env('WINK_STORAGE_PATH', 'wink/images'), /* |-------------------------------------------------------------------------- diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 00000000..ab4a7bcf --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,16 @@ + + + + + tests + + + + + + diff --git a/readme.md b/readme.md index 5d4bb4cf..f2b9256d 100644 --- a/readme.md +++ b/readme.md @@ -8,14 +8,28 @@ Wink is used to manage the [official Laravel blog](https://blog.laravel.com), [d Dark & Light modes available so everyone is happy 😁 +## Requirements + +- PHP 8.3, 8.4, or 8.5 +- Laravel 11, 12, or 13 + ## Installation Wink uses a separate database connection and authentication system so that you don't have to modify any of your project code. -To install Wink, run these commands in the root of your Laravel app: +Packagist `themsaid/wink` stops at Laravel 10. Install this fork from GitHub: + +```json +"repositories": [ + { + "type": "vcs", + "url": "https://github.com/drscript/wink" + } +] +``` ```sh -composer require themsaid/wink +composer require themsaid/wink:dev-1.x php artisan wink:install php artisan storage:link ``` @@ -28,6 +42,8 @@ php artisan wink:migrate Head to `yourproject.test/wink` and use the provided email and password to log in. +Uploaded images use the `public` disk and are stored in `storage/app/public/wink/images`. `storage:link` serves that directory. On Laravel 11 and newer the `local` disk points at `storage/app/private`, so leave `storage_disk` set to `public` unless you intend to use a private or S3 disk. + ## Uploading to S3 If you want to upload images to S3, update the `storage_disk` attribute in your `wink.php` configuration file to s3. Make sure your S3 disk is correctly configured in your `filesystems.php` configuration file. diff --git a/src/Console/MigrateCommand.php b/src/Console/MigrateCommand.php index 62a23093..cd48e509 100644 --- a/src/Console/MigrateCommand.php +++ b/src/Console/MigrateCommand.php @@ -38,7 +38,8 @@ public function handle() $this->call('migrate', [ '--database' => config('wink.database_connection'), - '--path' => 'vendor/themsaid/wink/src/Migrations', + '--path' => realpath(__DIR__.'/../Migrations'), + '--realpath' => true, '--force' => $this->option('force') ?? true, ]); diff --git a/src/Http/Controllers/ForgotPasswordController.php b/src/Http/Controllers/ForgotPasswordController.php index cb87b2a0..a93b5977 100644 --- a/src/Http/Controllers/ForgotPasswordController.php +++ b/src/Http/Controllers/ForgotPasswordController.php @@ -3,6 +3,7 @@ namespace Wink\Http\Controllers; use Illuminate\Routing\Controller; +use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Str; use Throwable; @@ -69,7 +70,7 @@ public function showNewPassword($token) cache()->forget('password.reset.'.$authorId); - $author->password = \Hash::make($password = Str::random()); + $author->password = Hash::make($password = Str::random()); $author->save(); diff --git a/src/Http/routes.php b/src/Http/routes.php index d9cae4ca..9deb3a12 100644 --- a/src/Http/routes.php +++ b/src/Http/routes.php @@ -1,5 +1,6 @@ {$this->getAuthIdentifierName()}; } + /** + * Get the name of the password attribute for the user. + * + * @return string + */ + public function getAuthPasswordName() + { + return 'password'; + } + /** * Get the password for the user. * diff --git a/src/WinkPost.php b/src/WinkPost.php index 161d0c14..c7011ed6 100644 --- a/src/WinkPost.php +++ b/src/WinkPost.php @@ -111,7 +111,7 @@ public function getContentAttribute() 'allow_unsafe_links' => false, ]); - return new HtmlString($converter->convertToHtml($this->body)); + return new HtmlString((string) $converter->convert((string) $this->body)); } /** diff --git a/src/WinkServiceProvider.php b/src/WinkServiceProvider.php index d9588c27..e1b05a02 100644 --- a/src/WinkServiceProvider.php +++ b/src/WinkServiceProvider.php @@ -35,26 +35,40 @@ private function registerRoutes() { $middlewareGroup = config('wink.middleware_group'); - Route::middleware($middlewareGroup) - ->as('wink.') - ->domain(config('wink.domain')) - ->prefix(config('wink.path')) - ->group(function () { - Route::get('/login', [LoginController::class, 'showLoginForm'])->name('auth.login'); - Route::post('/login', [LoginController::class, 'login'])->name('auth.attempt'); - - Route::get('/password/forgot', [ForgotPasswordController::class, 'showResetRequestForm'])->name('password.forgot'); - Route::post('/password/forgot', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email'); - Route::get('/password/reset/{token}', [ForgotPasswordController::class, 'showNewPassword'])->name('password.reset'); - }); - - Route::middleware([$middlewareGroup, Authenticate::class]) + $this->routeGroup($middlewareGroup)->group(function () { + Route::get('/login', [LoginController::class, 'showLoginForm'])->name('auth.login'); + Route::post('/login', [LoginController::class, 'login'])->name('auth.attempt'); + + Route::get('/password/forgot', [ForgotPasswordController::class, 'showResetRequestForm'])->name('password.forgot'); + Route::post('/password/forgot', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email'); + Route::get('/password/reset/{token}', [ForgotPasswordController::class, 'showNewPassword'])->name('password.reset'); + }); + + $this->routeGroup([$middlewareGroup, Authenticate::class])->group(function () { + $this->loadRoutesFrom(__DIR__.'/Http/routes.php'); + }); + } + + /** + * Start a Wink route group. + * + * An empty domain is omitted so routes stay host-agnostic. Laravel 13 + * prioritizes routes that declare a domain during matching. + * + * @param string|array $middleware + * @return \Illuminate\Routing\RouteRegistrar + */ + private function routeGroup($middleware) + { + $routes = Route::middleware($middleware) ->as('wink.') - ->domain(config('wink.domain')) - ->prefix(config('wink.path')) - ->group(function () { - $this->loadRoutesFrom(__DIR__.'/Http/routes.php'); - }); + ->prefix(config('wink.path')); + + if ($domain = config('wink.domain')) { + $routes->domain($domain); + } + + return $routes; } /** diff --git a/tests/AuthenticationTest.php b/tests/AuthenticationTest.php new file mode 100644 index 00000000..4213628c --- /dev/null +++ b/tests/AuthenticationTest.php @@ -0,0 +1,71 @@ +get('/wink')->assertRedirect(route('wink.auth.login')); + $this->get('/wink/login')->assertOk()->assertSee('Log In'); + $this->getJson('/wink/api/posts')->assertUnauthorized(); + } + + public function test_authors_can_log_in_and_out(): void + { + $this->author(); + + $this->post('/wink/login', [ + 'email' => 'admin@mail.com', + 'password' => 'secret', + ])->assertRedirect('/wink'); + + $this->get('/wink')->assertOk()->assertSee('Regina Phalange', false); + + $this->get('/wink/logout')->assertRedirect(route('wink.auth.login')); + $this->get('/wink/api/posts')->assertRedirect(route('wink.auth.login')); + } + + public function test_invalid_credentials_are_rejected(): void + { + $this->author(); + + $this->from('/wink/login')->post('/wink/login', [ + 'email' => 'admin@mail.com', + 'password' => 'wrong', + ])->assertRedirect('/wink/login')->assertSessionHasErrors('email'); + } + + public function test_password_reset_issues_a_new_password(): void + { + $author = $this->author(); + + Mail::fake(); + + $this->post('/wink/password/forgot', [ + 'email' => $author->email, + ])->assertRedirect(route('wink.password.forgot')); + + $token = null; + + Mail::assertSent(ResetPasswordEmail::class, function (ResetPasswordEmail $mail) use (&$token) { + $token = $mail->token; + + return $mail->hasTo('admin@mail.com'); + }); + + $this->get('/wink/password/reset/'.$token) + ->assertOk() + ->assertSee('Copy your new password'); + + $author->refresh(); + + $this->assertFalse(\Illuminate\Support\Facades\Hash::check('secret', $author->password)); + + $this->get('/wink/password/reset/'.$token) + ->assertRedirect(route('wink.password.forgot')); + } +} diff --git a/tests/DomainRoutingTest.php b/tests/DomainRoutingTest.php new file mode 100644 index 00000000..7f6d22dd --- /dev/null +++ b/tests/DomainRoutingTest.php @@ -0,0 +1,22 @@ +set('wink.domain', 'blog.test'); + } + + public function test_routes_are_limited_to_the_configured_domain(): void + { + $login = app('router')->getRoutes()->getByName('wink.auth.login'); + $posts = app('router')->getRoutes()->getByName('wink.posts.index'); + + $this->assertSame('blog.test', $login->getDomain()); + $this->assertSame('blog.test', $posts->getDomain()); + } +} diff --git a/tests/PackageTest.php b/tests/PackageTest.php new file mode 100644 index 00000000..6f38b1c8 --- /dev/null +++ b/tests/PackageTest.php @@ -0,0 +1,56 @@ +assertSame(WinkAuthor::class, config('auth.providers.wink_authors.model')); + $this->assertSame('wink_authors', config('auth.guards.wink.provider')); + + $login = app('router')->getRoutes()->getByName('wink.auth.login'); + + $this->assertNotNull($login); + $this->assertNull($login->getDomain()); + $this->assertSame('wink/login', $login->uri()); + $this->assertNotNull(app('router')->getRoutes()->getByName('wink.posts.index')); + } + + public function test_migrations_create_wink_tables_on_the_wink_connection(): void + { + $this->assertTrue(\Illuminate\Support\Facades\Schema::connection('wink')->hasTable('wink_posts')); + $this->assertTrue(\Illuminate\Support\Facades\Schema::connection('wink')->hasTable('wink_authors')); + $this->assertTrue(\Illuminate\Support\Facades\Schema::connection('wink')->hasTable('wink_tags')); + $this->assertTrue(\Illuminate\Support\Facades\Schema::connection('wink')->hasTable('wink_pages')); + $this->assertFalse(\Illuminate\Support\Facades\Schema::connection('sqlite')->hasTable('wink_posts')); + } + + public function test_install_command_publishes_assets_and_config(): void + { + $this->artisan('wink:install')->assertSuccessful(); + + $this->assertFileExists(public_path('vendor/wink/mix-manifest.json')); + $this->assertFileExists(config_path('wink.php')); + } + + public function test_migrate_command_creates_the_default_author(): void + { + $this->artisan('wink:migrate') + ->expectsOutputToContain('admin@mail.com') + ->assertSuccessful(); + + $author = WinkAuthor::first(); + + $this->assertNotNull($author); + $this->assertSame('admin@mail.com', $author->email); + $this->assertSame('password', $author->getAuthPasswordName()); + $this->assertTrue(Hash::isHashed($author->password)); + + $this->artisan('wink:migrate')->assertSuccessful(); + $this->assertSame(1, WinkAuthor::count()); + } +} diff --git a/tests/PublishingTest.php b/tests/PublishingTest.php new file mode 100644 index 00000000..4a0f22ae --- /dev/null +++ b/tests/PublishingTest.php @@ -0,0 +1,150 @@ +author(); + + $live = $this->postFor($author, [ + 'markdown' => true, + 'body' => '**Hello**', + 'published' => true, + 'publish_date' => now()->subDay(), + ]); + $draft = $this->postFor($author, [ + 'id' => (string) Str::uuid(), + 'slug' => 'draft', + 'published' => false, + 'publish_date' => now()->subDay(), + ]); + $scheduled = $this->postFor($author, [ + 'id' => (string) Str::uuid(), + 'slug' => 'scheduled', + 'published' => true, + 'publish_date' => now()->addDay(), + ]); + + $tag = WinkTag::create([ + 'id' => (string) Str::uuid(), + 'slug' => 'laravel', + 'name' => 'Laravel', + ]); + $live->tags()->attach($tag->id); + + $this->assertStringContainsString('Hello', (string) $live->content); + $this->assertSame('The body', $draft->content); + $this->assertSame([$live->id], WinkPost::live()->pluck('id')->all()); + $this->assertSame([$draft->id], WinkPost::draft()->pluck('id')->all()); + $this->assertSame([$scheduled->id], WinkPost::scheduled()->pluck('id')->all()); + $this->assertTrue(WinkPost::tag('laravel')->whereKey($live->id)->exists()); + } + + public function test_authors_can_manage_posts_pages_tags_and_team_members(): void + { + $author = $this->author(); + $this->actingAs($author, 'wink'); + + $this->postJson('/wink/api/posts/new', [ + 'id' => $postId = (string) Str::uuid(), + 'title' => 'First post', + 'slug' => 'first-post', + 'excerpt' => 'Excerpt', + 'body' => 'Body', + 'published' => true, + 'markdown' => false, + 'author_id' => $author->id, + 'publish_date' => now()->format('Y-m-d H:i:s'), + 'tags' => [ + ['id' => (string) Str::uuid(), 'name' => 'News'], + ], + ])->assertOk()->assertJsonPath('entry.slug', 'first-post'); + + $this->assertTrue(WinkPost::find($postId)->tags->contains('name', 'News')); + + $this->postJson('/wink/api/posts/new', [ + 'id' => (string) Str::uuid(), + 'title' => 'Duplicate', + 'slug' => 'first-post', + 'author_id' => $author->id, + 'publish_date' => now()->format('Y-m-d H:i:s'), + ])->assertStatus(422); + + $this->getJson('/wink/api/posts?status=published&author_id='.$author->id) + ->assertOk() + ->assertJsonPath('data.0.slug', 'first-post'); + + $this->postJson('/wink/api/pages/new', [ + 'id' => $pageId = (string) Str::uuid(), + 'title' => 'About', + 'slug' => 'about', + 'body' => 'About this site', + ])->assertOk(); + + $this->assertSame('About this site', WinkPage::find($pageId)->content); + + $this->postJson('/wink/api/tags/new', [ + 'id' => $tagId = (string) Str::uuid(), + 'name' => 'Laravel', + 'slug' => 'laravel', + ])->assertOk(); + + $this->deleteJson('/wink/api/tags/'.$tagId)->assertOk(); + $this->assertNull(WinkTag::find($tagId)); + + $teammateId = (string) Str::uuid(); + + $this->postJson('/wink/api/team/new', [ + 'id' => $teammateId, + 'name' => 'Ada Lovelace', + 'slug' => 'ada-lovelace', + 'email' => 'ada@example.com', + 'password' => 'secret-pass', + 'bio' => 'Author', + ])->assertOk(); + + $this->deleteJson('/wink/api/team/'.$author->id) + ->assertStatus(402) + ->assertJsonPath('message', 'Please remove the author\'s posts first.'); + + $this->deleteJson('/wink/api/posts/'.$postId)->assertOk(); + + $this->deleteJson('/wink/api/team/'.$author->id) + ->assertStatus(402) + ->assertJsonPath('message', 'You cannot delete yourself.'); + + $this->deleteJson('/wink/api/team/'.$teammateId)->assertOk(); + $this->assertNull(\Wink\WinkAuthor::find($teammateId)); + } + + public function test_image_uploads_are_stored_on_the_public_disk(): void + { + Storage::fake('public'); + + $this->actingAs($this->author(), 'wink'); + + $response = $this->postJson('/wink/api/uploads', [ + 'image' => UploadedFile::fake()->create('photo.jpg', 20, 'image/jpeg'), + ]); + + $response->assertOk(); + + $url = $response->json('url'); + + $this->assertIsString($url); + $this->assertStringContainsString('wink/images', $url); + + $stored = Storage::disk('public')->allFiles('wink/images'); + + $this->assertCount(1, $stored); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 00000000..9e8dd997 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,93 @@ +artisan('vendor:publish', [ + '--tag' => 'wink-assets', + '--force' => true, + ]); + } + + $this->artisan('migrate', [ + '--database' => 'wink', + '--path' => realpath(__DIR__.'/../src/Migrations'), + '--realpath' => true, + ]); + } + + protected function getPackageProviders($app) + { + return [ + WinkServiceProvider::class, + ]; + } + + protected function defineEnvironment($app) + { + $app['config']->set('app.key', 'base64:YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE='); + $app['config']->set('database.default', 'sqlite'); + $app['config']->set('database.connections.sqlite', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ]); + $app['config']->set('database.connections.wink', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ]); + $app['config']->set('wink.database_connection', 'wink'); + $app['config']->set('wink.storage_disk', 'public'); + $app['config']->set('wink.storage_path', 'wink/images'); + $app['config']->set('cache.default', 'array'); + $app['config']->set('session.driver', 'array'); + $app['config']->set('filesystems.disks.public', [ + 'driver' => 'local', + 'root' => storage_path('framework/testing/disks/public'), + 'url' => 'http://localhost/storage', + 'visibility' => 'public', + ]); + } + + protected function author(array $overrides = []): WinkAuthor + { + return WinkAuthor::create(array_merge([ + 'id' => (string) Str::uuid(), + 'name' => 'Regina Phalange', + 'slug' => 'regina-phalange', + 'email' => 'admin@mail.com', + 'password' => Hash::make('secret'), + 'bio' => 'This is me.', + ], $overrides)); + } + + protected function postFor(WinkAuthor $author, array $overrides = []): WinkPost + { + return WinkPost::create(array_merge([ + 'id' => (string) Str::uuid(), + 'slug' => 'hello-world', + 'title' => 'Hello World', + 'excerpt' => 'An excerpt', + 'body' => 'The body', + 'published' => true, + 'markdown' => false, + 'publish_date' => now()->subMinute(), + 'featured_image_caption' => '', + 'author_id' => $author->id, + ], $overrides)); + } +} From 128c97631346fb6ba03c1478e063ab7c4974693f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 25 Sep 2026 14:58:20 +0000 Subject: [PATCH 2/2] test: depend on stable Orchestra Testbench 11 The ^11.5 constraint only matched the 11.x dev branch. ^11.0 resolves to stable v11.3, which still runs the suite on Laravel 13. Co-authored-by: Yeta Zulu --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index bc4f2a39..02c7f6c1 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "league/commonmark": "^2.7" }, "require-dev": { - "orchestra/testbench": "^11.5", + "orchestra/testbench": "^11.0", "phpunit/phpunit": "^11.5.50||^12.5.8" }, "autoload": {