diff --git a/src/Illuminate/Contracts/Foundation/Vite.php b/src/Illuminate/Contracts/Foundation/Vite.php new file mode 100644 index 000000000000..c0f1ed0bdbd8 --- /dev/null +++ b/src/Illuminate/Contracts/Foundation/Vite.php @@ -0,0 +1,151 @@ + Vite::class, + ViteServiceProvider::class, ]; /** diff --git a/src/Illuminate/Foundation/Providers/ViteServiceProvider.php b/src/Illuminate/Foundation/Providers/ViteServiceProvider.php new file mode 100644 index 000000000000..a47957fad6ae --- /dev/null +++ b/src/Illuminate/Foundation/Providers/ViteServiceProvider.php @@ -0,0 +1,35 @@ +app->singleton('vite', fn ($app) => new ViteManager($app)); + $this->app->singleton('vite.app', fn ($app) => $app['vite']->app()); + $this->app->bind(ViteContract::class, 'vite.app'); + $this->app->bind(Vite::class, 'vite.app'); + } + + /** + * Get the services provided by the provider. + * + * @return array + */ + public function provides() + { + return ['vite', 'vite.app', ViteContract::class, Vite::class]; + } +} diff --git a/src/Illuminate/Foundation/Vite.php b/src/Illuminate/Foundation/Vite.php index 0eddc517b2b5..8baab5d49031 100644 --- a/src/Illuminate/Foundation/Vite.php +++ b/src/Illuminate/Foundation/Vite.php @@ -3,13 +3,16 @@ namespace Illuminate\Foundation; use Exception; -use Illuminate\Contracts\Support\Htmlable; +use Illuminate\Contracts\Foundation\Vite as ViteContract; +use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\HtmlString; use Illuminate\Support\Str; use Illuminate\Support\Traits\Macroable; +use ReflectionClass; +use ReflectionMethod; -class Vite implements Htmlable +class Vite implements ViteContract { use Macroable; @@ -90,6 +93,37 @@ class Vite implements Htmlable */ protected static $manifests = []; + /** + * Apply configuration to the Vite instance. + * + * @param array $config + * @return $this + */ + public function configure($config) + { + $class = new ReflectionClass($this); + $methods = $class->getMethods(ReflectionMethod::IS_PUBLIC); + $currentMethod = __FUNCTION__; + + $methodKeyMap = [ + 'useCspNonce' => 'nonce', + 'useScriptTagAttributes' => 'tag_attributes.script', + 'useStyleTagAttributes' => 'tag_attributes.style', + 'usePreloadTagAttributes' => 'tag_attributes.preload', + ]; + + collect($methods)->filter(fn (ReflectionMethod $method) => ! $method->isStatic() + && $method->getName() !== $currentMethod + && preg_match('/^(use|with)/', $method->getName()) + )->each(function (ReflectionMethod $method) use ($config, $methodKeyMap) { + $key = $methodKeyMap[$method->getName()] + ?? str($method->getName())->after('use')->after('with')->snake()->value(); + Arr::has($config, $key) && $method->invoke($this, Arr::get($config, $key)); + }); + + return $this; + } + /** * Get the preloaded assets. * @@ -113,7 +147,7 @@ public function cspNonce() /** * Generate or set a Content Security Policy nonce to apply to all generated tags. * - * @param ?string $nonce + * @param string|null $nonce * @return string */ public function useCspNonce($nonce = null) diff --git a/src/Illuminate/Foundation/ViteManager.php b/src/Illuminate/Foundation/ViteManager.php new file mode 100644 index 000000000000..85c977de51f6 --- /dev/null +++ b/src/Illuminate/Foundation/ViteManager.php @@ -0,0 +1,288 @@ +config->get('vite.app', 'default'); + } + + /** + * Create a new driver instance. + * + * @param string $driver + * @return \Illuminate\Contracts\Foundation\Vite + */ + protected function createDriver($driver) + { + try { + return parent::createDriver($driver); + } catch (InvalidArgumentException) { + return $this->createApp($driver); + } + } + + /** + * Create a new app instance. + * + * @param string $app + * @return \Illuminate\Contracts\Foundation\Vite + */ + protected function createApp($app) + { + return ($this->appFactory ?? function (ViteContract $vite, string $app, array $config) { + return $vite->configure($config); + })(new Vite, $app, config("vite.apps.$app", []), $this->container); + } + + /** + * Register an app factory callback. + * + * @param callable(\Illuminate\Contracts\Foundation\Vite, string, array, \Illuminate\Contracts\Container\Container): \Illuminate\Contracts\Foundation\Vite $appFactory + * @return $this + */ + public function useAppFactory($appFactory) + { + $this->appFactory = $appFactory; + + return $this; + } + + /** + * Get an app instance. + * + * @param string|null $app + * @return \Illuminate\Contracts\Foundation\Vite + * + * @throws \InvalidArgumentException + */ + public function app($app = null) + { + return $this->driver($app); + } + + /** + * Apply configuration to the Vite instance. + * + * @param array $config + * @return \Illuminate\Contracts\Foundation\Vite + */ + public function configure($config) + { + return $this->app()->configure($config); + } + + /** + * Get the preloaded assets. + * + * @return array + */ + public function preloadedAssets() + { + return $this->app()->preloadedAssets(); + } + + /** + * Get the Content Security Policy nonce applied to all generated tags. + * + * @return string|null + */ + public function cspNonce() + { + return $this->app()->cspNonce(); + } + + /** + * Generate or set a Content Security Policy nonce to apply to all generated tags. + * + * @param string|null $nonce + * @return string + */ + public function useCspNonce($nonce = null) + { + return $this->app()->useCspNonce($nonce); + } + + /** + * Use the given key to detect integrity hashes in the manifest. + * + * @param string|false $key + * @return \Illuminate\Contracts\Foundation\Vite + */ + public function useIntegrityKey($key) + { + return $this->app()->useIntegrityKey($key); + } + + /** + * Set the Vite entry points. + * + * @param array $entryPoints + * @return \Illuminate\Contracts\Foundation\Vite + */ + public function withEntryPoints($entryPoints) + { + return $this->app()->withEntryPoints($entryPoints); + } + + /** + * Set the filename for the manifest file. + * + * @param string $filename + * @return \Illuminate\Contracts\Foundation\Vite + */ + public function useManifestFilename($filename) + { + return $this->app()->useManifestFilename($filename); + } + + /** + * Get the Vite "hot" file path. + * + * @return string + */ + public function hotFile(): string + { + return $this->app()->hotFile(); + } + + /** + * Set the Vite "hot" file path. + * + * @param string $path + * @return \Illuminate\Contracts\Foundation\Vite + */ + public function useHotFile($path) + { + return $this->app()->useHotFile($path); + } + + /** + * Set the Vite build directory. + * + * @param string $path + * @return \Illuminate\Contracts\Foundation\Vite + */ + public function useBuildDirectory($path) + { + return $this->app()->useBuildDirectory($path); + } + + /** + * Use the given callback to resolve attributes for script tags. + * + * @param (callable(string, string, ?array, ?array): array)|array $attributes + * @return \Illuminate\Contracts\Foundation\Vite + */ + public function useScriptTagAttributes($attributes) + { + return $this->app()->useScriptTagAttributes($attributes); + } + + /** + * Use the given callback to resolve attributes for style tags. + * + * @param (callable(string, string, ?array, ?array): array)|array $attributes + * @return \Illuminate\Contracts\Foundation\Vite + */ + public function useStyleTagAttributes($attributes) + { + return $this->app()->useStyleTagAttributes($attributes); + } + + /** + * Use the given callback to resolve attributes for preload tags. + * + * @param (callable(string, string, ?array, ?array): array)|array $attributes + * @return \Illuminate\Contracts\Foundation\Vite + */ + public function usePreloadTagAttributes($attributes) + { + return $this->app()->usePreloadTagAttributes($attributes); + } + + /** + * Generate Vite tags for an entrypoint. + * + * @param string|string[] $entrypoints + * @param string|null $buildDirectory + * @return \Illuminate\Support\HtmlString + * + * @throws \Exception + */ + public function __invoke($entrypoints, $buildDirectory = null) + { + return $this->app()->__invoke($entrypoints, $buildDirectory); + } + + /** + * Generate React refresh runtime script. + * + * @return \Illuminate\Support\HtmlString|void + */ + public function reactRefresh() + { + return $this->app()->reactRefresh(); + } + + /** + * Get the URL for an asset. + * + * @param string $asset + * @param string|null $buildDirectory + * @return string + */ + public function asset($asset, $buildDirectory = null) + { + return $this->app()->asset($asset, $buildDirectory); + } + + /** + * Get a unique hash representing the current manifest, or null if there is no manifest. + * + * @param string|null $buildDirectory + * @return string|null + */ + public function manifestHash($buildDirectory = null) + { + return $this->app()->manifestHash($buildDirectory); + } + + /** + * Determine if the HMR server is running. + * + * @return bool + */ + public function isRunningHot() + { + return $this->app()->isRunningHot(); + } + + /** + * Get the Vite tag content as a string of HTML. + * + * @return string + */ + public function toHtml() + { + return $this->app()->toHtml(); + } +} diff --git a/src/Illuminate/Support/Facades/Vite.php b/src/Illuminate/Support/Facades/Vite.php index ad5576463699..99e27ce270ac 100644 --- a/src/Illuminate/Support/Facades/Vite.php +++ b/src/Illuminate/Support/Facades/Vite.php @@ -3,29 +3,35 @@ namespace Illuminate\Support\Facades; /** + * @method static string getDefaultDriver() + * @method static \Illuminate\Foundation\ViteManager useAppFactory(callable(\Illuminate\Contracts\Foundation\Vite, string, array, \Illuminate\Contracts\Container\Container): \Illuminate\Contracts\Foundation\Vite $appFactory) + * @method static \Illuminate\Contracts\Foundation\Vite app(string|null $app = null) + * @method static \Illuminate\Contracts\Foundation\Vite configure(array $config) * @method static array preloadedAssets() * @method static string|null cspNonce() - * @method static string useCspNonce(?string $nonce = null) - * @method static \Illuminate\Foundation\Vite useIntegrityKey(string|false $key) - * @method static \Illuminate\Foundation\Vite withEntryPoints(array $entryPoints) - * @method static \Illuminate\Foundation\Vite useManifestFilename(string $filename) + * @method static string useCspNonce(string|null $nonce = null) + * @method static \Illuminate\Contracts\Foundation\Vite useIntegrityKey(string|false $key) + * @method static \Illuminate\Contracts\Foundation\Vite withEntryPoints(array $entryPoints) + * @method static \Illuminate\Contracts\Foundation\Vite useManifestFilename(string $filename) * @method static string hotFile() - * @method static \Illuminate\Foundation\Vite useHotFile(string $path) - * @method static \Illuminate\Foundation\Vite useBuildDirectory(string $path) - * @method static \Illuminate\Foundation\Vite useScriptTagAttributes((callable(string, string, ?array, ?array): array)|array $attributes) - * @method static \Illuminate\Foundation\Vite useStyleTagAttributes((callable(string, string, ?array, ?array): array)|array $attributes) - * @method static \Illuminate\Foundation\Vite usePreloadTagAttributes((callable(string, string, ?array, ?array): array)|array $attributes) + * @method static \Illuminate\Contracts\Foundation\Vite useHotFile(string $path) + * @method static \Illuminate\Contracts\Foundation\Vite useBuildDirectory(string $path) + * @method static \Illuminate\Contracts\Foundation\Vite useScriptTagAttributes((callable(string, string, ?array, ?array): array)|array $attributes) + * @method static \Illuminate\Contracts\Foundation\Vite useStyleTagAttributes((callable(string, string, ?array, ?array): array)|array $attributes) + * @method static \Illuminate\Contracts\Foundation\Vite usePreloadTagAttributes((callable(string, string, ?array, ?array): array)|array $attributes) * @method static \Illuminate\Support\HtmlString|void reactRefresh() * @method static string asset(string $asset, string|null $buildDirectory = null) * @method static string|null manifestHash(string|null $buildDirectory = null) * @method static bool isRunningHot() * @method static string toHtml() - * @method static void macro(string $name, object|callable $macro) - * @method static void mixin(object $mixin, bool $replace = true) - * @method static bool hasMacro(string $name) - * @method static void flushMacros() + * @method static mixed driver(string|null $driver = null) + * @method static \Illuminate\Foundation\ViteManager extend(string $driver, \Closure $callback) + * @method static array getDrivers() + * @method static \Illuminate\Contracts\Container\Container getContainer() + * @method static \Illuminate\Foundation\ViteManager setContainer(\Illuminate\Contracts\Container\Container $container) + * @method static \Illuminate\Foundation\ViteManager forgetDrivers() * - * @see \Illuminate\Foundation\Vite + * @see \Illuminate\Foundation\ViteManager */ class Vite extends Facade { @@ -36,6 +42,6 @@ class Vite extends Facade */ protected static function getFacadeAccessor() { - return \Illuminate\Foundation\Vite::class; + return 'vite'; } } diff --git a/src/Illuminate/View/Compilers/Concerns/CompilesHelpers.php b/src/Illuminate/View/Compilers/Concerns/CompilesHelpers.php index 6c6fcd996830..d43fcead1367 100644 --- a/src/Illuminate/View/Compilers/Concerns/CompilesHelpers.php +++ b/src/Illuminate/View/Compilers/Concerns/CompilesHelpers.php @@ -59,9 +59,20 @@ protected function compileVite($arguments) { $arguments ??= '()'; - $class = Vite::class; + return ""; + } + + /** + * Compile the "viteApp" statements into valid PHP. + * + * @param ?string $arguments + * @return string + */ + protected function compileViteApp($arguments) + { + $arguments ??= '()'; - return ""; + return "app{$arguments}->toHtml(); ?>"; } /** @@ -71,8 +82,6 @@ protected function compileVite($arguments) */ protected function compileViteReactRefresh() { - $class = Vite::class; - - return "reactRefresh(); ?>"; + return "reactRefresh(); ?>"; } } diff --git a/tests/Foundation/FoundationViteTest.php b/tests/Foundation/FoundationViteTest.php index 68814a7535b1..2c2be0be4e33 100644 --- a/tests/Foundation/FoundationViteTest.php +++ b/tests/Foundation/FoundationViteTest.php @@ -7,6 +7,7 @@ use Illuminate\Support\Facades\Vite as ViteFacade; use Illuminate\Support\Str; use Orchestra\Testbench\TestCase; +use ReflectionObject; class FoundationViteTest extends TestCase { @@ -982,6 +983,50 @@ public function testItCanConfigureTheManifestFilename() rmdir(public_path($buildDir)); } + public function testItCreatesInstances() + { + $this->assertSame(ViteFacade::app(), ViteFacade::app()); + $this->assertSame(ViteFacade::app('app1'), ViteFacade::app('app1')); + $this->assertNotSame(ViteFacade::app('app2'), ViteFacade::app('app3')); + } + + public function testItCanBeConfiguredWithArray() + { + $config = [ + 'nonce' => 'expected-nonce', + 'integrity_key' => 'some-integrity-key', + 'entry_points' => ['resources/js/app.js'], + 'hot_file' => 'cold', + 'build_directory' => 'build/packages', + 'manifest_filename' => 'oktoberfest.json', + 'tag_attributes' => [ + 'script' => ['type' => 'text/javascript', 'nomodule'], + 'style' => ['type' => 'text/css'], + 'preload' => ['rel' => 'dummy'], + ], + ]; + + $vite = app(Vite::class)->configure($config); + + $this->assertEquals($vite->cspNonce(), $config['nonce']); + $this->assertEquals($this->getViteProperty($vite, 'integrityKey'), $config['integrity_key']); + $this->assertEquals($this->getViteProperty($vite, 'entryPoints'), $config['entry_points']); + $this->assertEquals($vite->hotFile(), $config['hot_file']); + $this->assertEquals($this->getViteProperty($vite, 'buildDirectory'), $config['build_directory']); + $this->assertEquals($this->getViteProperty($vite, 'manifestFilename'), $config['manifest_filename']); + $this->assertEquals($this->getViteProperty($vite, 'scriptTagAttributesResolvers')[0](), $config['tag_attributes']['script']); + $this->assertEquals($this->getViteProperty($vite, 'styleTagAttributesResolvers')[0](), $config['tag_attributes']['style']); + $this->assertEquals($this->getViteProperty($vite, 'preloadTagAttributesResolvers')[0](), $config['tag_attributes']['preload']); + } + + protected function getViteProperty($vite, $propertyName) + { + $property = (new ReflectionObject($vite))->getProperty($propertyName); + $property->setAccessible(true); + + return $property->getValue($vite); + } + protected function makeViteManifest($contents = null, $path = 'build') { app()->singleton('path.public', fn () => __DIR__); diff --git a/tests/Http/Middleware/VitePreloadingTest.php b/tests/Http/Middleware/VitePreloadingTest.php index 641280cc8d9e..a55f07dce7e1 100644 --- a/tests/Http/Middleware/VitePreloadingTest.php +++ b/tests/Http/Middleware/VitePreloadingTest.php @@ -21,7 +21,7 @@ protected function tearDown(): void public function testItDoesNotSetLinkTagWhenNoTagsHaveBeenPreloaded() { $app = new Container(); - $app->instance(Vite::class, new class extends Vite + $app->instance('vite', new class extends Vite { protected $preloadedAssets = []; }); @@ -37,7 +37,7 @@ public function testItDoesNotSetLinkTagWhenNoTagsHaveBeenPreloaded() public function testItAddsPreloadLinkHeader() { $app = new Container(); - $app->instance(Vite::class, new class extends Vite + $app->instance('vite', new class extends Vite { protected $preloadedAssets = [ 'https://laravel.com/app.js' => [ diff --git a/tests/View/Blade/BladeHelpersTest.php b/tests/View/Blade/BladeHelpersTest.php index 8e071c38b6c6..11c1c6089a9a 100644 --- a/tests/View/Blade/BladeHelpersTest.php +++ b/tests/View/Blade/BladeHelpersTest.php @@ -11,10 +11,13 @@ public function testEchosAreCompiled() $this->assertSame('', $this->compiler->compileString('@dd($var1)')); $this->assertSame('', $this->compiler->compileString('@dd($var1, $var2)')); $this->assertSame('', $this->compiler->compileString('@dump($var1, $var2)')); - $this->assertSame('', $this->compiler->compileString('@vite')); - $this->assertSame('', $this->compiler->compileString('@vite()')); - $this->assertSame('', $this->compiler->compileString('@vite(\'resources/js/app.js\')')); - $this->assertSame('', $this->compiler->compileString('@vite([\'resources/js/app.js\'])')); - $this->assertSame('reactRefresh(); ?>', $this->compiler->compileString('@viteReactRefresh')); + $this->assertSame('', $this->compiler->compileString('@vite')); + $this->assertSame('', $this->compiler->compileString('@vite()')); + $this->assertSame('', $this->compiler->compileString('@vite(\'resources/js/app.js\')')); + $this->assertSame('', $this->compiler->compileString('@vite([\'resources/js/app.js\'])')); + $this->assertSame('app()->toHtml(); ?>', $this->compiler->compileString('@viteApp')); + $this->assertSame('app()->toHtml(); ?>', $this->compiler->compileString('@viteApp()')); + $this->assertSame('app(\'app\')->toHtml(); ?>', $this->compiler->compileString('@viteApp(\'app\')')); + $this->assertSame('reactRefresh(); ?>', $this->compiler->compileString('@viteReactRefresh')); } }