Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ VITE_CACHE_CONFIG=default
```

> [!TIP]
> Caching is disabled by default. Enable it in production for ~5-10x faster manifest reads (~0.1ms vs ~2-7ms per request).
> Enabling caching could improve manifest read performance, particularly when using memory-based solutions such as Redis that avoid file I/O operations.

### Multiple Configurations

Expand Down
20 changes: 13 additions & 7 deletions src/Service/AssetService.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use CakeVite\Enum\ScriptType;
use CakeVite\Exception\ConfigurationException;
use CakeVite\ValueObject\AssetTag;
use CakeVite\ValueObject\ManifestCollection;
use CakeVite\ValueObject\ManifestEntry;
use CakeVite\ValueObject\ViteConfig;

Expand Down Expand Up @@ -111,17 +112,18 @@ private function generateDevelopmentScriptTags(ViteConfig $config, array $option
private function generateProductionScriptTags(ViteConfig $config, array $options): array
{
$env = $this->environmentService->detect($config);
$manifest = $this->manifestService->load($config, $env);
$fullManifest = $this->manifestService->load($config, $env);

// Apply filters if specified
// Apply filters if specified (for selecting which entries to render)
$manifestForEntries = $fullManifest;
if (!empty($options['files'])) {
$manifest = $manifest->filterByPattern($options['files']);
$manifestForEntries = $manifestForEntries->filterByPattern($options['files']);
} elseif (!empty($options['filter'])) {
$manifest = $manifest->filterByPattern($options['filter']);
$manifestForEntries = $manifestForEntries->filterByPattern($options['filter']);
}

// Filter to script entries and sort by load order
$entries = $manifest
$entries = $manifestForEntries
->filterByType(AssetType::Script)
->filterEntries()
->sortByLoadOrder();
Expand All @@ -131,9 +133,10 @@ private function generateProductionScriptTags(ViteConfig $config, array $options
$alreadyPreloaded = [];

// Generate preload tags first (if enabled)
// Use full manifest to resolve import keys
if (!$config->preloadMode->isNone()) {
foreach ($entries as $entry) {
$preloadTags = $this->generatePreloadTags($entry, $config, $alreadyPreloaded);
$preloadTags = $this->generatePreloadTags($entry, $fullManifest, $config, $alreadyPreloaded);
$tags = array_merge($tags, $preloadTags);
}
}
Expand Down Expand Up @@ -163,19 +166,22 @@ private function generateProductionScriptTags(ViteConfig $config, array $options
* Generate preload tags for an entry's imports
*
* @param \CakeVite\ValueObject\ManifestEntry $entry Manifest entry
* @param \CakeVite\ValueObject\ManifestCollection $manifest Full manifest for resolving imports
* @param \CakeVite\ValueObject\ViteConfig $config Configuration
* @param array<string, bool> $alreadyPreloaded Track preloaded URLs to avoid duplicates
* @return array<\CakeVite\ValueObject\AssetTag>
*/
private function generatePreloadTags(
ManifestEntry $entry,
ManifestCollection $manifest,
ViteConfig $config,
array &$alreadyPreloaded,
): array {
$tags = [];
$pluginPrefix = $config->pluginName ? $config->pluginName . '.' : '';

foreach ($entry->getImportUrls() as $importUrl) {
// Resolve import keys to actual file URLs using the manifest
foreach ($manifest->resolveImportUrls($entry->imports) as $importUrl) {
$fullUrl = $pluginPrefix . $importUrl;

if (isset($alreadyPreloaded[$fullUrl])) {
Expand Down
49 changes: 49 additions & 0 deletions src/ValueObject/ManifestCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,53 @@ public function sortByLoadOrder(): self

return new self($items);
}

/**
* Find entry by manifest key
*
* @param string $key Manifest key to search for
* @return \CakeVite\ValueObject\ManifestEntry|null Entry if found, null otherwise
*/
public function findByKey(string $key): ?ManifestEntry
{
foreach ($this as $entry) {
if ($entry->key === $key) {
return $entry;
}
}

return null;
}

/**
* Resolve import keys to file URLs
*
* In Vite's manifest, the `imports` array contains keys that reference
* other entries in the manifest, not direct file paths. This method
* looks up each import key and returns the actual file URL.
*
* Performance: Uses indexBy() for O(n+m) complexity instead of O(n*m)
* with repeated linear searches.
*
* @param array<string> $importKeys Import keys from manifest entry
* @return array<string> Resolved file URLs
*/
public function resolveImportUrls(array $importKeys): array
{
if ($importKeys === []) {
return [];
}

// Create keyed lookup for O(1) access - O(n) operation done once
$keyedManifest = $this->indexBy(fn(ManifestEntry $entry): string => $entry->key)->toArray();

$urls = [];
foreach ($importKeys as $key) {
if (isset($keyedManifest[$key]) && $keyedManifest[$key] instanceof ManifestEntry) {
$urls[] = $keyedManifest[$key]->getUrl();
}
}

return $urls;
}
}
64 changes: 62 additions & 2 deletions src/View/Helper/ViteHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,17 @@ public function script(array|string $options = [], array|ViteConfig|null $config

$tags = $this->getAssetService()->generateScriptTags($config, $options);

// Render script tags
// Render tags (preload tags as link elements, script tags as script elements)
foreach ($tags as $tag) {
$this->Html->script($tag->url, array_merge(['block' => $block], $tag->attributes));
if ($tag->isPreload) {
// Render preload tags as <link rel="modulepreload" href="...">
$preloadType = $tag->preloadType ?? 'modulepreload';
$linkTag = $this->buildPreloadLinkTag($preloadType, $tag->url, $tag->attributes);
$this->getView()->append($block, $linkTag);
} else {
// Render regular script tags
$this->Html->script($tag->url, array_merge(['block' => $block], $tag->attributes));
}
}

// Add dependent CSS if in production
Expand Down Expand Up @@ -310,6 +318,58 @@ private function mergePreloadOption(
return ['preload' => $preloadMode];
}

/**
* Build preload link tag with proper attribute rendering
*
* Supports attributes like crossorigin, integrity, as, etc.
* Filters out 'rel' attribute to avoid conflicts with preloadType.
*
* @param string $preloadType Preload type (modulepreload, preload, etc.)
* @param string $url Asset URL
* @param array<string, mixed> $attributes HTML attributes
* @return string Complete link tag HTML
*/
private function buildPreloadLinkTag(string $preloadType, string $url, array $attributes): string
{
// Filter out 'rel' attribute to avoid conflicts
$attributes = array_filter(
$attributes,
fn($key): bool => $key !== 'rel',
ARRAY_FILTER_USE_KEY,
);

// Build attribute string
$attributesString = '';
foreach ($attributes as $attrName => $attrValue) {
if ($attrValue === null) {
continue;
}

// Handle boolean attributes (e.g., async, defer)
if (is_bool($attrValue)) {
if ($attrValue) {
$attributesString .= ' ' . h((string)$attrName);
}

continue;
}

// Regular attributes with values
$attributesString .= sprintf(
' %s="%s"',
h((string)$attrName),
h((string)$attrValue),
);
}

return sprintf(
'<link rel="%s" href="%s"%s>',
h($preloadType),
h($url),
$attributesString,
);
}

/**
* Get or create asset service
*
Expand Down
13 changes: 8 additions & 5 deletions tests/Fixture/manifest-with-imports.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@
"src": "src/app.ts",
"isEntry": true,
"css": ["assets/app-xyz789.css"],
"imports": ["assets/vendor-def456.js", "assets/utils-ghi789.js"]
"imports": ["_vendor-def456.js", "_utils-ghi789.js"]
},
"assets/vendor-def456.js": {
"_vendor-def456.js": {
"file": "assets/vendor-def456.js",
"src": "_vendor-def456.js",
"isEntry": false,
"imports": ["assets/react-jkl012.js"]
"imports": ["_react-jkl012.js"]
},
"assets/utils-ghi789.js": {
"_utils-ghi789.js": {
"file": "assets/utils-ghi789.js",
"src": "_utils-ghi789.js",
"isEntry": false
},
"assets/react-jkl012.js": {
"_react-jkl012.js": {
"file": "assets/react-jkl012.js",
"src": "_react-jkl012.js",
"isEntry": false
}
}
Loading
Loading