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..a6d115f 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 = $this->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". @@ -188,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 e1def86..96ef234 100644 --- a/src/Loader/WebpackManifestLoader.php +++ b/src/Loader/WebpackManifestLoader.php @@ -4,12 +4,20 @@ namespace Inpsyde\Assets\Loader; +use Inpsyde\Assets\Asset; +use Inpsyde\Assets\AssetFactory; +use Inpsyde\Assets\Exception; + /** * 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 +25,127 @@ 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 + * @throws Exception\InvalidArgumentException + * @throws Exception\MissingArgumentException + */ + protected function handleAsArray(string $handle, array $configuration, string $directory): ?Asset + { + $file = $this->extractFilePath($configuration); + + if (!$file) { + return null; + } + + $sanitizedFile = $this->sanitizeFileName($file); + $class = $this->resolveClassByExtension($sanitizedFile); + + if (!$class) { + return null; + } + + $location = $this->buildLocations($configuration); + $version = $this->extractVersion($configuration); + $handle = $this->normalizeHandle($handle); + + $configuration['handle'] = $handle; + $configuration['url'] = $this->fileUrl($sanitizedFile); + $configuration['filePath'] = $this->filePath($sanitizedFile, $directory); + $configuration['type'] = $class; + $configuration['location'] = $location; + $configuration['version'] = $version; + + return AssetFactory::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->normalizeHandle($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, + ]; } /**