Skip to content

Commit a8a8ba8

Browse files
authored
Merge pull request #62 from inpsyde/feature/wp-modules-and-webpack-autoloader
WebPack Loader Script Modules Support
2 parents b5def07 + b1ea517 commit a8a8ba8

File tree

5 files changed

+152
-10
lines changed

5 files changed

+152
-10
lines changed

docs/loaders.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ The [webpack-manifest-plugin](https://www.npmjs.com/package/webpack-manifest-plu
2424
```json
2525
{
2626
"script.js": "/public/path/script.23dafsf2138d.js",
27+
"module.mjs": "/public/path/module.12aafrf5675d.mjs",
28+
"custom.module.js": "/public/path/custom.12aafrf5675d.module.js",
29+
"@vendor/module.js" "/public/path/@vendor/module.js",
2730
"style.css": "style.23dafsf2138d.css",
2831
"sub-folder/style.css": ""
2932
}
@@ -52,6 +55,14 @@ $loader->withDirectoryUrl('www.example.com/path/to/assets/');
5255
$assets = $loader->load('manifest.json');
5356
```
5457

58+
The loader does support scripts modules, files with the `.mjs` extension.
59+
60+
However, due to the limitations imposed on those files in regard to the MIME, we added a support to those files ending with `.module.js`.
61+
This permits us to load those files as script modules too even if we do not have control over the server configuration.
62+
63+
Moreover, if your file ends with `.module.js` or `.mjs`, the loader will automatically resolve these files as a `Inpsyde/Assets/ScriptModule`.
64+
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.
65+
5566
### `EncoreEntrypointsLoader`
5667

5768
[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.

src/Loader/AbstractWebpackLoader.php

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Inpsyde\Assets\Exception\FileNotFoundException;
1111
use Inpsyde\Assets\Exception\InvalidResourceException;
1212
use Inpsyde\Assets\Script;
13+
use Inpsyde\Assets\ScriptModule;
1314
use Inpsyde\Assets\Style;
1415

1516
abstract class AbstractWebpackLoader implements LoaderInterface
@@ -110,20 +111,27 @@ protected function buildAsset(string $handle, string $fileUrl, string $filePath)
110111
$extensionsToClass = [
111112
'css' => Style::class,
112113
'js' => Script::class,
114+
'mjs' => ScriptModule::class,
115+
'module.js' => ScriptModule::class,
113116
];
114117

115118
/** @var array{filename?:string, extension?:string} $pathInfo */
116119
$pathInfo = pathinfo($filePath);
120+
$baseName = $pathInfo['basename'] ?? '';
117121
$filename = $pathInfo['filename'] ?? '';
118122
$extension = $pathInfo['extension'] ?? '';
119123

124+
if (self::isModule($baseName)) {
125+
$extension = 'module.js';
126+
}
127+
120128
if (!in_array($extension, array_keys($extensionsToClass), true)) {
121129
return null;
122130
}
123131

124132
$class = $extensionsToClass[$extension];
125133

126-
/** @var Style|Script $asset */
134+
/** @var Style|Script|ScriptModule $asset */
127135
$asset = new $class($handle, $fileUrl, $this->resolveLocation($filename));
128136
$asset->withFilePath($filePath);
129137
$asset->canEnqueue(true);
@@ -137,6 +145,15 @@ protected function buildAsset(string $handle, string $fileUrl, string $filePath)
137145
return $asset;
138146
}
139147

148+
protected static function isModule(string $fileName): bool
149+
{
150+
// TODO replace it with `str_ends_with` once dropping support for php 7.4
151+
$strEndsWith = static function (string $haystack, string $needle): bool {
152+
return substr_compare($haystack, $needle, -strlen($needle)) === 0;
153+
};
154+
return $strEndsWith($fileName, '.module.js') || $strEndsWith($fileName, '.mjs');
155+
}
156+
140157
/**
141158
* The "file"-value can contain:
142159
* - URL
@@ -159,6 +176,34 @@ protected function sanitizeFileName(string $file): string
159176
return ltrim($parsedUrl['path'] ?? $file, './');
160177
}
161178

179+
/**
180+
* Internal function to sanitize the handle based on the file
181+
* by taking into consideration that @vendor can be present.
182+
*
183+
* @param string $file
184+
*
185+
* @return string
186+
* @example /path/to/@vendor/script.module.js -> @vendor/script.module
187+
*
188+
* @example /path/to/script.js -> script
189+
* @example @vendor/script.module.js -> @vendor/script.module
190+
*/
191+
protected function sanitizeHandle(string $file): string
192+
{
193+
$pathInfo = pathinfo($file);
194+
195+
$dirName = $pathInfo['dirname'] ?? '';
196+
$parts = explode('@', $dirName);
197+
$vendor = $parts[1] ?? null;
198+
199+
$handle = $pathInfo['filename'];
200+
if ($vendor !== null) {
201+
$handle = "@{$vendor}/{$handle}";
202+
}
203+
204+
return $handle;
205+
}
206+
162207
/**
163208
* Internal function to resolve a location for a given file name.
164209
*

src/Loader/WebpackManifestLoader.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@ protected function parseData(array $data, string $resource): array
1818
$directory = trailingslashit(dirname($resource));
1919
$assets = [];
2020
foreach ($data as $handle => $file) {
21-
// It can be possible, that the "handle"-key is a filepath.
22-
$handle = pathinfo($handle, PATHINFO_FILENAME);
23-
21+
$handle = $this->sanitizeHandle($handle);
2422
$sanitizedFile = $this->sanitizeFileName($file);
2523

2624
$fileUrl = (!$this->directoryUrl)

tests/phpunit/Unit/Loader/AbstractWebpackLoaderTest.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Inpsyde\Assets\Exception\InvalidResourceException;
1010
use Inpsyde\Assets\Loader\AbstractWebpackLoader;
1111
use Inpsyde\Assets\Script;
12+
use Inpsyde\Assets\ScriptModule;
1213
use Inpsyde\Assets\Style;
1314
use Inpsyde\Assets\Tests\Unit\AbstractTestCase;
1415
use org\bovigo\vfs\vfsStream;
@@ -104,6 +105,64 @@ public function resolveLocation(string $fileName): int
104105
static::assertSame($expectedLocation, $loader->resolveLocation($inputFile));
105106
}
106107

108+
public function testBuildModulesAssets(): void
109+
{
110+
vfsStream::newFile('assets/my.mjs')
111+
->withContent('console.log("Hello, World!");')
112+
->at($this->root);
113+
114+
$handle = 'asset-handle';
115+
$fileUrl = 'https://example.com/assets/my.mjs';
116+
$filePath = vfsStream::url('tmp/assets/my.module.mjs');
117+
118+
$loader = new class extends AbstractWebpackLoader {
119+
protected function parseData(array $data, string $resource): array
120+
{
121+
return [];
122+
}
123+
124+
public function buildAsset(string $handle, string $fileUrl, string $filePath): ?Asset
125+
{
126+
return parent::buildAsset($handle, $fileUrl, $filePath);
127+
}
128+
};
129+
130+
$asset = $loader->buildAsset($handle, $fileUrl, $filePath);
131+
132+
$this->assertInstanceOf(ScriptModule::class, $asset);
133+
$this->assertSame($handle, $asset->handle());
134+
$this->assertSame($fileUrl, $asset->url());
135+
}
136+
137+
public function testBuildCustomModulesAssets(): void
138+
{
139+
vfsStream::newFile('assets/my.module.js')
140+
->withContent('console.log("Hello, World!");')
141+
->at($this->root);
142+
143+
$handle = 'asset-handle';
144+
$fileUrl = 'https://example.com/assets/my.module.js';
145+
$filePath = vfsStream::url('tmp/assets/my.module.js');
146+
147+
$loader = new class extends AbstractWebpackLoader {
148+
protected function parseData(array $data, string $resource): array
149+
{
150+
return [];
151+
}
152+
153+
public function buildAsset(string $handle, string $fileUrl, string $filePath): ?Asset
154+
{
155+
return parent::buildAsset($handle, $fileUrl, $filePath);
156+
}
157+
};
158+
159+
$asset = $loader->buildAsset($handle, $fileUrl, $filePath);
160+
161+
$this->assertInstanceOf(ScriptModule::class, $asset);
162+
$this->assertSame($handle, $asset->handle());
163+
$this->assertSame($fileUrl, $asset->url());
164+
}
165+
107166
/**
108167
* @return \Generator
109168
*/

tests/phpunit/Unit/Loader/WebpackManifestLoaderTest.php

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Inpsyde\Assets\Asset;
88
use Inpsyde\Assets\Loader\WebpackManifestLoader;
99
use Inpsyde\Assets\Script;
10+
use Inpsyde\Assets\ScriptModule;
1011
use Inpsyde\Assets\Style;
1112
use Inpsyde\Assets\Tests\Unit\AbstractTestCase;
1213
use org\bovigo\vfs\vfsStream;
@@ -37,10 +38,10 @@ public function testLoadFromManifest(
3738
string $expectedHandle,
3839
string $expectedFileName,
3940
string $expectedClass
40-
): void {
41-
41+
): void
42+
{
4243
$expectedDirUrl = 'http://localhost.com/assets/';
43-
$expectedFileUrl = $expectedDirUrl . $expectedFileName;
44+
$expectedFileUrl = $expectedDirUrl . ltrim($expectedFileName, '/');
4445

4546
$loader = new WebpackManifestLoader();
4647
$loader->withDirectoryUrl($expectedDirUrl);
@@ -64,14 +65,20 @@ public function testLoadFromManifestMultipleAssets(): void
6465
[
6566
'script' => 'script.js',
6667
'style' => 'style.css',
68+
'module' => 'module.mjs',
69+
'custom-module' => 'custom.module.js',
70+
'@vendor/module' => 'vendor.module.js',
6771
]
6872
);
6973

7074
$loader = new WebpackManifestLoader();
7175
$assets = $loader->load($this->mockManifestJson($json));
7276

73-
static::assertCount(2, $assets);
77+
static::assertCount(5, $assets);
7478

79+
static::assertInstanceOf(ScriptModule::class, $assets[4]);
80+
static::assertInstanceOf(ScriptModule::class, $assets[3]);
81+
static::assertInstanceOf(ScriptModule::class, $assets[2]);
7582
static::assertInstanceOf(Script::class, $assets[0]);
7683
static::assertInstanceOf(Style::class, $assets[1]);
7784
}
@@ -102,8 +109,8 @@ public function testLoadFromManifestWithAlternativeUrl(
102109
string $json,
103110
string $alternativeUrl,
104111
string $expectedUrl
105-
): void {
106-
112+
): void
113+
{
107114
$loader = new WebpackManifestLoader();
108115
$loader->withDirectoryUrl($alternativeUrl);
109116
$assets = $loader->load($this->mockManifestJson($json));
@@ -197,10 +204,32 @@ public function provideManifest(): \Generator
197204
'sub-folder/script.js',
198205
Script::class,
199206
];
207+
208+
yield 'with @vendor in handle for modules' => [
209+
'{"@vendor/script.module.js": "script.module.js"}',
210+
'@vendor/script.module',
211+
'script.module.js',
212+
ScriptModule::class,
213+
];
214+
215+
yield 'with @vendor in handle for modules and file path' => [
216+
'{"@vendor/script.module.js": "/path/to/script.module.js"}',
217+
'@vendor/script.module',
218+
'/path/to/script.module.js',
219+
ScriptModule::class,
220+
];
221+
222+
yield 'with complex @vendor in handle for modules and file path' => [
223+
'{"path/to/@vendor/script.module.js": "/path/to/script.module.js"}',
224+
'@vendor/script.module',
225+
'/path/to/script.module.js',
226+
ScriptModule::class,
227+
];
200228
}
201229

202230
/**
203231
* @param string $json
232+
*
204233
* @return string
205234
*/
206235
private function mockManifestJson(string $json): string

0 commit comments

Comments
 (0)