Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
72 changes: 70 additions & 2 deletions docs/loaders.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<?php
Expand All @@ -43,7 +43,7 @@ $loader = new WebpackManifestLoader();
$assets = $loader->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
<?php
Expand All @@ -63,6 +63,74 @@ This permits us to load those files as script modules too even if we do not have
Moreover, if your file ends with `.module.js` or `.mjs`, the loader will automatically resolve these files as a `Inpsyde/Assets/ScriptModule`.
Additionally, we support `@vendor/` in the handle name when parsing from `manifest.json`. Before, the `@vendor/` was detected as part of the filepath and being stripped away.

#### Alternative Manifest Configuration

An alternative output could be where the value associated with the handle is an object.
In such a case, the configuration given must be compliant to the `AssetFactory` configuration.

You can know which keys and values are supported by checking the `AssetFactory` types, respectively `AssetConfig` and `AssetExtensionConfig`.

Here is an example of such a manifest:

```json
{
"my-handle": {
"filePath": "script.js",
"location": [
"frontend",
"backend"
],
"enqueue": false,
"inFooter": true,
"version": "1.0.0",
"condition": "",
"attributes": {
"async": false,
"defer": false
}
}
}
```

You can build the aforementioned configuration by implementing a custom generator for the `webpack-manifest-plugin`.

For instance, you can have the following `generate` callback passed to the `ManifestPlugin`:

```js
new WebpackManifestPlugin({
// ... other options

generate: (seed, files) => {
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.
Expand Down
1 change: 0 additions & 1 deletion src/AssetFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
* filePath?: string,
* version?: string,
* enqueue?: bool,
* version?: string,
* handler?: class-string<Handler\ScriptHandler>|class-string<Handler\StyleHandler>|class-string<Handler\ScriptModuleHandler>,
* location?: AssetLocation,
* condition?: string,
Expand Down
46 changes: 30 additions & 16 deletions src/Loader/AbstractWebpackLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
Expand All @@ -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".
Expand Down
131 changes: 121 additions & 10 deletions src/Loader/WebpackManifestLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,146 @@

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
{
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, '/');
}
}
44 changes: 44 additions & 0 deletions tests/phpunit/Unit/Loader/WebpackManifestLoaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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,
];
}

/**
Expand Down