Subversion-Projekte lars-tiefland.laravel_shop

Revision

Zur aktuellen Revision | Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
namespace Illuminate\Foundation;
4
 
5
use Exception;
6
use Illuminate\Contracts\Support\Htmlable;
7
use Illuminate\Support\Collection;
8
use Illuminate\Support\HtmlString;
9
use Illuminate\Support\Str;
10
use Illuminate\Support\Traits\Macroable;
11
 
12
class Vite implements Htmlable
13
{
14
    use Macroable;
15
 
16
    /**
17
     * The Content Security Policy nonce to apply to all generated tags.
18
     *
19
     * @var string|null
20
     */
21
    protected $nonce;
22
 
23
    /**
24
     * The key to check for integrity hashes within the manifest.
25
     *
26
     * @var string|false
27
     */
28
    protected $integrityKey = 'integrity';
29
 
30
    /**
31
     * The configured entry points.
32
     *
33
     * @var array
34
     */
35
    protected $entryPoints = [];
36
 
37
    /**
38
     * The path to the "hot" file.
39
     *
40
     * @var string|null
41
     */
42
    protected $hotFile;
43
 
44
    /**
45
     * The path to the build directory.
46
     *
47
     * @var string
48
     */
49
    protected $buildDirectory = 'build';
50
 
51
    /**
52
     * The name of the manifest file.
53
     *
54
     * @var string
55
     */
56
    protected $manifestFilename = 'manifest.json';
57
 
58
    /**
59
     * The script tag attributes resolvers.
60
     *
61
     * @var array
62
     */
63
    protected $scriptTagAttributesResolvers = [];
64
 
65
    /**
66
     * The style tag attributes resolvers.
67
     *
68
     * @var array
69
     */
70
    protected $styleTagAttributesResolvers = [];
71
 
72
    /**
73
     * The preload tag attributes resolvers.
74
     *
75
     * @var array
76
     */
77
    protected $preloadTagAttributesResolvers = [];
78
 
79
    /**
80
     * The preloaded assets.
81
     *
82
     * @var array
83
     */
84
    protected $preloadedAssets = [];
85
 
86
    /**
87
     * The cached manifest files.
88
     *
89
     * @var array
90
     */
91
    protected static $manifests = [];
92
 
93
    /**
94
     * Get the preloaded assets.
95
     *
96
     * @return array
97
     */
98
    public function preloadedAssets()
99
    {
100
        return $this->preloadedAssets;
101
    }
102
 
103
    /**
104
     * Get the Content Security Policy nonce applied to all generated tags.
105
     *
106
     * @return string|null
107
     */
108
    public function cspNonce()
109
    {
110
        return $this->nonce;
111
    }
112
 
113
    /**
114
     * Generate or set a Content Security Policy nonce to apply to all generated tags.
115
     *
116
     * @param  ?string  $nonce
117
     * @return string
118
     */
119
    public function useCspNonce($nonce = null)
120
    {
121
        return $this->nonce = $nonce ?? Str::random(40);
122
    }
123
 
124
    /**
125
     * Use the given key to detect integrity hashes in the manifest.
126
     *
127
     * @param  string|false  $key
128
     * @return $this
129
     */
130
    public function useIntegrityKey($key)
131
    {
132
        $this->integrityKey = $key;
133
 
134
        return $this;
135
    }
136
 
137
    /**
138
     * Set the Vite entry points.
139
     *
140
     * @param  array  $entryPoints
141
     * @return $this
142
     */
143
    public function withEntryPoints($entryPoints)
144
    {
145
        $this->entryPoints = $entryPoints;
146
 
147
        return $this;
148
    }
149
 
150
    /**
151
     * Set the filename for the manifest file.
152
     *
153
     * @param  string  $filename
154
     * @return $this
155
     */
156
    public function useManifestFilename($filename)
157
    {
158
        $this->manifestFilename = $filename;
159
 
160
        return $this;
161
    }
162
 
163
    /**
164
     * Get the Vite "hot" file path.
165
     *
166
     * @return string
167
     */
168
    public function hotFile()
169
    {
170
        return $this->hotFile ?? public_path('/hot');
171
    }
172
 
173
    /**
174
     * Set the Vite "hot" file path.
175
     *
176
     * @param  string  $path
177
     * @return $this
178
     */
179
    public function useHotFile($path)
180
    {
181
        $this->hotFile = $path;
182
 
183
        return $this;
184
    }
185
 
186
    /**
187
     * Set the Vite build directory.
188
     *
189
     * @param  string  $path
190
     * @return $this
191
     */
192
    public function useBuildDirectory($path)
193
    {
194
        $this->buildDirectory = $path;
195
 
196
        return $this;
197
    }
198
 
199
    /**
200
     * Use the given callback to resolve attributes for script tags.
201
     *
202
     * @param  (callable(string, string, ?array, ?array): array)|array  $attributes
203
     * @return $this
204
     */
205
    public function useScriptTagAttributes($attributes)
206
    {
207
        if (! is_callable($attributes)) {
208
            $attributes = fn () => $attributes;
209
        }
210
 
211
        $this->scriptTagAttributesResolvers[] = $attributes;
212
 
213
        return $this;
214
    }
215
 
216
    /**
217
     * Use the given callback to resolve attributes for style tags.
218
     *
219
     * @param  (callable(string, string, ?array, ?array): array)|array  $attributes
220
     * @return $this
221
     */
222
    public function useStyleTagAttributes($attributes)
223
    {
224
        if (! is_callable($attributes)) {
225
            $attributes = fn () => $attributes;
226
        }
227
 
228
        $this->styleTagAttributesResolvers[] = $attributes;
229
 
230
        return $this;
231
    }
232
 
233
    /**
234
     * Use the given callback to resolve attributes for preload tags.
235
     *
236
     * @param  (callable(string, string, ?array, ?array): (array|false))|array|false  $attributes
237
     * @return $this
238
     */
239
    public function usePreloadTagAttributes($attributes)
240
    {
241
        if (! is_callable($attributes)) {
242
            $attributes = fn () => $attributes;
243
        }
244
 
245
        $this->preloadTagAttributesResolvers[] = $attributes;
246
 
247
        return $this;
248
    }
249
 
250
    /**
251
     * Generate Vite tags for an entrypoint.
252
     *
253
     * @param  string|string[]  $entrypoints
254
     * @param  string|null  $buildDirectory
255
     * @return \Illuminate\Support\HtmlString
256
     *
257
     * @throws \Exception
258
     */
259
    public function __invoke($entrypoints, $buildDirectory = null)
260
    {
261
        $entrypoints = collect($entrypoints);
262
        $buildDirectory ??= $this->buildDirectory;
263
 
264
        if ($this->isRunningHot()) {
265
            return new HtmlString(
266
                $entrypoints
267
                    ->prepend('@vite/client')
268
                    ->map(fn ($entrypoint) => $this->makeTagForChunk($entrypoint, $this->hotAsset($entrypoint), null, null))
269
                    ->join('')
270
            );
271
        }
272
 
273
        $manifest = $this->manifest($buildDirectory);
274
 
275
        $tags = collect();
276
        $preloads = collect();
277
 
278
        foreach ($entrypoints as $entrypoint) {
279
            $chunk = $this->chunk($manifest, $entrypoint);
280
 
281
            $preloads->push([
282
                $chunk['src'],
283
                $this->assetPath("{$buildDirectory}/{$chunk['file']}"),
284
                $chunk,
285
                $manifest,
286
            ]);
287
 
288
            foreach ($chunk['imports'] ?? [] as $import) {
289
                $preloads->push([
290
                    $import,
291
                    $this->assetPath("{$buildDirectory}/{$manifest[$import]['file']}"),
292
                    $manifest[$import],
293
                    $manifest,
294
                ]);
295
 
296
                foreach ($manifest[$import]['css'] ?? [] as $css) {
297
                    $partialManifest = Collection::make($manifest)->where('file', $css);
298
 
299
                    $preloads->push([
300
                        $partialManifest->keys()->first(),
301
                        $this->assetPath("{$buildDirectory}/{$css}"),
302
                        $partialManifest->first(),
303
                        $manifest,
304
                    ]);
305
 
306
                    $tags->push($this->makeTagForChunk(
307
                        $partialManifest->keys()->first(),
308
                        $this->assetPath("{$buildDirectory}/{$css}"),
309
                        $partialManifest->first(),
310
                        $manifest
311
                    ));
312
                }
313
            }
314
 
315
            $tags->push($this->makeTagForChunk(
316
                $entrypoint,
317
                $this->assetPath("{$buildDirectory}/{$chunk['file']}"),
318
                $chunk,
319
                $manifest
320
            ));
321
 
322
            foreach ($chunk['css'] ?? [] as $css) {
323
                $partialManifest = Collection::make($manifest)->where('file', $css);
324
 
325
                $preloads->push([
326
                    $partialManifest->keys()->first(),
327
                    $this->assetPath("{$buildDirectory}/{$css}"),
328
                    $partialManifest->first(),
329
                    $manifest,
330
                ]);
331
 
332
                $tags->push($this->makeTagForChunk(
333
                    $partialManifest->keys()->first(),
334
                    $this->assetPath("{$buildDirectory}/{$css}"),
335
                    $partialManifest->first(),
336
                    $manifest
337
                ));
338
            }
339
        }
340
 
341
        [$stylesheets, $scripts] = $tags->unique()->partition(fn ($tag) => str_starts_with($tag, '<link'));
342
 
343
        $preloads = $preloads->unique()
344
            ->sortByDesc(fn ($args) => $this->isCssPath($args[1]))
345
            ->map(fn ($args) => $this->makePreloadTagForChunk(...$args));
346
 
347
        return new HtmlString($preloads->join('').$stylesheets->join('').$scripts->join(''));
348
    }
349
 
350
    /**
351
     * Make tag for the given chunk.
352
     *
353
     * @param  string  $src
354
     * @param  string  $url
355
     * @param  array|null  $chunk
356
     * @param  array|null  $manifest
357
     * @return string
358
     */
359
    protected function makeTagForChunk($src, $url, $chunk, $manifest)
360
    {
361
        if (
362
            $this->nonce === null
363
            && $this->integrityKey !== false
364
            && ! array_key_exists($this->integrityKey, $chunk ?? [])
365
            && $this->scriptTagAttributesResolvers === []
366
            && $this->styleTagAttributesResolvers === []) {
367
            return $this->makeTag($url);
368
        }
369
 
370
        if ($this->isCssPath($url)) {
371
            return $this->makeStylesheetTagWithAttributes(
372
                $url,
373
                $this->resolveStylesheetTagAttributes($src, $url, $chunk, $manifest)
374
            );
375
        }
376
 
377
        return $this->makeScriptTagWithAttributes(
378
            $url,
379
            $this->resolveScriptTagAttributes($src, $url, $chunk, $manifest)
380
        );
381
    }
382
 
383
    /**
384
     * Make a preload tag for the given chunk.
385
     *
386
     * @param  string  $src
387
     * @param  string  $url
388
     * @param  array  $chunk
389
     * @param  array  $manifest
390
     * @return string
391
     */
392
    protected function makePreloadTagForChunk($src, $url, $chunk, $manifest)
393
    {
394
        $attributes = $this->resolvePreloadTagAttributes($src, $url, $chunk, $manifest);
395
 
396
        if ($attributes === false) {
397
            return '';
398
        }
399
 
400
        $this->preloadedAssets[$url] = $this->parseAttributes(
401
            Collection::make($attributes)->forget('href')->all()
402
        );
403
 
404
        return '<link '.implode(' ', $this->parseAttributes($attributes)).' />';
405
    }
406
 
407
    /**
408
     * Resolve the attributes for the chunks generated script tag.
409
     *
410
     * @param  string  $src
411
     * @param  string  $url
412
     * @param  array|null  $chunk
413
     * @param  array|null  $manifest
414
     * @return array
415
     */
416
    protected function resolveScriptTagAttributes($src, $url, $chunk, $manifest)
417
    {
418
        $attributes = $this->integrityKey !== false
419
            ? ['integrity' => $chunk[$this->integrityKey] ?? false]
420
            : [];
421
 
422
        foreach ($this->scriptTagAttributesResolvers as $resolver) {
423
            $attributes = array_merge($attributes, $resolver($src, $url, $chunk, $manifest));
424
        }
425
 
426
        return $attributes;
427
    }
428
 
429
    /**
430
     * Resolve the attributes for the chunks generated stylesheet tag.
431
     *
432
     * @param  string  $src
433
     * @param  string  $url
434
     * @param  array|null  $chunk
435
     * @param  array|null  $manifest
436
     * @return array
437
     */
438
    protected function resolveStylesheetTagAttributes($src, $url, $chunk, $manifest)
439
    {
440
        $attributes = $this->integrityKey !== false
441
            ? ['integrity' => $chunk[$this->integrityKey] ?? false]
442
            : [];
443
 
444
        foreach ($this->styleTagAttributesResolvers as $resolver) {
445
            $attributes = array_merge($attributes, $resolver($src, $url, $chunk, $manifest));
446
        }
447
 
448
        return $attributes;
449
    }
450
 
451
    /**
452
     * Resolve the attributes for the chunks generated preload tag.
453
     *
454
     * @param  string  $src
455
     * @param  string  $url
456
     * @param  array  $chunk
457
     * @param  array  $manifest
458
     * @return array|false
459
     */
460
    protected function resolvePreloadTagAttributes($src, $url, $chunk, $manifest)
461
    {
462
        $attributes = $this->isCssPath($url) ? [
463
            'rel' => 'preload',
464
            'as' => 'style',
465
            'href' => $url,
466
            'nonce' => $this->nonce ?? false,
467
            'crossorigin' => $this->resolveStylesheetTagAttributes($src, $url, $chunk, $manifest)['crossorigin'] ?? false,
468
        ] : [
469
            'rel' => 'modulepreload',
470
            'href' => $url,
471
            'nonce' => $this->nonce ?? false,
472
            'crossorigin' => $this->resolveScriptTagAttributes($src, $url, $chunk, $manifest)['crossorigin'] ?? false,
473
        ];
474
 
475
        $attributes = $this->integrityKey !== false
476
            ? array_merge($attributes, ['integrity' => $chunk[$this->integrityKey] ?? false])
477
            : $attributes;
478
 
479
        foreach ($this->preloadTagAttributesResolvers as $resolver) {
480
            if (false === ($resolvedAttributes = $resolver($src, $url, $chunk, $manifest))) {
481
                return false;
482
            }
483
 
484
            $attributes = array_merge($attributes, $resolvedAttributes);
485
        }
486
 
487
        return $attributes;
488
    }
489
 
490
    /**
491
     * Generate an appropriate tag for the given URL in HMR mode.
492
     *
493
     * @deprecated Will be removed in a future Laravel version.
494
     *
495
     * @param  string  $url
496
     * @return string
497
     */
498
    protected function makeTag($url)
499
    {
500
        if ($this->isCssPath($url)) {
501
            return $this->makeStylesheetTag($url);
502
        }
503
 
504
        return $this->makeScriptTag($url);
505
    }
506
 
507
    /**
508
     * Generate a script tag for the given URL.
509
     *
510
     * @deprecated Will be removed in a future Laravel version.
511
     *
512
     * @param  string  $url
513
     * @return string
514
     */
515
    protected function makeScriptTag($url)
516
    {
517
        return $this->makeScriptTagWithAttributes($url, []);
518
    }
519
 
520
    /**
521
     * Generate a stylesheet tag for the given URL in HMR mode.
522
     *
523
     * @deprecated Will be removed in a future Laravel version.
524
     *
525
     * @param  string  $url
526
     * @return string
527
     */
528
    protected function makeStylesheetTag($url)
529
    {
530
        return $this->makeStylesheetTagWithAttributes($url, []);
531
    }
532
 
533
    /**
534
     * Generate a script tag with attributes for the given URL.
535
     *
536
     * @param  string  $url
537
     * @param  array  $attributes
538
     * @return string
539
     */
540
    protected function makeScriptTagWithAttributes($url, $attributes)
541
    {
542
        $attributes = $this->parseAttributes(array_merge([
543
            'type' => 'module',
544
            'src' => $url,
545
            'nonce' => $this->nonce ?? false,
546
        ], $attributes));
547
 
548
        return '<script '.implode(' ', $attributes).'></script>';
549
    }
550
 
551
    /**
552
     * Generate a link tag with attributes for the given URL.
553
     *
554
     * @param  string  $url
555
     * @param  array  $attributes
556
     * @return string
557
     */
558
    protected function makeStylesheetTagWithAttributes($url, $attributes)
559
    {
560
        $attributes = $this->parseAttributes(array_merge([
561
            'rel' => 'stylesheet',
562
            'href' => $url,
563
            'nonce' => $this->nonce ?? false,
564
        ], $attributes));
565
 
566
        return '<link '.implode(' ', $attributes).' />';
567
    }
568
 
569
    /**
570
     * Determine whether the given path is a CSS file.
571
     *
572
     * @param  string  $path
573
     * @return bool
574
     */
575
    protected function isCssPath($path)
576
    {
577
        return preg_match('/\.(css|less|sass|scss|styl|stylus|pcss|postcss)$/', $path) === 1;
578
    }
579
 
580
    /**
581
     * Parse the attributes into key="value" strings.
582
     *
583
     * @param  array  $attributes
584
     * @return array
585
     */
586
    protected function parseAttributes($attributes)
587
    {
588
        return Collection::make($attributes)
589
            ->reject(fn ($value, $key) => in_array($value, [false, null], true))
590
            ->flatMap(fn ($value, $key) => $value === true ? [$key] : [$key => $value])
591
            ->map(fn ($value, $key) => is_int($key) ? $value : $key.'="'.$value.'"')
592
            ->values()
593
            ->all();
594
    }
595
 
596
    /**
597
     * Generate React refresh runtime script.
598
     *
599
     * @return \Illuminate\Support\HtmlString|void
600
     */
601
    public function reactRefresh()
602
    {
603
        if (! $this->isRunningHot()) {
604
            return;
605
        }
606
 
607
        $attributes = $this->parseAttributes([
608
            'nonce' => $this->cspNonce(),
609
        ]);
610
 
611
        return new HtmlString(
612
            sprintf(
613
                <<<'HTML'
614
                <script type="module" %s>
615
                    import RefreshRuntime from '%s'
616
                    RefreshRuntime.injectIntoGlobalHook(window)
617
                    window.$RefreshReg$ = () => {}
618
                    window.$RefreshSig$ = () => (type) => type
619
                    window.__vite_plugin_react_preamble_installed__ = true
620
                </script>
621
                HTML,
622
                implode(' ', $attributes),
623
                $this->hotAsset('@react-refresh')
624
            )
625
        );
626
    }
627
 
628
    /**
629
     * Get the path to a given asset when running in HMR mode.
630
     *
631
     * @return string
632
     */
633
    protected function hotAsset($asset)
634
    {
635
        return rtrim(file_get_contents($this->hotFile())).'/'.$asset;
636
    }
637
 
638
    /**
639
     * Get the URL for an asset.
640
     *
641
     * @param  string  $asset
642
     * @param  string|null  $buildDirectory
643
     * @return string
644
     */
645
    public function asset($asset, $buildDirectory = null)
646
    {
647
        $buildDirectory ??= $this->buildDirectory;
648
 
649
        if ($this->isRunningHot()) {
650
            return $this->hotAsset($asset);
651
        }
652
 
653
        $chunk = $this->chunk($this->manifest($buildDirectory), $asset);
654
 
655
        return $this->assetPath($buildDirectory.'/'.$chunk['file']);
656
    }
657
 
658
    /**
659
     * Generate an asset path for the application.
660
     *
661
     * @param  string  $path
662
     * @param  bool|null  $secure
663
     * @return string
664
     */
665
    protected function assetPath($path, $secure = null)
666
    {
667
        return asset($path, $secure);
668
    }
669
 
670
    /**
671
     * Get the the manifest file for the given build directory.
672
     *
673
     * @param  string  $buildDirectory
674
     * @return array
675
     *
676
     * @throws \Exception
677
     */
678
    protected function manifest($buildDirectory)
679
    {
680
        $path = $this->manifestPath($buildDirectory);
681
 
682
        if (! isset(static::$manifests[$path])) {
683
            if (! is_file($path)) {
684
                throw new Exception("Vite manifest not found at: {$path}");
685
            }
686
 
687
            static::$manifests[$path] = json_decode(file_get_contents($path), true);
688
        }
689
 
690
        return static::$manifests[$path];
691
    }
692
 
693
    /**
694
     * Get the path to the manifest file for the given build directory.
695
     *
696
     * @param  string  $buildDirectory
697
     * @return string
698
     */
699
    protected function manifestPath($buildDirectory)
700
    {
701
        return public_path($buildDirectory.'/'.$this->manifestFilename);
702
    }
703
 
704
    /**
705
     * Get a unique hash representing the current manifest, or null if there is no manifest.
706
     *
707
     * @param  string|null  $buildDirectory
708
     * @return string|null
709
     */
710
    public function manifestHash($buildDirectory = null)
711
    {
712
        $buildDirectory ??= $this->buildDirectory;
713
 
714
        if ($this->isRunningHot()) {
715
            return null;
716
        }
717
 
718
        if (! is_file($path = $this->manifestPath($buildDirectory))) {
719
            return null;
720
        }
721
 
722
        return md5_file($path) ?: null;
723
    }
724
 
725
    /**
726
     * Get the chunk for the given entry point / asset.
727
     *
728
     * @param  array  $manifest
729
     * @param  string  $file
730
     * @return array
731
     *
732
     * @throws \Exception
733
     */
734
    protected function chunk($manifest, $file)
735
    {
736
        if (! isset($manifest[$file])) {
737
            throw new Exception("Unable to locate file in Vite manifest: {$file}.");
738
        }
739
 
740
        return $manifest[$file];
741
    }
742
 
743
    /**
744
     * Determine if the HMR server is running.
745
     *
746
     * @return bool
747
     */
748
    public function isRunningHot()
749
    {
750
        return is_file($this->hotFile());
751
    }
752
 
753
    /**
754
     * Get the Vite tag content as a string of HTML.
755
     *
756
     * @return string
757
     */
758
    public function toHtml()
759
    {
760
        return $this->__invoke($this->entryPoints)->toHtml();
761
    }
762
}