Revision 148 | Blame | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed
<?phpnamespace Illuminate\Foundation;use Exception;use Illuminate\Contracts\Support\Htmlable;use Illuminate\Support\Collection;use Illuminate\Support\HtmlString;use Illuminate\Support\Str;use Illuminate\Support\Traits\Macroable;class Vite implements Htmlable{use Macroable;/*** The Content Security Policy nonce to apply to all generated tags.** @var string|null*/protected $nonce;/*** The key to check for integrity hashes within the manifest.** @var string|false*/protected $integrityKey = 'integrity';/*** The configured entry points.** @var array*/protected $entryPoints = [];/*** The path to the "hot" file.** @var string|null*/protected $hotFile;/*** The path to the build directory.** @var string*/protected $buildDirectory = 'build';/*** The name of the manifest file.** @var string*/protected $manifestFilename = 'manifest.json';/*** The script tag attributes resolvers.** @var array*/protected $scriptTagAttributesResolvers = [];/*** The style tag attributes resolvers.** @var array*/protected $styleTagAttributesResolvers = [];/*** The preload tag attributes resolvers.** @var array*/protected $preloadTagAttributesResolvers = [];/*** The preloaded assets.** @var array*/protected $preloadedAssets = [];/*** The cached manifest files.** @var array*/protected static $manifests = [];/*** Get the preloaded assets.** @return array*/public function preloadedAssets(){return $this->preloadedAssets;}/*** Get the Content Security Policy nonce applied to all generated tags.** @return string|null*/public function cspNonce(){return $this->nonce;}/*** 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->nonce = $nonce ?? Str::random(40);}/*** Use the given key to detect integrity hashes in the manifest.** @param string|false $key* @return $this*/public function useIntegrityKey($key){$this->integrityKey = $key;return $this;}/*** Set the Vite entry points.** @param array $entryPoints* @return $this*/public function withEntryPoints($entryPoints){$this->entryPoints = $entryPoints;return $this;}/*** Set the filename for the manifest file.** @param string $filename* @return $this*/public function useManifestFilename($filename){$this->manifestFilename = $filename;return $this;}/*** Get the Vite "hot" file path.** @return string*/public function hotFile(){return $this->hotFile ?? public_path('/hot');}/*** Set the Vite "hot" file path.** @param string $path* @return $this*/public function useHotFile($path){$this->hotFile = $path;return $this;}/*** Set the Vite build directory.** @param string $path* @return $this*/public function useBuildDirectory($path){$this->buildDirectory = $path;return $this;}/*** Use the given callback to resolve attributes for script tags.** @param (callable(string, string, ?array, ?array): array)|array $attributes* @return $this*/public function useScriptTagAttributes($attributes){if (! is_callable($attributes)) {$attributes = fn () => $attributes;}$this->scriptTagAttributesResolvers[] = $attributes;return $this;}/*** Use the given callback to resolve attributes for style tags.** @param (callable(string, string, ?array, ?array): array)|array $attributes* @return $this*/public function useStyleTagAttributes($attributes){if (! is_callable($attributes)) {$attributes = fn () => $attributes;}$this->styleTagAttributesResolvers[] = $attributes;return $this;}/*** Use the given callback to resolve attributes for preload tags.** @param (callable(string, string, ?array, ?array): (array|false))|array|false $attributes* @return $this*/public function usePreloadTagAttributes($attributes){if (! is_callable($attributes)) {$attributes = fn () => $attributes;}$this->preloadTagAttributesResolvers[] = $attributes;return $this;}/*** 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){$entrypoints = collect($entrypoints);$buildDirectory ??= $this->buildDirectory;if ($this->isRunningHot()) {return new HtmlString($entrypoints->prepend('@vite/client')->map(fn ($entrypoint) => $this->makeTagForChunk($entrypoint, $this->hotAsset($entrypoint), null, null))->join(''));}$manifest = $this->manifest($buildDirectory);$tags = collect();$preloads = collect();foreach ($entrypoints as $entrypoint) {$chunk = $this->chunk($manifest, $entrypoint);$preloads->push([$chunk['src'],$this->assetPath("{$buildDirectory}/{$chunk['file']}"),$chunk,$manifest,]);foreach ($chunk['imports'] ?? [] as $import) {$preloads->push([$import,$this->assetPath("{$buildDirectory}/{$manifest[$import]['file']}"),$manifest[$import],$manifest,]);foreach ($manifest[$import]['css'] ?? [] as $css) {$partialManifest = Collection::make($manifest)->where('file', $css);$preloads->push([$partialManifest->keys()->first(),$this->assetPath("{$buildDirectory}/{$css}"),$partialManifest->first(),$manifest,]);$tags->push($this->makeTagForChunk($partialManifest->keys()->first(),$this->assetPath("{$buildDirectory}/{$css}"),$partialManifest->first(),$manifest));}}$tags->push($this->makeTagForChunk($entrypoint,$this->assetPath("{$buildDirectory}/{$chunk['file']}"),$chunk,$manifest));foreach ($chunk['css'] ?? [] as $css) {$partialManifest = Collection::make($manifest)->where('file', $css);$preloads->push([$partialManifest->keys()->first(),$this->assetPath("{$buildDirectory}/{$css}"),$partialManifest->first(),$manifest,]);$tags->push($this->makeTagForChunk($partialManifest->keys()->first(),$this->assetPath("{$buildDirectory}/{$css}"),$partialManifest->first(),$manifest));}}[$stylesheets, $scripts] = $tags->unique()->partition(fn ($tag) => str_starts_with($tag, '<link'));$preloads = $preloads->unique()->sortByDesc(fn ($args) => $this->isCssPath($args[1]))->map(fn ($args) => $this->makePreloadTagForChunk(...$args));return new HtmlString($preloads->join('').$stylesheets->join('').$scripts->join(''));}/*** Make tag for the given chunk.** @param string $src* @param string $url* @param array|null $chunk* @param array|null $manifest* @return string*/protected function makeTagForChunk($src, $url, $chunk, $manifest){if ($this->nonce === null&& $this->integrityKey !== false&& ! array_key_exists($this->integrityKey, $chunk ?? [])&& $this->scriptTagAttributesResolvers === []&& $this->styleTagAttributesResolvers === []) {return $this->makeTag($url);}if ($this->isCssPath($url)) {return $this->makeStylesheetTagWithAttributes($url,$this->resolveStylesheetTagAttributes($src, $url, $chunk, $manifest));}return $this->makeScriptTagWithAttributes($url,$this->resolveScriptTagAttributes($src, $url, $chunk, $manifest));}/*** Make a preload tag for the given chunk.** @param string $src* @param string $url* @param array $chunk* @param array $manifest* @return string*/protected function makePreloadTagForChunk($src, $url, $chunk, $manifest){$attributes = $this->resolvePreloadTagAttributes($src, $url, $chunk, $manifest);if ($attributes === false) {return '';}$this->preloadedAssets[$url] = $this->parseAttributes(Collection::make($attributes)->forget('href')->all());return '<link '.implode(' ', $this->parseAttributes($attributes)).' />';}/*** Resolve the attributes for the chunks generated script tag.** @param string $src* @param string $url* @param array|null $chunk* @param array|null $manifest* @return array*/protected function resolveScriptTagAttributes($src, $url, $chunk, $manifest){$attributes = $this->integrityKey !== false? ['integrity' => $chunk[$this->integrityKey] ?? false]: [];foreach ($this->scriptTagAttributesResolvers as $resolver) {$attributes = array_merge($attributes, $resolver($src, $url, $chunk, $manifest));}return $attributes;}/*** Resolve the attributes for the chunks generated stylesheet tag.** @param string $src* @param string $url* @param array|null $chunk* @param array|null $manifest* @return array*/protected function resolveStylesheetTagAttributes($src, $url, $chunk, $manifest){$attributes = $this->integrityKey !== false? ['integrity' => $chunk[$this->integrityKey] ?? false]: [];foreach ($this->styleTagAttributesResolvers as $resolver) {$attributes = array_merge($attributes, $resolver($src, $url, $chunk, $manifest));}return $attributes;}/*** Resolve the attributes for the chunks generated preload tag.** @param string $src* @param string $url* @param array $chunk* @param array $manifest* @return array|false*/protected function resolvePreloadTagAttributes($src, $url, $chunk, $manifest){$attributes = $this->isCssPath($url) ? ['rel' => 'preload','as' => 'style','href' => $url,'nonce' => $this->nonce ?? false,'crossorigin' => $this->resolveStylesheetTagAttributes($src, $url, $chunk, $manifest)['crossorigin'] ?? false,] : ['rel' => 'modulepreload','href' => $url,'nonce' => $this->nonce ?? false,'crossorigin' => $this->resolveScriptTagAttributes($src, $url, $chunk, $manifest)['crossorigin'] ?? false,];$attributes = $this->integrityKey !== false? array_merge($attributes, ['integrity' => $chunk[$this->integrityKey] ?? false]): $attributes;foreach ($this->preloadTagAttributesResolvers as $resolver) {if (false === ($resolvedAttributes = $resolver($src, $url, $chunk, $manifest))) {return false;}$attributes = array_merge($attributes, $resolvedAttributes);}return $attributes;}/*** Generate an appropriate tag for the given URL in HMR mode.** @deprecated Will be removed in a future Laravel version.** @param string $url* @return string*/protected function makeTag($url){if ($this->isCssPath($url)) {return $this->makeStylesheetTag($url);}return $this->makeScriptTag($url);}/*** Generate a script tag for the given URL.** @deprecated Will be removed in a future Laravel version.** @param string $url* @return string*/protected function makeScriptTag($url){return $this->makeScriptTagWithAttributes($url, []);}/*** Generate a stylesheet tag for the given URL in HMR mode.** @deprecated Will be removed in a future Laravel version.** @param string $url* @return string*/protected function makeStylesheetTag($url){return $this->makeStylesheetTagWithAttributes($url, []);}/*** Generate a script tag with attributes for the given URL.** @param string $url* @param array $attributes* @return string*/protected function makeScriptTagWithAttributes($url, $attributes){$attributes = $this->parseAttributes(array_merge(['type' => 'module','src' => $url,'nonce' => $this->nonce ?? false,], $attributes));return '<script '.implode(' ', $attributes).'></script>';}/*** Generate a link tag with attributes for the given URL.** @param string $url* @param array $attributes* @return string*/protected function makeStylesheetTagWithAttributes($url, $attributes){$attributes = $this->parseAttributes(array_merge(['rel' => 'stylesheet','href' => $url,'nonce' => $this->nonce ?? false,], $attributes));return '<link '.implode(' ', $attributes).' />';}/*** Determine whether the given path is a CSS file.** @param string $path* @return bool*/protected function isCssPath($path){return preg_match('/\.(css|less|sass|scss|styl|stylus|pcss|postcss)$/', $path) === 1;}/*** Parse the attributes into key="value" strings.** @param array $attributes* @return array*/protected function parseAttributes($attributes){return Collection::make($attributes)->reject(fn ($value, $key) => in_array($value, [false, null], true))->flatMap(fn ($value, $key) => $value === true ? [$key] : [$key => $value])->map(fn ($value, $key) => is_int($key) ? $value : $key.'="'.$value.'"')->values()->all();}/*** Generate React refresh runtime script.** @return \Illuminate\Support\HtmlString|void*/public function reactRefresh(){if (! $this->isRunningHot()) {return;}$attributes = $this->parseAttributes(['nonce' => $this->cspNonce(),]);return new HtmlString(sprintf(<<<'HTML'<script type="module" %s>import RefreshRuntime from '%s'RefreshRuntime.injectIntoGlobalHook(window)window.$RefreshReg$ = () => {}window.$RefreshSig$ = () => (type) => typewindow.__vite_plugin_react_preamble_installed__ = true</script>HTML,implode(' ', $attributes),$this->hotAsset('@react-refresh')));}/*** Get the path to a given asset when running in HMR mode.** @return string*/protected function hotAsset($asset){return rtrim(file_get_contents($this->hotFile())).'/'.$asset;}/*** Get the URL for an asset.** @param string $asset* @param string|null $buildDirectory* @return string*/public function asset($asset, $buildDirectory = null){$buildDirectory ??= $this->buildDirectory;if ($this->isRunningHot()) {return $this->hotAsset($asset);}$chunk = $this->chunk($this->manifest($buildDirectory), $asset);return $this->assetPath($buildDirectory.'/'.$chunk['file']);}/*** Generate an asset path for the application.** @param string $path* @param bool|null $secure* @return string*/protected function assetPath($path, $secure = null){return asset($path, $secure);}/*** Get the the manifest file for the given build directory.** @param string $buildDirectory* @return array** @throws \Exception*/protected function manifest($buildDirectory){$path = $this->manifestPath($buildDirectory);if (! isset(static::$manifests[$path])) {if (! is_file($path)) {throw new Exception("Vite manifest not found at: {$path}");}static::$manifests[$path] = json_decode(file_get_contents($path), true);}return static::$manifests[$path];}/*** Get the path to the manifest file for the given build directory.** @param string $buildDirectory* @return string*/protected function manifestPath($buildDirectory){return public_path($buildDirectory.'/'.$this->manifestFilename);}/*** 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){$buildDirectory ??= $this->buildDirectory;if ($this->isRunningHot()) {return null;}if (! is_file($path = $this->manifestPath($buildDirectory))) {return null;}return md5_file($path) ?: null;}/*** Get the chunk for the given entry point / asset.** @param array $manifest* @param string $file* @return array** @throws \Exception*/protected function chunk($manifest, $file){if (! isset($manifest[$file])) {throw new Exception("Unable to locate file in Vite manifest: {$file}.");}return $manifest[$file];}/*** Determine if the HMR server is running.** @return bool*/public function isRunningHot(){return is_file($this->hotFile());}/*** Get the Vite tag content as a string of HTML.** @return string*/public function toHtml(){return $this->__invoke($this->entryPoints)->toHtml();}}