Skip to content

Commit 8801460

Browse files
committed
Add renderStylePreloads method and corresponding Twig function for CSS preload hints
- Implemented renderStylePreloads in ViteManifest to generate <link rel="preload" as="style"> tags for CSS files. - Added vite_css_preload Twig function to utilize the new method. - Updated README and CHANGELOG to reflect these changes. - Added tests for style preload functionality and ensured compatibility with existing features.
1 parent 51196e1 commit 8801460

8 files changed

Lines changed: 145 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

7+
## 1.2.0
8+
- Fix `getStyle()` silently dropping the standalone `.css` output file when a chunk also has a `css[]` array (assignment was overwriting instead of merging)
9+
- Add `renderStylePreloads()` method to `ViteManifest` and `ViteManifestInterface` to emit `<link rel="preload" as="style">` hints for CSS files
10+
- Add `vite_css_preload()` Twig function backed by `renderStylePreloads()`
11+
712
## 1.1.0
813
- Support CSS standalone file by [@imagoiq](https://github.com/imagoiq) in [#1](https://github.com/userfrosting/vite-php-twig/pull/1)
914
- Add PHP 8.4 Tests

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ $manifest->getStyles('views/foo.js'); // Styles
3030
$manifest->getImports('views/foo.js'); // Preloads
3131

3232
// Render HTML tags for `views/foo.js` entry
33-
$manifest->renderScripts('views/foo.js'); // Scripts
34-
$manifest->renderStyles('views/foo.js'); // Styles
35-
$manifest->renderPreloads('views/foo.js'); // Preloads
33+
$manifest->renderScripts('views/foo.js'); // Scripts
34+
$manifest->renderStyles('views/foo.js'); // Styles
35+
$manifest->renderPreloads('views/foo.js'); // JS module preloads
36+
$manifest->renderStylePreloads('views/foo.js'); // CSS preload hints
3637

3738
// If you have multiple entry point scripts on the same page, you should pass them in a single call to avoid duplicates - for example:
3839
$manifest->getScripts('views/foo.js', 'views/bar.js');
@@ -68,8 +69,14 @@ Now, to render all of the `script` and `link` tags for a specific "entry" (e.g.
6869
{{ vite_js('views/foo.js') }}
6970
{{ vite_css('views/foo.js') }}
7071
{{ vite_preload('views/foo.js') }}
72+
{{ vite_css_preload('views/foo.js') }}
7173
```
7274

75+
- `vite_js` — renders `<script type="module">` tags for the entry's JS files.
76+
- `vite_css` — renders `<link rel="stylesheet">` tags for the entry's CSS files.
77+
- `vite_preload` — renders `<link rel="modulepreload">` hints for eagerly fetching imported JS chunks.
78+
- `vite_css_preload` — renders `<link rel="preload" as="style">` hints so the browser discovers CSS files earlier, even though `vite_css` already adds the `<link rel="stylesheet">` tags.
79+
7380
If you have multiple entry point scripts on the same page, you should pass them in a single call to avoid duplicates - for example:
7481
```twig
7582
{{ vite_js('views/foo.js', 'views/bar.js') }}

src/ViteManifest.php

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,31 @@ public function renderPreloads(string ...$entries): string
123123
return implode('', $tags);
124124
}
125125

126+
/**
127+
* Fetches the style files from the manifest for the specified entries and
128+
* returns them as HTML style preload hints. If the dev server is used, no
129+
* preloading tags are returned for CSS coming from JS chunks, since Vite
130+
* injects those at runtime.
131+
*
132+
* @param string ...$entries
133+
*
134+
* @throws JsonException If manifest can't be read
135+
* @throws EntrypointNotFoundException If an entry point is not found in the
136+
* manifest.
137+
*
138+
* @return string The CSS preload tags
139+
*/
140+
public function renderStylePreloads(string ...$entries): string
141+
{
142+
$styles = $this->getStyles(...$entries);
143+
144+
$tags = array_map(function (string $file) {
145+
return $this->renderStylePreload($file);
146+
}, $styles);
147+
148+
return implode('', $tags);
149+
}
150+
126151
/**
127152
* Fetches and returns all script files for the specified entry points, or
128153
* the server config if development server is activated.
@@ -273,7 +298,7 @@ protected function getStyle(string $entry): array
273298

274299
// Add entry point chunk's css list
275300
if (array_key_exists('css', $chunk)) {
276-
$files = $chunk['css'];
301+
$files = array_merge($files, $chunk['css']);
277302
}
278303

279304
// Recursively follow all chunks in the entry point's imports list
@@ -428,6 +453,21 @@ protected function renderPreload(string $fileName): string
428453
);
429454
}
430455

456+
/**
457+
* Return style preload tag for specified filename.
458+
*
459+
* @param string $fileName
460+
*
461+
* @return string
462+
*/
463+
protected function renderStylePreload(string $fileName): string
464+
{
465+
return sprintf(
466+
'<link rel="preload" as="style" href="%s" />',
467+
$fileName
468+
);
469+
}
470+
431471
/**
432472
* Prefixes a file name with either the dev server URL or the base path,
433473
* depending on whether the dev server is used or not.

src/ViteManifestInterface.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ public function renderStyles(string ...$entries): string;
5050
*/
5151
public function renderPreloads(string ...$entries): string;
5252

53+
/**
54+
* Fetches the style files from the manifest for the specified entries and
55+
* returns them as HTML style preload hints. If the dev server is used, no
56+
* preloading tags are returned for CSS coming from JS chunks, since Vite
57+
* injects those at runtime.
58+
*
59+
* @param string ...$entries
60+
*
61+
* @return string The CSS preload tags
62+
*/
63+
public function renderStylePreloads(string ...$entries): string;
64+
5365
/**
5466
* Fetches and returns all script files for the specified entry points, or
5567
* the server config if development server is activated.

src/ViteTwigExtension.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
* - vite_js(string $entryName)
2323
* - vite_css(string $entryName)
2424
* - vite_preload(string $entryName)
25+
* - vite_css_preload(string $entryName)
2526
*
2627
* @see https://vitejs.dev/guide/backend-integration
2728
*/
@@ -44,6 +45,7 @@ public function getFunctions(): array
4445
new TwigFunction('vite_js', [$this->manifest, 'renderScripts'], ['is_safe' => ['html']]),
4546
new TwigFunction('vite_css', [$this->manifest, 'renderStyles'], ['is_safe' => ['html']]),
4647
new TwigFunction('vite_preload', [$this->manifest, 'renderPreloads'], ['is_safe' => ['html']]),
48+
new TwigFunction('vite_css_preload', [$this->manifest, 'renderStylePreloads'], ['is_safe' => ['html']]),
4749
];
4850
}
4951
}

tests/ViteManifestTest.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,4 +284,51 @@ public function testPreloadTags(): void
284284
$result = $manifest->renderPreloads('views/bar.js');
285285
$this->assertSame(implode('', $expected), $result);
286286
}
287+
288+
// Bug 1 — getStyle() must not drop the standalone .css file when css[] is also present
289+
public function testGetStylesWithStandaloneAndCssArray(): void
290+
{
291+
$manifest = new ViteManifest($this->manifestFile);
292+
293+
$this->assertSame(
294+
['assets/mixed-standalone.css', 'assets/mixed-extra.css'],
295+
$manifest->getStyles('views/mixed.js')
296+
);
297+
}
298+
299+
// Bug 2 — renderStylePreloads() must emit <link rel="preload" as="style"> tags
300+
public function testStylePreloadTags(): void
301+
{
302+
$manifest = new ViteManifest($this->manifestFile);
303+
304+
// views/foo.js has css[] and an import that also has css[]
305+
$expected = implode('', [
306+
'<link rel="preload" as="style" href="assets/foo-5UjPuW-k.css" />',
307+
'<link rel="preload" as="style" href="assets/shared-ChJ_j-JJ.css" />',
308+
]);
309+
$this->assertSame($expected, $manifest->renderStylePreloads('views/foo.js'));
310+
311+
// views/bar.js inherits css only via its import
312+
$expected = '<link rel="preload" as="style" href="assets/shared-ChJ_j-JJ.css" />';
313+
$this->assertSame($expected, $manifest->renderStylePreloads('views/bar.js'));
314+
315+
// Entry with both standalone .css file and css[] must include both
316+
$expected = implode('', [
317+
'<link rel="preload" as="style" href="assets/mixed-standalone.css" />',
318+
'<link rel="preload" as="style" href="assets/mixed-extra.css" />',
319+
]);
320+
$this->assertSame($expected, $manifest->renderStylePreloads('views/mixed.js'));
321+
}
322+
323+
public function testStylePreloadTagsDevServer(): void
324+
{
325+
$manifest = new ViteManifest(
326+
manifestPath: $this->manifestFile,
327+
devEnabled: true,
328+
);
329+
330+
// In dev mode getStyles() returns no CSS for JS entries, so no preload tags
331+
$this->assertSame('', $manifest->renderStylePreloads('views/foo.js'));
332+
$this->assertSame('', $manifest->renderStylePreloads('views/bar.js'));
333+
}
287334
}

tests/ViteTwigExtensionTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,27 @@ public function testPreloadTags(): void
113113
$this->assertSame(implode('', $expected), $result);
114114
}
115115

116+
public function testCssPreloadTags(): void
117+
{
118+
// views/foo.js
119+
$expected = implode('', [
120+
'<link rel="preload" as="style" href="assets/foo-5UjPuW-k.css" />',
121+
'<link rel="preload" as="style" href="assets/shared-ChJ_j-JJ.css" />',
122+
]);
123+
$this->assertSame($expected, $this->twig->createTemplate("{{ vite_css_preload('views/foo.js') }}")->render());
124+
125+
// views/bar.js
126+
$expected = '<link rel="preload" as="style" href="assets/shared-ChJ_j-JJ.css" />';
127+
$this->assertSame($expected, $this->twig->createTemplate("{{ vite_css_preload('views/bar.js') }}")->render());
128+
129+
// Entry with both standalone .css file and css[] must include both
130+
$expected = implode('', [
131+
'<link rel="preload" as="style" href="assets/mixed-standalone.css" />',
132+
'<link rel="preload" as="style" href="assets/mixed-extra.css" />',
133+
]);
134+
$this->assertSame($expected, $this->twig->createTemplate("{{ vite_css_preload('views/mixed.js') }}")->render());
135+
}
136+
116137
public function testDevServer(): void
117138
{
118139
$manifest = new ViteManifest(
@@ -142,5 +163,6 @@ public function testDevServer(): void
142163
// No styles or imports
143164
$this->assertSame('', $twig->createTemplate("{{ vite_css('views/foo.js') }}")->render());
144165
$this->assertSame('', $twig->createTemplate("{{ vite_preload('views/bar.js') }}")->render());
166+
$this->assertSame('', $twig->createTemplate("{{ vite_css_preload('views/foo.js') }}")->render());
145167
}
146168
}

tests/manifests/manifest.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,11 @@
4949
"file": "standalone-DEF.css",
5050
"src": "standalone.less",
5151
"isEntry": true
52+
},
53+
"views/mixed.js": {
54+
"file": "assets/mixed-standalone.css",
55+
"src": "views/mixed.js",
56+
"isEntry": true,
57+
"css": ["assets/mixed-extra.css"]
5258
}
5359
}

0 commit comments

Comments
 (0)