Skip to content

Commit 9006591

Browse files
committed
Initial commit
0 parents  commit 9006591

11 files changed

+412
-0
lines changed

composer.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "boo/micro-webpack-encore-plugin",
3+
"description": "This is a Micro plugin for webpack-encore support",
4+
"type": "library",
5+
"version": "0.1",
6+
"minimum-stability": "stable",
7+
"license": "MIT",
8+
"require": {
9+
"php": "^8.0|^8.1|^8.2",
10+
"micro/kernel-app": "^1",
11+
"micro/kernel-boot-dependency": "^1",
12+
"micro/kernel-boot-configuration": "^1",
13+
"micro/plugin-twig": "^1"
14+
},
15+
"autoload": {
16+
"psr-4": {
17+
"Boo\\MicroPlugin\\WebpackEncore\\": "src/"
18+
}
19+
},
20+
"authors": [
21+
{
22+
"name": "Oleksii Bulba",
23+
"email": "[email protected]"
24+
}
25+
]
26+
}

src/Asset/EntrypointLookup.php

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Boo\MicroPlugin\WebpackEncore\Asset;
6+
7+
use Boo\MicroPlugin\WebpackEncore\Exception\EntrypointNotFoundException;
8+
9+
class EntrypointLookup implements EntrypointLookupInterface
10+
{
11+
private mixed $entriesData = null;
12+
13+
private array $returnedFiles = [];
14+
15+
public function __construct(private readonly string $entrypointJsonPath)
16+
{
17+
}
18+
19+
/**
20+
* @inheritDoc
21+
*/
22+
public function getJavaScriptFiles(string $entryName): array
23+
{
24+
return $this->getEntryFiles($entryName, 'js');
25+
}
26+
27+
/**
28+
* @inheritDoc
29+
*/
30+
public function getCssFiles(string $entryName): array
31+
{
32+
return $this->getEntryFiles($entryName, 'css');
33+
}
34+
35+
public function entryExists(string $entryName): bool
36+
{
37+
$entriesData = $this->getEntriesData();
38+
39+
return isset($entriesData['entrypoints'][$entryName]);
40+
}
41+
42+
private function getEntryFiles(string $entryName, string $key): array
43+
{
44+
$this->validateEntryName($entryName);
45+
$entriesData = $this->getEntriesData();
46+
$entryData = $entriesData['entrypoints'][$entryName] ?? [];
47+
48+
if (!isset($entryData[$key])) {
49+
// If we don't find the file type then just send back nothing.
50+
return [];
51+
}
52+
53+
// make sure to not return the same file multiple times
54+
$entryFiles = $entryData[$key];
55+
$newFiles = array_values(array_diff($entryFiles, $this->returnedFiles));
56+
$this->returnedFiles = array_merge($this->returnedFiles, $newFiles);
57+
58+
return $newFiles;
59+
}
60+
61+
private function validateEntryName(string $entryName)
62+
{
63+
$entriesData = $this->getEntriesData();
64+
if (!isset($entriesData['entrypoints'][$entryName])) {
65+
$withoutExtension = substr($entryName, 0, strrpos($entryName, '.'));
66+
67+
if (isset($entriesData['entrypoints'][$withoutExtension])) {
68+
throw new EntrypointNotFoundException(sprintf('Could not find the entry "%s". Try "%s" instead (without the extension).', $entryName, $withoutExtension));
69+
}
70+
71+
throw new EntrypointNotFoundException(sprintf('Could not find the entry "%s" in "%s". Found: %s.', $entryName, $this->entrypointJsonPath, implode(', ', array_keys($entriesData['entrypoints']))));
72+
}
73+
}
74+
75+
private function getEntriesData(): array
76+
{
77+
if (null !== $this->entriesData) {
78+
return $this->entriesData;
79+
}
80+
81+
if (!file_exists($this->entrypointJsonPath)) {
82+
throw new \InvalidArgumentException(sprintf('Could not find the entrypoints file from Webpack: the file "%s" does not exist.', $this->entrypointJsonPath));
83+
}
84+
85+
$this->entriesData = json_decode(file_get_contents($this->entrypointJsonPath), true);
86+
87+
if (null === $this->entriesData) {
88+
throw new \InvalidArgumentException(sprintf('There was a problem JSON decoding the "%s" file', $this->entrypointJsonPath));
89+
}
90+
91+
if (!isset($this->entriesData['entrypoints'])) {
92+
throw new \InvalidArgumentException(sprintf('Could not find an "entrypoints" key in the "%s" file', $this->entrypointJsonPath));
93+
}
94+
95+
return $this->entriesData;
96+
}
97+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Boo\MicroPlugin\WebpackEncore\Asset;
6+
7+
use Boo\MicroPlugin\WebpackEncore\Exception\EntrypointNotFoundException;
8+
9+
interface EntrypointLookupInterface
10+
{
11+
/**
12+
* @throws EntrypointNotFoundException if an entry name is passed that does not exist in entrypoints.json
13+
*/
14+
public function getJavaScriptFiles(string $entryName): array;
15+
16+
/**
17+
* @throws EntrypointNotFoundException if an entry name is passed that does not exist in entrypoints.json
18+
*/
19+
public function getCssFiles(string $entryName): array;
20+
21+
public function entryExists(string $entryName): bool;
22+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Boo\MicroPlugin\WebpackEncore\Exception;
6+
7+
class EntrypointNotFoundException extends \InvalidArgumentException
8+
{
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Boo\MicroPlugin\WebpackEncore\Exception;
6+
7+
class UndefinedBuildException extends \InvalidArgumentException
8+
{
9+
}

src/TagRenderer/TagRenderer.php

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Boo\MicroPlugin\WebpackEncore\TagRenderer;
6+
7+
use Boo\MicroPlugin\WebpackEncore\Asset\EntrypointLookupInterface;
8+
9+
class TagRenderer implements TagRendererInterface
10+
{
11+
private array $renderedFiles = [];
12+
private array $defaultAttributes = [];
13+
private array $defaultScriptAttributes = [];
14+
private array $defaultLinkAttributes = [];
15+
16+
public function __construct(private readonly EntrypointLookupInterface $entrypointLookup)
17+
{
18+
}
19+
20+
public function renderWebpackScriptTags(string $entryName, string $packageName = null, array $extraAttributes = []): string
21+
{
22+
$scriptTags = [];
23+
24+
foreach ($this->entrypointLookup->getJavaScriptFiles($entryName) as $filename) {
25+
$attributes = [];
26+
$attributes['src'] = $filename; // $this->getAssetPath($filename, $packageName);
27+
$attributes = array_merge($attributes, $this->defaultAttributes, $this->defaultScriptAttributes, $extraAttributes);
28+
29+
$scriptTags[] = sprintf(
30+
'<script %s></script>',
31+
$this->convertArrayToAttributes($attributes)
32+
);
33+
34+
$this->renderedFiles['scripts'][] = $attributes['src'];
35+
}
36+
37+
return implode('', $scriptTags);
38+
}
39+
40+
public function renderWebpackLinkTags(string $entryName, string $packageName = null, array $extraAttributes = []): string
41+
{
42+
$scriptTags = [];
43+
44+
foreach ($this->entrypointLookup->getCssFiles($entryName) as $filename) {
45+
$attributes = [];
46+
$attributes['rel'] = 'stylesheet';
47+
$attributes['href'] = $filename; // $this->getAssetPath($filename, $packageName);
48+
$attributes = array_merge($attributes, $this->defaultAttributes, $this->defaultLinkAttributes, $extraAttributes);
49+
50+
$scriptTags[] = sprintf(
51+
'<link %s>',
52+
$this->convertArrayToAttributes($attributes)
53+
);
54+
55+
$this->renderedFiles['styles'][] = $attributes['href'];
56+
}
57+
58+
return implode('', $scriptTags);
59+
}
60+
61+
private function convertArrayToAttributes(array $attributesMap): string
62+
{
63+
// remove attributes set specifically to false
64+
$attributesMap = array_filter($attributesMap, static function ($value) {
65+
return false !== $value;
66+
});
67+
68+
return implode(' ', array_map(
69+
static function ($key, $value) {
70+
// allows for things like defer: true to only render "defer"
71+
if (true === $value || null === $value) {
72+
return $key;
73+
}
74+
75+
return sprintf('%s="%s"', $key, htmlentities($value));
76+
},
77+
array_keys($attributesMap),
78+
$attributesMap
79+
));
80+
}
81+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Boo\MicroPlugin\WebpackEncore\TagRenderer;
6+
7+
interface TagRendererInterface
8+
{
9+
public function renderWebpackScriptTags(string $entryName, string $packageName = null, array $extraAttributes = []): string;
10+
11+
public function renderWebpackLinkTags(string $entryName, string $packageName = null, array $extraAttributes = []): string;
12+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Boo\MicroPlugin\WebpackEncore\Twig\Extension;
6+
7+
use Boo\MicroPlugin\WebpackEncore\Asset\EntrypointLookupInterface;
8+
use Boo\MicroPlugin\WebpackEncore\TagRenderer\TagRendererInterface;
9+
use Twig\Extension\AbstractExtension;
10+
use Twig\TwigFunction;
11+
12+
class EntryFilesTwigExtension extends AbstractExtension
13+
{
14+
private TagRendererInterface $tagRenderer;
15+
private EntrypointLookupInterface $entrypointLookup;
16+
17+
public function __construct(
18+
TagRendererInterface $tagRenderer,
19+
EntrypointLookupInterface $entrypointLookupCollection
20+
) {
21+
$this->tagRenderer = $tagRenderer;
22+
$this->entrypointLookup = $entrypointLookupCollection;
23+
}
24+
25+
public function getFunctions(): array
26+
{
27+
return [
28+
new TwigFunction('encore_entry_js_files', [$this->entrypointLookup, 'getJavaScriptFiles']),
29+
new TwigFunction('encore_entry_css_files', [$this->entrypointLookup, 'getCssFiles']),
30+
new TwigFunction('encore_entry_script_tags', [$this->tagRenderer, 'renderWebpackScriptTags'], ['is_safe' => ['html']]),
31+
new TwigFunction('encore_entry_link_tags', [$this->tagRenderer, 'renderWebpackLinkTags'], ['is_safe' => ['html']]),
32+
new TwigFunction('encore_entry_exists', [$this->entrypointLookup, 'entryExists']),
33+
];
34+
}
35+
}

src/WebpackEncorePlugin.php

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Boo\MicroPlugin\WebpackEncore;
6+
7+
use Micro\Component\DependencyInjection\Container;
8+
use Micro\Framework\Kernel\Configuration\PluginConfigurationInterface;
9+
use Micro\Framework\Kernel\Plugin\ConfigurableInterface;
10+
use Micro\Framework\Kernel\Plugin\DependencyProviderInterface;
11+
use Micro\Plugin\Twig\Plugin\TwigExtensionPluginInterface;
12+
use Boo\MicroPlugin\WebpackEncore\Asset\EntrypointLookup;
13+
use Boo\MicroPlugin\WebpackEncore\Asset\EntrypointLookupInterface;
14+
use Boo\MicroPlugin\WebpackEncore\TagRenderer\TagRenderer;
15+
use Boo\MicroPlugin\WebpackEncore\TagRenderer\TagRendererInterface;
16+
use Boo\MicroPlugin\WebpackEncore\Twig\Extension\EntryFilesTwigExtension;
17+
use Twig\Extension\ExtensionInterface;
18+
19+
class WebpackEncorePlugin implements DependencyProviderInterface, TwigExtensionPluginInterface, ConfigurableInterface
20+
{
21+
private ?TagRendererInterface $tagRenderer = null;
22+
23+
private ?EntrypointLookupInterface $entrypointLookup = null;
24+
25+
private WebpackEncorePluginConfigurationInterface $configuration;
26+
27+
public function provideDependencies(Container $container): void
28+
{
29+
$container->register(TagRendererInterface::class, function () {
30+
return $this->createTagRenderer();
31+
});
32+
$container->register(EntrypointLookupInterface::class, function () {
33+
return $this->createEntrypointLookup();
34+
});
35+
}
36+
37+
public function provideTwigExtensions(): iterable
38+
{
39+
yield $this->createEntryFilesTwigExtension();
40+
}
41+
42+
/**
43+
* {@inheritDoc}
44+
*/
45+
public function setConfiguration(PluginConfigurationInterface $pluginConfiguration): void
46+
{
47+
if ($pluginConfiguration instanceof WebpackEncorePluginConfigurationInterface) {
48+
$this->configuration = $pluginConfiguration;
49+
50+
return;
51+
}
52+
53+
throw new \InvalidArgumentException(sprintf(
54+
'Plugin configuration should implement %s, provided %s',
55+
WebpackEncorePluginConfigurationInterface::class,
56+
\get_class($pluginConfiguration)
57+
));
58+
}
59+
60+
/**
61+
* {@inheritDoc}
62+
*/
63+
public function configuration(): WebpackEncorePluginConfigurationInterface
64+
{
65+
return $this->configuration;
66+
}
67+
68+
private function createEntryFilesTwigExtension(): ExtensionInterface
69+
{
70+
return new EntryFilesTwigExtension($this->createTagRenderer(), $this->createEntrypointLookup());
71+
}
72+
73+
private function createTagRenderer(): TagRendererInterface
74+
{
75+
if (null === $this->tagRenderer) {
76+
$this->tagRenderer = new TagRenderer($this->createEntrypointLookup());
77+
}
78+
79+
return $this->tagRenderer;
80+
}
81+
82+
private function createEntrypointLookup(): EntrypointLookupInterface
83+
{
84+
if (null === $this->entrypointLookup) {
85+
$this->entrypointLookup = new EntrypointLookup($this->configuration->getOutputPath().'/entrypoints.json');
86+
}
87+
88+
return $this->entrypointLookup;
89+
}
90+
}

0 commit comments

Comments
 (0)