From ce5a06fef4986487e074359e94aa93425b2773df Mon Sep 17 00:00:00 2001 From: Guido Scialfa Date: Sat, 1 Nov 2025 18:19:57 +0100 Subject: [PATCH 1/4] Add support for manifest configuration with advanced asset options - Refactored `WebpackManifestLoader` and `AbstractWebpackLoader` to handle array-based manifest entries with custom configuration. - Updated `AssetFactory` to support type-safe configurations combining `AssetConfig` and `AssetExtensionConfig`. - Introduced new test cases for custom manifest configurations. - Enhanced documentation to detail alternative manifest formats and examples. --- docs/loaders.md | 72 +++++++++- src/AssetFactory.php | 1 - src/Loader/AbstractWebpackLoader.php | 46 +++--- src/Loader/WebpackManifestLoader.php | 131 ++++++++++++++++-- .../Unit/Loader/WebpackManifestLoaderTest.php | 44 ++++++ 5 files changed, 265 insertions(+), 29 deletions(-) diff --git a/docs/loaders.md b/docs/loaders.md index 0d2869e..f7c871f 100644 --- a/docs/loaders.md +++ b/docs/loaders.md @@ -32,7 +32,7 @@ The [webpack-manifest-plugin](https://www.npmjs.com/package/webpack-manifest-plu } ``` -To load this file in your application you can do following: +To load this file in your application, you can do the following: ```php load('manifest.json'); ``` -If the Asset URL needs to be changed, you can use following: +If the Asset URL needs to be changed, you can use the following: ```php { + return files.reduce((manifest, { name, path }) => { + const cleanName = name.replace(/\.js$/, ''); + switch(cleanName) { + case 'handle-name': + manifest[cleanName] = { + filePath: path, + location: ['frontend'], + enqueue: true, + inFooter: true, + version: '1.0.0', + condition: '', + attributes: { + async: false, + defer: false + } + }; + return manifest; + default: + manifest[cleanName] = path; + return manifest; + } + }, seed); + }, + + // ... other options +}) +``` + +Note: An alternative could be to have a `json` file from which retrieve the configuration for each handle. + ### `EncoreEntrypointsLoader` [Symfony Webpack Encore](https://symfony.com/doc/current/frontend.html) provides a custom implementation of the [assets-webpack-plugin](https://www.npmjs.com/package/assets-webpack-plugin) which groups asset chunks into a single array for a given handle. diff --git a/src/AssetFactory.php b/src/AssetFactory.php index af55780..ce1c077 100644 --- a/src/AssetFactory.php +++ b/src/AssetFactory.php @@ -25,7 +25,6 @@ * filePath?: string, * version?: string, * enqueue?: bool, - * version?: string, * handler?: class-string|class-string|class-string, * location?: AssetLocation, * condition?: string, diff --git a/src/Loader/AbstractWebpackLoader.php b/src/Loader/AbstractWebpackLoader.php index 913c76b..c04a8f7 100644 --- a/src/Loader/AbstractWebpackLoader.php +++ b/src/Loader/AbstractWebpackLoader.php @@ -108,29 +108,16 @@ private function getJSONErrorMessage(int $errorCode): string */ protected function buildAsset(string $handle, string $fileUrl, string $filePath): ?Asset { - $extensionsToClass = [ - 'css' => Style::class, - 'js' => Script::class, - 'mjs' => ScriptModule::class, - 'module.js' => ScriptModule::class, - ]; - /** @var array{filename?:string, extension?:string} $pathInfo */ $pathInfo = pathinfo($filePath); - $baseName = $pathInfo['basename'] ?? ''; $filename = $pathInfo['filename'] ?? ''; - $extension = $pathInfo['extension'] ?? ''; - if (self::isModule($baseName)) { - $extension = 'module.js'; - } + $class = self::resolveClassByExtension($filePath); - if (!in_array($extension, array_keys($extensionsToClass), true)) { + if (!$class) { return null; } - $class = $extensionsToClass[$extension]; - /** @var Style|Script|ScriptModule $asset */ $asset = new $class($handle, $fileUrl, $this->resolveLocation($filename)); $asset->withFilePath($filePath); @@ -145,6 +132,33 @@ protected function buildAsset(string $handle, string $fileUrl, string $filePath) return $asset; } + protected function resolveClassByExtension(string $filePath): ?string + { + $extensionsToClass = [ + 'css' => Style::class, + 'js' => Script::class, + 'mjs' => ScriptModule::class, + 'module.js' => ScriptModule::class, + ]; + + // TODO Maybe make use of \SplFileInfo since it's typed and we can share it + // we have to just make a factory method and that's it. + /** @var array{filename?:string, extension?:string} $pathInfo */ + $pathInfo = pathinfo($filePath); + $baseName = $pathInfo['basename'] ?? ''; + $extension = $pathInfo['extension'] ?? ''; + + if (self::isModule($baseName)) { + $extension = 'module.js'; + } + + if (!in_array($extension, array_keys($extensionsToClass), true)) { + return null; + } + + return $extensionsToClass[$extension]; + } + protected static function isModule(string $fileName): bool { // TODO replace it with `str_ends_with` once dropping support for php 7.4 @@ -168,7 +182,7 @@ protected static function isModule(string $fileName): bool */ protected function sanitizeFileName(string $file): string { - // Check, if the given "file"-value is an URL + // Check if the given "file"-value is a URL $parsedUrl = parse_url($file); // the "file"-value can contain "./file.css" or "/file.css". diff --git a/src/Loader/WebpackManifestLoader.php b/src/Loader/WebpackManifestLoader.php index e1def86..184315e 100644 --- a/src/Loader/WebpackManifestLoader.php +++ b/src/Loader/WebpackManifestLoader.php @@ -4,12 +4,19 @@ namespace Inpsyde\Assets\Loader; +use Inpsyde\Assets\Asset; +use Inpsyde\Assets\AssetFactory; + /** * Implementation of Webpack manifest.json parsing into Assets. * * @link https://www.npmjs.com/package/webpack-manifest-plugin * * @package Inpsyde\Assets\Loader + * + * @phpstan-import-type AssetConfig from AssetFactory + * @phpstan-import-type AssetExtensionConfig from AssetFactory + * @phpstan-type Configuration = AssetConfig&AssetExtensionConfig */ class WebpackManifestLoader extends AbstractWebpackLoader { @@ -17,22 +24,126 @@ protected function parseData(array $data, string $resource): array { $directory = trailingslashit(dirname($resource)); $assets = []; - foreach ($data as $handle => $file) { - $handle = $this->sanitizeHandle($handle); - $sanitizedFile = $this->sanitizeFileName($file); - - $fileUrl = (!$this->directoryUrl) - ? $file - : $this->directoryUrl . $sanitizedFile; + foreach ($data as $handle => $fileOrArray) { + $asset = null; - $filePath = $directory . $sanitizedFile; + if (is_array($fileOrArray)) { + $asset = $this->handleAsArray($handle, $fileOrArray, $directory); + } + if (is_string($fileOrArray)) { + $asset = $this->handleUsingFileName($handle, $fileOrArray, $directory); + } - $asset = $this->buildAsset($handle, $fileUrl, $filePath); - if ($asset !== null) { + if ($asset) { $assets[] = $asset; } } return $assets; } + + /** + * @param Configuration $configuration + */ + protected function handleAsArray(string $handle, array $configuration, string $directory): ?Asset + { + $file = $this->extractFilePath($configuration); + $location = $this->buildLocations($configuration); + $version = $this->extractVersion($configuration); + + if (!$file) { + return null; + } + + $handle = $this->sanitizeHandle($handle); + $sanitizedFile = $this->sanitizeFileName($file); + $class = self::resolveClassByExtension($sanitizedFile); + + if (!$class) { + return null; + } + + $factory = new AssetFactory(); + + $configuration['handle'] = $handle; + $configuration['url'] = $this->fileUrl($sanitizedFile); + $configuration['filePath'] = $this->filePath($sanitizedFile, $directory); + $configuration['type'] = $class; + $configuration['location'] = $location; + $configuration['version'] = $version; + + return $factory->create($configuration); + } + + /** + * @param Configuration $configuration + */ + protected function extractFilePath(array $configuration): ?string + { + $filePath = $configuration['filePath'] ?? null; + return is_string($filePath) ? $filePath : null; + } + + /** + * @param Configuration $configuration + */ + protected function extractVersion(array $configuration): ?string + { + $version = $configuration['version'] ?? null; + + if (!is_string($version)) { + $version = ''; + } + + // Autodiscover version is always true by default for the Webpack Manifest Loader + if ($version) { + $this->enableAutodiscoverVersion(); + } + + return $version; + } + + /** + * @param Configuration $configuration + */ + protected function buildLocations(array $configuration): int + { + $locations = $configuration['location'] ?? null; + $locations = is_array($locations) ? $locations : []; + + if (count($locations) === 0) { + return Asset::FRONTEND; + } + + $locations = array_unique($locations); + $collector = array_shift($locations); + $collector = static::resolveLocation("-{$collector}"); + foreach ($locations as $location) { + $collector |= static::resolveLocation("-{$location}"); + } + + return $collector; + } + + protected function handleUsingFileName(string $handle, string $file, string $directory): ?Asset + { + $handle = $this->sanitizeHandle($handle); + $sanitizedFile = $this->sanitizeFileName($file); + $fileUrl = $this->fileUrl($sanitizedFile); + $filePath = $this->filePath($sanitizedFile, $directory); + + return $this->buildAsset($handle, $fileUrl, $filePath); + } + + protected function fileUrl(string $file): string + { + $sanitizedFile = $this->sanitizeFileName($file); + return (!$this->directoryUrl) ? $file : $this->directoryUrl . $sanitizedFile; + } + + protected function filePath(string $file, string $directory): string + { + $sanitizedFile = $this->sanitizeFileName($file); + return untrailingslashit($directory) . '/' . ltrim($sanitizedFile, '/'); + } } diff --git a/tests/phpunit/Unit/Loader/WebpackManifestLoaderTest.php b/tests/phpunit/Unit/Loader/WebpackManifestLoaderTest.php index 591a5d8..b9b4681 100644 --- a/tests/phpunit/Unit/Loader/WebpackManifestLoaderTest.php +++ b/tests/phpunit/Unit/Loader/WebpackManifestLoaderTest.php @@ -83,6 +83,43 @@ public function testLoadFromManifestMultipleAssets(): void static::assertInstanceOf(Style::class, $assets[1]); } + public function testCustomManifestEntryConfiguration(): void + { + $json = json_encode( + [ + 'handle-name' => [ + 'filePath' => 'handle-name-script.js', + 'location' => [ + 'frontend', + 'backend' + ], + 'enqueue' => false, + 'inFooter' => true, + 'version' => '1.0.0', + 'condition' => '', + 'attributes' => [ + 'async' => false, + 'defer' => false, + ] + ], + ] + ); + + $loader = new WebpackManifestLoader(); + $assets = $loader->load($this->mockManifestJson($json)); + $asset = $assets[0]; + + static::assertInstanceOf(Script::class, $asset); + static::assertSame('handle-name-script.js', $asset->url()); + static::assertSame('vfs://tmp/handle-name-script.js', $asset->filePath()); + static::assertSame('1.0.0', $asset->version()); + static::assertFalse($asset->enqueue()); + static::assertTrue($asset->inFooter()); + static::assertFalse($asset->attributes()['async']); + static::assertFalse($asset->attributes()['defer']); + static::assertSame(Asset::BACKEND | Asset::FRONTEND, $asset->location()); + } + /** * @test */ @@ -225,6 +262,13 @@ public function provideManifest(): \Generator '/path/to/script.module.js', ScriptModule::class, ]; + + yield 'with asset configuration as array from the manifest' => [ + '{"my-handle":{"filePath":"script.js","location":["frontend","backend"],"enqueue":false,"inFooter":true,"version":"1.0.0","condition":"","attributes":{"async":false,"defer":false}}}', + 'my-handle', + 'script.js', + Script::class, + ]; } /** From ad57b084577a227c415cfcfe65f09ba52c65ec0f Mon Sep 17 00:00:00 2001 From: Guido Scialfa Date: Tue, 11 Nov 2025 09:26:36 +0100 Subject: [PATCH 2/4] Normalize handle method in `AbstractWebpackLoader` and refactor `WebpackManifestLoader` for improved asset handling - Renamed `sanitizeHandle` to `normalizeHandle` for clarity. - Adjusted `handleAsArray` to streamline configuration processing and include exception handling. - Updated `AssetFactory` usage to a static call for better readability. --- src/Loader/AbstractWebpackLoader.php | 2 +- src/Loader/WebpackManifestLoader.php | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Loader/AbstractWebpackLoader.php b/src/Loader/AbstractWebpackLoader.php index c04a8f7..5cd1ea5 100644 --- a/src/Loader/AbstractWebpackLoader.php +++ b/src/Loader/AbstractWebpackLoader.php @@ -202,7 +202,7 @@ protected function sanitizeFileName(string $file): string * @example /path/to/script.js -> script * @example @vendor/script.module.js -> @vendor/script.module */ - protected function sanitizeHandle(string $file): string + protected function normalizeHandle(string $file): string { $pathInfo = pathinfo($file); diff --git a/src/Loader/WebpackManifestLoader.php b/src/Loader/WebpackManifestLoader.php index 184315e..d456205 100644 --- a/src/Loader/WebpackManifestLoader.php +++ b/src/Loader/WebpackManifestLoader.php @@ -6,6 +6,7 @@ use Inpsyde\Assets\Asset; use Inpsyde\Assets\AssetFactory; +use Inpsyde\Assets\Exception; /** * Implementation of Webpack manifest.json parsing into Assets. @@ -44,18 +45,17 @@ protected function parseData(array $data, string $resource): array /** * @param Configuration $configuration + * @throws Exception\InvalidArgumentException + * @throws Exception\MissingArgumentException */ protected function handleAsArray(string $handle, array $configuration, string $directory): ?Asset { $file = $this->extractFilePath($configuration); - $location = $this->buildLocations($configuration); - $version = $this->extractVersion($configuration); if (!$file) { return null; } - $handle = $this->sanitizeHandle($handle); $sanitizedFile = $this->sanitizeFileName($file); $class = self::resolveClassByExtension($sanitizedFile); @@ -63,7 +63,9 @@ protected function handleAsArray(string $handle, array $configuration, string $d return null; } - $factory = new AssetFactory(); + $location = $this->buildLocations($configuration); + $version = $this->extractVersion($configuration); + $handle = $this->normalizeHandle($handle); $configuration['handle'] = $handle; $configuration['url'] = $this->fileUrl($sanitizedFile); @@ -72,7 +74,7 @@ protected function handleAsArray(string $handle, array $configuration, string $d $configuration['location'] = $location; $configuration['version'] = $version; - return $factory->create($configuration); + return AssetFactory::create($configuration); } /** @@ -127,7 +129,7 @@ protected function buildLocations(array $configuration): int protected function handleUsingFileName(string $handle, string $file, string $directory): ?Asset { - $handle = $this->sanitizeHandle($handle); + $handle = $this->normalizeHandle($handle); $sanitizedFile = $this->sanitizeFileName($file); $fileUrl = $this->fileUrl($sanitizedFile); $filePath = $this->filePath($sanitizedFile, $directory); From 114e686ae7e1acab1de49aa68caa782fb6f1609a Mon Sep 17 00:00:00 2001 From: Guido Scialfa Date: Tue, 11 Nov 2025 09:43:38 +0100 Subject: [PATCH 3/4] Make `resolveClassByExtension` static in `AbstractWebpackLoader` for consistency with static-based asset resolution --- src/Loader/AbstractWebpackLoader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Loader/AbstractWebpackLoader.php b/src/Loader/AbstractWebpackLoader.php index 5cd1ea5..b6e52b4 100644 --- a/src/Loader/AbstractWebpackLoader.php +++ b/src/Loader/AbstractWebpackLoader.php @@ -132,7 +132,7 @@ protected function buildAsset(string $handle, string $fileUrl, string $filePath) return $asset; } - protected function resolveClassByExtension(string $filePath): ?string + protected static function resolveClassByExtension(string $filePath): ?string { $extensionsToClass = [ 'css' => Style::class, From 1226fda950fe774e807a70992b079f0af86a788e Mon Sep 17 00:00:00 2001 From: Guido Scialfa Date: Tue, 11 Nov 2025 10:58:35 +0100 Subject: [PATCH 4/4] Make `resolveClassByExtension` non-static in `AbstractWebpackLoader` for consistency with instance-based asset resolution - Updated `AbstractWebpackLoader` and `WebpackManifestLoader` to use the instance method `resolveClassByExtension`. --- src/Loader/AbstractWebpackLoader.php | 4 ++-- src/Loader/WebpackManifestLoader.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Loader/AbstractWebpackLoader.php b/src/Loader/AbstractWebpackLoader.php index b6e52b4..a6d115f 100644 --- a/src/Loader/AbstractWebpackLoader.php +++ b/src/Loader/AbstractWebpackLoader.php @@ -112,7 +112,7 @@ protected function buildAsset(string $handle, string $fileUrl, string $filePath) $pathInfo = pathinfo($filePath); $filename = $pathInfo['filename'] ?? ''; - $class = self::resolveClassByExtension($filePath); + $class = $this->resolveClassByExtension($filePath); if (!$class) { return null; @@ -132,7 +132,7 @@ protected function buildAsset(string $handle, string $fileUrl, string $filePath) return $asset; } - protected static function resolveClassByExtension(string $filePath): ?string + protected function resolveClassByExtension(string $filePath): ?string { $extensionsToClass = [ 'css' => Style::class, diff --git a/src/Loader/WebpackManifestLoader.php b/src/Loader/WebpackManifestLoader.php index d456205..96ef234 100644 --- a/src/Loader/WebpackManifestLoader.php +++ b/src/Loader/WebpackManifestLoader.php @@ -57,7 +57,7 @@ protected function handleAsArray(string $handle, array $configuration, string $d } $sanitizedFile = $this->sanitizeFileName($file); - $class = self::resolveClassByExtension($sanitizedFile); + $class = $this->resolveClassByExtension($sanitizedFile); if (!$class) { return null;