Skip to content

Commit baf526e

Browse files
jessarchertimacdonaldlucasmichot
authored
[9.x] Vite (#42785)
* Add Vite helpers * Remove vite helper * Add @viteReactRefresh directive * Add docblock * Remove unused import * Automatically clean up after tests * Use ASSET_URL when resolving assets * support default entrypoint * update docblock * Linting * Support CSS entry points * Remove default entry points Blade apps and SPAs have different entry point requirements so there is no sensible default. * Fix test namespace Co-authored-by: Lucas Michot <513603+lucasmichot@users.noreply.github.com> * Add missing import Co-authored-by: Tim MacDonald <hello@timacdonald.me> Co-authored-by: Lucas Michot <513603+lucasmichot@users.noreply.github.com>
1 parent 63e22b0 commit baf526e

File tree

6 files changed

+418
-1
lines changed

6 files changed

+418
-1
lines changed

src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,18 @@
44

55
use Closure;
66
use Illuminate\Foundation\Mix;
7+
use Illuminate\Foundation\Vite;
78
use Mockery;
89

910
trait InteractsWithContainer
1011
{
12+
/**
13+
* The original Vite handler.
14+
*
15+
* @var \Illuminate\Foundation\Vite|null
16+
*/
17+
protected $originalVite;
18+
1119
/**
1220
* The original Laravel Mix handler.
1321
*
@@ -90,6 +98,38 @@ protected function forgetMock($abstract)
9098
return $this;
9199
}
92100

101+
/**
102+
* Register an empty handler for Vite in the container.
103+
*
104+
* @return $this
105+
*/
106+
protected function withoutVite()
107+
{
108+
if ($this->originalVite == null) {
109+
$this->originalVite = app(Vite::class);
110+
}
111+
112+
$this->swap(Vite::class, function () {
113+
return '';
114+
});
115+
116+
return $this;
117+
}
118+
119+
/**
120+
* Restore Vite in the container.
121+
*
122+
* @return $this
123+
*/
124+
protected function withVite()
125+
{
126+
if ($this->originalVite) {
127+
$this->app->instance(Vite::class, $this->originalVite);
128+
}
129+
130+
return $this;
131+
}
132+
93133
/**
94134
* Register an empty handler for Laravel Mix in the container.
95135
*
@@ -109,7 +149,7 @@ protected function withoutMix()
109149
}
110150

111151
/**
112-
* Register an empty handler for Laravel Mix in the container.
152+
* Restore Laravel Mix in the container.
113153
*
114154
* @return $this
115155
*/

src/Illuminate/Foundation/Vite.php

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
<?php
2+
3+
namespace Illuminate\Foundation;
4+
5+
use Exception;
6+
use Illuminate\Support\HtmlString;
7+
use Illuminate\Support\Str;
8+
9+
class Vite
10+
{
11+
/**
12+
* Generate Vite tags for an entrypoint.
13+
*
14+
* @param string|string[] $entrypoints
15+
* @param string $buildDirectory
16+
* @return \Illuminate\Support\HtmlString
17+
*
18+
* @throws \Exception
19+
*/
20+
public function __invoke($entrypoints, $buildDirectory = 'build')
21+
{
22+
static $manifests = [];
23+
24+
$entrypoints = collect($entrypoints);
25+
$buildDirectory = Str::start($buildDirectory, '/');
26+
27+
if (is_file(public_path('/hot'))) {
28+
$url = rtrim(file_get_contents(public_path('/hot')));
29+
30+
return new HtmlString(
31+
$entrypoints
32+
->map(fn ($entrypoint) => $this->makeTag("{$url}/{$entrypoint}"))
33+
->prepend($this->makeScriptTag("{$url}/@vite/client"))
34+
->join('')
35+
);
36+
}
37+
38+
$manifestPath = public_path($buildDirectory.'/manifest.json');
39+
40+
if (! isset($manifests[$manifestPath])) {
41+
if (! is_file($manifestPath)) {
42+
throw new Exception("Vite manifest not found at: {$manifestPath}");
43+
}
44+
45+
$manifests[$manifestPath] = json_decode(file_get_contents($manifestPath), true);
46+
}
47+
48+
$manifest = $manifests[$manifestPath];
49+
50+
$tags = collect();
51+
52+
foreach ($entrypoints as $entrypoint) {
53+
if (! isset($manifest[$entrypoint])) {
54+
throw new Exception("Unable to locate file in Vite manifest: {$entrypoint}.");
55+
}
56+
57+
$tags->push($this->makeTag(asset("{$buildDirectory}/{$manifest[$entrypoint]['file']}")));
58+
59+
if (isset($manifest[$entrypoint]['css'])) {
60+
foreach ($manifest[$entrypoint]['css'] as $css) {
61+
$tags->push($this->makeStylesheetTag(asset("{$buildDirectory}/{$css}")));
62+
}
63+
}
64+
65+
if (isset($manifest[$entrypoint]['imports'])) {
66+
foreach ($manifest[$entrypoint]['imports'] as $import) {
67+
if (isset($manifest[$import]['css'])) {
68+
foreach ($manifest[$import]['css'] as $css) {
69+
$tags->push($this->makeStylesheetTag(asset("{$buildDirectory}/{$css}")));
70+
}
71+
}
72+
}
73+
}
74+
}
75+
76+
[$stylesheets, $scripts] = $tags->partition(fn ($tag) => str_starts_with($tag, '<link'));
77+
78+
return new HtmlString($stylesheets->join('').$scripts->join(''));
79+
}
80+
81+
/**
82+
* Generate React refresh runtime script.
83+
*
84+
* @return \Illuminate\Support\HtmlString|void
85+
*/
86+
public function reactRefresh()
87+
{
88+
if (! is_file(public_path('/hot'))) {
89+
return;
90+
}
91+
92+
$url = rtrim(file_get_contents(public_path('/hot')));
93+
94+
return new HtmlString(
95+
sprintf(
96+
<<<'HTML'
97+
<script type="module">
98+
import RefreshRuntime from '%s/@react-refresh'
99+
RefreshRuntime.injectIntoGlobalHook(window)
100+
window.$RefreshReg$ = () => {}
101+
window.$RefreshSig$ = () => (type) => type
102+
window.__vite_plugin_react_preamble_installed__ = true
103+
</script>
104+
HTML,
105+
$url
106+
)
107+
);
108+
}
109+
110+
/**
111+
* Generate an appropriate tag for the given URL.
112+
*
113+
* @param string $url
114+
* @return string
115+
*/
116+
protected function makeTag($url)
117+
{
118+
if ($this->isCssPath($url)) {
119+
return $this->makeStylesheetTag($url);
120+
}
121+
122+
return $this->makeScriptTag($url);
123+
}
124+
125+
/**
126+
* Generate a script tag for the given URL.
127+
*
128+
* @param string $url
129+
* @return string
130+
*/
131+
protected function makeScriptTag($url)
132+
{
133+
return sprintf('<script type="module" src="%s"></script>', $url);
134+
}
135+
136+
/**
137+
* Generate a stylesheet tag for the given URL.
138+
*
139+
* @param string $url
140+
* @return string
141+
*/
142+
protected function makeStylesheetTag($url)
143+
{
144+
return sprintf('<link rel="stylesheet" href="%s" />', $url);
145+
}
146+
147+
/**
148+
* Determine whether the given path is a CSS file.
149+
*
150+
* @param string $path
151+
* @return bool
152+
*/
153+
protected function isCssPath($path)
154+
{
155+
return preg_match('/\.(css|less|sass|scss|styl|stylus|pcss|postcss)$/', $path) === 1;
156+
}
157+
}

src/Illuminate/View/Compilers/Concerns/CompilesHelpers.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Illuminate\View\Compilers\Concerns;
44

5+
use Illuminate\Foundation\Vite;
6+
57
trait CompilesHelpers
68
{
79
/**
@@ -46,4 +48,31 @@ protected function compileMethod($method)
4648
{
4749
return "<?php echo method_field{$method}; ?>";
4850
}
51+
52+
/**
53+
* Compile the "vite" statements into valid PHP.
54+
*
55+
* @param ?string $arguments
56+
* @return string
57+
*/
58+
protected function compileVite($arguments)
59+
{
60+
$arguments ??= '()';
61+
62+
$class = Vite::class;
63+
64+
return "<?php echo app('$class'){$arguments}; ?>";
65+
}
66+
67+
/**
68+
* Compile the "viteReactRefresh" statements into valid PHP.
69+
*
70+
* @return string
71+
*/
72+
protected function compileViteReactRefresh()
73+
{
74+
$class = Vite::class;
75+
76+
return "<?php echo app('$class')->reactRefresh(); ?>";
77+
}
4978
}

0 commit comments

Comments
 (0)