Skip to content

Commit

Permalink
add tag renderer tests
Browse files Browse the repository at this point in the history
  • Loading branch information
lhapaipai committed Oct 30, 2023
1 parent 40cb64e commit 14b7d6a
Show file tree
Hide file tree
Showing 15 changed files with 580 additions and 222 deletions.
2 changes: 1 addition & 1 deletion .phpunit.result.cache
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"version":1,"defects":[],"times":{"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testRenderTagBasic":1.815}}
{"version":1,"defects":{"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testRenderTag":3,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testGenerateTagWithCustomAttributes":3,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testGenerateTagWithCustomAttributes with data set #0":3,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testGenerateTagWithCustomAttributes with data set #5":3,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testGenerateScript with data set #0":3,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testGenerateScript with data set #1":3,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testGenerateLinkStylesheet with data set #0":3,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testGenerateLinkStylesheet with data set #1":3,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testSpecialTag":3,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testGenerateLinkPreload with data set #0":3,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testGenerateLinkPreload with data set #1":3},"times":{"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testRenderTagBasic":2.477,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testRenderTagBasic1":0,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testRenderTag":0.004,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testGenerateTagWithCustomAttributes":0.004,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testGenerateTagWithCustomAttributes with data set #0":0.003,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testGenerateTagWithCustomAttributes with data set #1":0,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testGenerateTagWithCustomAttributes with data set #2":0,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testGenerateTagWithCustomAttributes with data set #3":0,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testGenerateTagWithCustomAttributes with data set #4":0,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testGenerateTagWithCustomAttributes with data set #5":0,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testGenerateTagWithGlobalAttributes with data set #0":0,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testGenerateScript with data set #0":0,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testGenerateScript with data set #1":0,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testGenerateScript with data set #2":0,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testGenerateLinkStylesheet with data set #0":0,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testGenerateLinkStylesheet with data set #1":0,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testSpecialTag":0,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testGenerateLinkPreload with data set #0":0,"Pentatrion\\ViteBundle\\Tests\\Asset\\TagRendererTest::testGenerateLinkPreload with data set #1":0}}
211 changes: 123 additions & 88 deletions src/Asset/EntrypointRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@

namespace Pentatrion\ViteBundle\Asset;

use Pentatrion\ViteBundle\Event\RenderAssetTagEvent;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

class EntrypointRenderer
{
private EntrypointsLookupCollection $entrypointsLookupCollection;
private TagRendererCollection $tagRendererCollection;
private bool $useAbsoluteUrl;
private RouterInterface $router;
private EventDispatcherInterface $eventDispatcher;

private $returnedViteClients = [];
private $returnedReactRefresh = [];
private $returnedViteClient = false;
private $returnedReactRefresh = false;
private $returnedPreloadedScripts = [];

private $hasReturnedViteLegacyScripts = false;
Expand All @@ -21,12 +24,14 @@ public function __construct(
EntrypointsLookupCollection $entrypointsLookupCollection,
TagRendererCollection $tagRendererCollection,
bool $useAbsoluteUrl,
RouterInterface $router = null
RouterInterface $router = null,
EventDispatcherInterface $eventDispatcher = null
) {
$this->entrypointsLookupCollection = $entrypointsLookupCollection;
$this->tagRendererCollection = $tagRendererCollection;
$this->useAbsoluteUrl = $useAbsoluteUrl;
$this->router = $router;
$this->eventDispatcher = $eventDispatcher;
}

private function getEntrypointsLookup(string $configName = null): EntrypointsLookup
Expand All @@ -39,8 +44,46 @@ private function getTagRenderer(string $configName = null): TagRenderer
return $this->tagRendererCollection->getTagRenderer($configName);
}

public function renderScripts(string $entryName, array $options = [], $configName = null): string
private function completeURL(string $path, $useAbsoluteUrl = false)
{
if (false === $useAbsoluteUrl || null === $this->router) {
return $path;
}

return $this->router->getContext()->getScheme().'://'.$this->router->getContext()->getHost().$path;
}

private function shouldUseAbsoluteURL(array $options, $configName)
{
$viteServer = $this->getEntrypointsLookup($configName)->getViteServer($configName);

return false === $viteServer && ($this->useAbsoluteUrl || (isset($options['absolute_url']) && true === $options['absolute_url']));
}

public function getMode(string $configName = null): ?string
{
$entrypointsLookup = $this->getEntrypointsLookup($configName);

if (!$entrypointsLookup->hasFile($configName)) {
return null;
}

return $entrypointsLookup->isBuild() ? 'build' : 'dev';
}

public function reset()
{
// resets the state of this service
$this->returnedViteClient = false;
$this->returnedReactRefresh = false;
}

public function renderScripts(
string $entryName,
array $options = [],
$configName = null,
$toString = true
): string {
$entrypointsLookup = $this->getEntrypointsLookup($configName);
$tagRenderer = $this->getTagRenderer($configName);

Expand All @@ -50,97 +93,89 @@ public function renderScripts(string $entryName, array $options = [], $configNam

$useAbsoluteUrl = $this->shouldUseAbsoluteURL($options, $configName);

$content = [];
$tags = [];
$viteServer = $entrypointsLookup->getViteServer();
$isBuild = $entrypointsLookup->isBuild();

if (false !== $viteServer) {
// vite server is active
if (!isset($this->returnedViteClients[$configName])) {
$content[] = $tagRenderer->renderTag('script', [
'type' => 'module',
'src' => $viteServer['origin'].$viteServer['base'].'@vite/client',
]);
$this->returnedViteClients[$configName] = true;
if (!$this->returnedViteClient) {
$tags[] = $tagRenderer->createViteClientScript($viteServer['origin'].$viteServer['base'].'@vite/client');
$this->returnedViteClient = true;
}
if (!isset($this->returnedReactRefresh[$configName]) && isset($options['dependency']) && 'react' === $options['dependency']) {
$content[] = $tagRenderer->renderReactRefreshInline($viteServer['origin'].$viteServer['base']);
$this->returnedReactRefresh[$configName] = true;

if (
!$this->returnedReactRefresh
&& isset($options['dependency']) && 'react' === $options['dependency']
) {
$tags[] = $tagRenderer->createReactRefreshScript($viteServer['origin'].$viteServer['base']);

$this->$this->returnedReactRefresh = true;
}
} elseif (
$entrypointsLookup->isLegacyPluginEnabled()
&& !$this->hasReturnedViteLegacyScripts
) {
/* legacy section when vite server is inactive */

/* set or not the __vite_is_modern_browser variable */
$content[] = $tagRenderer::DETECT_MODERN_BROWSER_CODE;

/* if your browser understands the modules but not dynamic import,
* load the legacy entrypoints
*/
$content[] = $tagRenderer::DYNAMIC_FALLBACK_INLINE_CODE;

/* Safari 10.1 supports modules, but does not support the `nomodule` attribute
* it will load <script nomodule> anyway */
$content[] = $tagRenderer::SAFARI10_NO_MODULE_FIX;
$tags[] = $tagRenderer->createDetectModernBrowserScript();
$tags[] = $tagRenderer->createDynamicFallbackScript();
$tags[] = $tagRenderer->createSafariNoModuleScript();

foreach ($entrypointsLookup->getJSFiles('polyfills-legacy') as $fileWithHash) {
// normally only one js file
$content[] = $tagRenderer->renderTag('script', [
'nomodule' => true,
'crossorigin' => true,
'src' => $this->completeURL($fileWithHash['path'], $useAbsoluteUrl),
'id' => 'vite-legacy-polyfill',
]);
$tags[] = $tagRenderer->createScriptTag(
[
'nomodule' => true,
'crossorigin' => true,
'src' => $this->completeURL($fileWithHash['path'], $useAbsoluteUrl),
'id' => 'vite-legacy-polyfill',
]
);
}
$this->hasReturnedViteLegacyScripts = true;
}

/* normal js scripts */
foreach ($entrypointsLookup->getJSFiles($entryName) as $fileWithHash) {
$attributes = array_merge([
'type' => 'module',
'src' => $this->completeURL($fileWithHash['path'], $useAbsoluteUrl),
'integrity' => $fileWithHash['hash'],
], $options['attr'] ?? []);
$content[] = $tagRenderer->renderScriptFile($attributes, '', $isBuild);
$tags[] = $tagRenderer->createScriptTag(
array_merge(
[
'type' => 'module',
'src' => $this->completeURL($fileWithHash['path'], $useAbsoluteUrl),
'integrity' => $fileWithHash['hash'],
],
$options['attr'] ?? []
)
);
}

/* legacy js scripts */
if ($entrypointsLookup->hasLegacy($entryName)) {
$id = self::pascalToKebab("vite-legacy-entry-$entryName");

$content[] = $tagRenderer->renderScriptFile([
'nomodule' => true,
'data-src' => $this->completeURL($entrypointsLookup->getLegacyJSFile($entryName), $useAbsoluteUrl),
'id' => $id,
'crossorigin' => true,
'class' => 'vite-legacy-entry',
], $tagRenderer->getSystemJSInlineCode($id), $isBuild);
}

return implode(PHP_EOL, $content);
}

private function completeURL(string $path, $useAbsoluteUrl = false)
{
if (false === $useAbsoluteUrl || null === $this->router) {
return $path;
$file = $entrypointsLookup->getLegacyJSFile($entryName);
$tags[] = $tagRenderer->createScriptTag(
[
'nomodule' => true,
'data-src' => $this->completeURL($file['path'], $useAbsoluteUrl),
'id' => $id,
'crossorigin' => true,
'class' => 'vite-legacy-entry',
'integrity' => $file['hash'],
],
InlineContent::getSystemJSInlineCode($id)
);
}

return $this->router->getContext()->getScheme().'://'.$this->router->getContext()->getHost().$path;
}

private function shouldUseAbsoluteURL(array $options, $configName)
{
$viteServer = $this->getEntrypointsLookup($configName)->getViteServer($configName);

return false === $viteServer && ($this->useAbsoluteUrl || (isset($options['absolute_url']) && true === $options['absolute_url']));
return $this->renderTags($tags, $isBuild, $toString);
}

public function renderLinks(string $entryName, array $options = [], $configName = null): string
{
public function renderLinks(
string $entryName,
array $options = [],
$configName = null,
$toString = true
): string {
$entrypointsLookup = $this->getEntrypointsLookup($configName);
$tagRenderer = $this->getTagRenderer($configName);

Expand All @@ -151,20 +186,22 @@ public function renderLinks(string $entryName, array $options = [], $configName
$useAbsoluteUrl = $this->shouldUseAbsoluteURL($options, $configName);
$isBuild = $entrypointsLookup->isBuild();

$content = [];
$tags = [];

foreach ($entrypointsLookup->getCSSFiles($entryName) as $fileWithHash) {
$content[] = $tagRenderer->renderLinkStylesheet($this->completeURL($fileWithHash['path'], $useAbsoluteUrl), array_merge([
'integrity' => $fileWithHash['hash'],
], $options['attr'] ?? []), $isBuild);
$tags[] = $tagRenderer->createLinkStylesheetTag(
$this->completeURL($fileWithHash['path'], $useAbsoluteUrl),
array_merge(['integrity' => $fileWithHash['hash']], $options['attr'] ?? [])
);
}

if ($isBuild) {
foreach ($entrypointsLookup->getJavascriptDependencies($entryName) as $fileWithHash) {
if (false === \in_array($fileWithHash['path'], $this->returnedPreloadedScripts, true)) {
$content[] = $tagRenderer->renderLinkPreload($this->completeURL($fileWithHash['path'], $useAbsoluteUrl), [
'integrity' => $fileWithHash['hash'],
], $isBuild);
$tags[] = $tagRenderer->createModulePreloadLinkTag(
$this->completeURL($fileWithHash['path'], $useAbsoluteUrl),
['integrity' => $fileWithHash['hash']]
);
$this->returnedPreloadedScripts[] = $fileWithHash['path'];
}
}
Expand All @@ -173,33 +210,31 @@ public function renderLinks(string $entryName, array $options = [], $configName
if ($isBuild && isset($options['preloadDynamicImports']) && true === $options['preloadDynamicImports']) {
foreach ($entrypointsLookup->getJavascriptDynamicDependencies($entryName) as $fileWithHash) {
if (false === \in_array($fileWithHash['path'], $this->returnedPreloadedScripts, true)) {
$content[] = $tagRenderer->renderLinkPreload($this->completeURL($fileWithHash['path'], $useAbsoluteUrl), [
'integrity' => $fileWithHash['hash'],
], $isBuild);
$tags[] = $tagRenderer->createModulePreloadLinkTag(
$this->completeURL($fileWithHash['path'], $useAbsoluteUrl),
['integrity' => $fileWithHash['hash']]
);
$this->returnedPreloadedScripts[] = $fileWithHash['path'];
}
}
}

return implode(PHP_EOL, $content);
return $this->renderTags($tags, $isBuild, $toString);
}

public function getMode(string $configName = null): ?string
public function renderTags(array $tags, $isBuild, $toString)
{
$entrypointsLookup = $this->getEntrypointsLookup($configName);

if (!$entrypointsLookup->hasFile($configName)) {
return null;
if (null !== $this->eventDispatcher) {
foreach ($tags as $tag) {
$this->eventDispatcher->dispatch(new RenderAssetTagEvent($isBuild, $tag));
}
}

return $entrypointsLookup->isBuild() ? 'build' : 'dev';
}

public function reset()
{
// resets the state of this service
$this->returnedViteClients = [];
$this->returnedReactRefresh = [];
return $toString
? implode(PHP_EOL, array_map(function ($tagEvent) {
return TagRenderer::generateTag($tagEvent);
}, $tags))
: $tags;
}

public static function pascalToKebab(string $str): string
Expand Down
2 changes: 1 addition & 1 deletion src/Asset/EntrypointsLookup.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public function getLegacyJSFile($entryName): string

$legacyEntryName = $entryInfos['entryPoints'][$entryName]['legacy'];

return $entryInfos['entryPoints'][$legacyEntryName]['js'][0]['path'];
return $entryInfos['entryPoints'][$legacyEntryName]['js'][0];
}

private function throwIfEntrypointIsMissing(string $entryName): void
Expand Down
56 changes: 56 additions & 0 deletions src/Asset/InlineContent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace Pentatrion\ViteBundle\Asset;

class InlineContent
{
/* Safari 10.1 supports modules, but does not support the `nomodule` attribute
* it will load <script nomodule> anyway
* https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc
*/
public const SAFARI10_NO_MODULE_FIX_INLINE_CODE = '!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",(function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()}),!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();';

/* set or not the __vite_is_modern_browser variable */
public const DETECT_MODERN_BROWSER_INLINE_CODE = 'try{import.meta.url;import("_").catch(()=>1);}catch(e){}window.__vite_is_modern_browser=true;';

/* if your browser understands the modules but not dynamic import,
* load the legacy entrypoints
*
* load the <script nomodule crossorigin id="vite-legacy-polyfill" src="..."></script>
* and the <script nomodule crossorigin id="vite-legacy-entry" data-src="..."></script>
* if browser accept modules but don't dynamic import or import.meta
*/
public const DYNAMIC_FALLBACK_INLINE_CODE = <<<INLINE
\n (function() {
if (window.__vite_is_modern_browser) return;
console.warn("vite: loading legacy build because dynamic import or import.meta.url is unsupported, syntax error above should be ignored");
var legacyPolyfill = document.getElementById("vite-legacy-polyfill")
var script = document.createElement("script");
script.src = legacyPolyfill.src;
script.onload = function() {
document.querySelectorAll("script.vite-legacy-entry").forEach(function(elt) {
System.import(elt.getAttribute("data-src"));
});
};
document.body.appendChild(script);
})();\n
INLINE;

public static function getSystemJSInlineCode($id): string
{
$content = 'System.import(document.getElementById("__ID__").getAttribute("data-src"))';

return str_replace('__ID__', $id, $content);
}

public static function getReactRefreshInlineCode(string $devServerUrl)
{
return <<<INLINE
\n import RefreshRuntime from "$devServerUrl@react-refresh"
RefreshRuntime.injectIntoGlobalHook(window)
window.\$RefreshReg$ = () => {}
window.\$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true\n
INLINE;
}
}
Loading

0 comments on commit 14b7d6a

Please sign in to comment.