Skip to content

Commit 553b2cd

Browse files
committed
Adding new Twig functions to fetch the *source* of files
1 parent df06196 commit 553b2cd

File tree

10 files changed

+245
-1
lines changed

10 files changed

+245
-1
lines changed

composer.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,16 @@
2626
"symfony/config": "^3.4 || ^4.0 || ^5.0",
2727
"symfony/dependency-injection": "^3.4 || ^4.0 || ^5.0",
2828
"symfony/http-kernel": "^3.4 || ^4.0 || ^5.0",
29+
"symfony/polyfill-php80": "^1.15",
2930
"symfony/service-contracts": "^1.0 || ^2.0"
3031
},
3132
"require-dev": {
3233
"symfony/framework-bundle": "^3.4 || ^4.0 || ^5.0",
3334
"symfony/phpunit-bridge": "^4.3.5 || ^5.0",
3435
"symfony/twig-bundle": "^3.4 || ^4.0 || ^5.0",
35-
"symfony/web-link": "^3.4 || ^4.0 || ^5.0"
36+
"symfony/web-link": "^3.4 || ^4.0 || ^5.0",
37+
"twig/extra-bundle": "3.x-dev",
38+
"twig/cssinliner-extra": "3.x-dev"
3639
},
3740
"extra": {
3841
"thanks": {

src/Asset/BuildFileLocator.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
namespace Symfony\WebpackEncoreBundle\Asset;
4+
5+
/**
6+
* Attempts to read the source of built files.
7+
*
8+
* @internal
9+
*/
10+
final class BuildFileLocator
11+
{
12+
private $buildPaths;
13+
14+
private $ensureFileExists = true;
15+
16+
/**
17+
* @param string[] $buildPaths
18+
*/
19+
public function __construct(array $buildPaths)
20+
{
21+
$this->buildPaths = $buildPaths;
22+
}
23+
24+
public function findFile(string $path, string $buildName = '_default'): string
25+
{
26+
if (!isset($this->buildPaths[$buildName])) {
27+
throw new \InvalidArgumentException(sprintf('Invalid build name "%s"', $buildName));
28+
}
29+
30+
// sanity / security check
31+
if (!str_ends_with($path, '.css') && !str_ends_with($path, '.js')) {
32+
throw new \InvalidArgumentException('Can only read files ending in .css and .js');
33+
}
34+
35+
$buildPath = $this->buildPaths[$buildName];
36+
37+
$targetPath = $this->combinePaths($buildPath, $path);
38+
39+
if ($this->ensureFileExists && !file_exists($targetPath)) {
40+
throw new \LogicException(sprintf('Cannot determine how to locate the "%s" file by combining with the output_path "%s"', $path, $buildPath));
41+
}
42+
43+
return $targetPath;
44+
}
45+
46+
/**
47+
* This method tries to combine the build path and asset path to get a final path.
48+
*
49+
* It's really an "attempt" and will work in all normal cases, but not
50+
* in all cases. For example with this config:
51+
*
52+
* output_path: %kernel.project_dir%/public/build
53+
*
54+
* If you pass an asset whose path is "build/file1.js", this would
55+
* remove the duplicated "build" on both and return a final path of:
56+
*
57+
* %kernel.project_dir%/public/build/file1.js
58+
*/
59+
private function combinePaths(string $buildPath, string $path): string
60+
{
61+
$pathParts = explode('/', ltrim($path, '/'));
62+
$buildPathInfo = new \SplFileInfo($buildPath);
63+
64+
while (true) {
65+
// break if there are no directories left
66+
if (count($pathParts) == 1) {
67+
break;
68+
}
69+
70+
// break if the beginning of the path and the "directory name" of the build path
71+
// don't intersect
72+
if ($pathParts[0] !== $buildPathInfo->getFilename()) {
73+
break;
74+
}
75+
76+
// pop the first "directory" off of the path
77+
unset($pathParts[0]);
78+
$pathParts = array_values($pathParts);
79+
}
80+
81+
return $buildPathInfo->getPathname().'/'.implode('/', $pathParts);
82+
}
83+
84+
/**
85+
* @internal
86+
*/
87+
public function disableFileExistsCheck(): void
88+
{
89+
$this->ensureFileExists = false;
90+
}
91+
}

src/DependencyInjection/WebpackEncoreExtension.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@ public function load(array $configs, ContainerBuilder $container)
3535

3636
$factories = [];
3737
$cacheKeys = [];
38+
$buildPaths = [];
3839

3940
if (false !== $config['output_path']) {
4041
$factories['_default'] = $this->entrypointFactory($container, '_default', $config['output_path'], $config['cache'], $config['strict_mode']);
4142
$cacheKeys['_default'] = $config['output_path'].'/'.self::ENTRYPOINTS_FILE_NAME;
43+
$buildPaths['_default'] = $config['output_path'];
4244

4345
$container->getDefinition('webpack_encore.entrypoint_lookup_collection')
4446
->setArgument(1, '_default');
@@ -47,6 +49,7 @@ public function load(array $configs, ContainerBuilder $container)
4749
foreach ($config['builds'] as $name => $path) {
4850
$factories[$name] = $this->entrypointFactory($container, $name, $path, $config['cache'], $config['strict_mode']);
4951
$cacheKeys[rawurlencode($name)] = $path.'/'.self::ENTRYPOINTS_FILE_NAME;
52+
$buildPaths[$name] = $path;
5053
}
5154

5255
$container->getDefinition('webpack_encore.exception_listener')
@@ -61,6 +64,9 @@ public function load(array $configs, ContainerBuilder $container)
6164
$container->setAlias(EntrypointLookupInterface::class, new Alias($this->getEntrypointServiceId('_default')));
6265
}
6366

67+
$container->getDefinition('webpack_encore.build_file_locator')
68+
->replaceArgument(0, $buildPaths);
69+
6470
$defaultAttributes = [];
6571

6672
if (false !== $config['crossorigin']) {

src/Resources/config/services.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<argument type="collection">
2929
<argument key="webpack_encore.entrypoint_lookup_collection" type="service" id="webpack_encore.entrypoint_lookup_collection" />
3030
<argument key="webpack_encore.tag_renderer" type="service" id="webpack_encore.tag_renderer" />
31+
<argument key="webpack_encore.build_file_locator" type="service" id="webpack_encore.build_file_locator" />
3132
</argument>
3233
</service>
3334
</argument>
@@ -60,5 +61,9 @@
6061
<tag name="kernel.event_subscriber" />
6162
<argument type="service" id="webpack_encore.tag_renderer" />
6263
</service>
64+
65+
<service id="webpack_encore.build_file_locator" class="Symfony\WebpackEncoreBundle\Asset\BuildFileLocator">
66+
<argument /> <!-- build paths -->
67+
</service>
6368
</services>
6469
</container>

src/Twig/EntryFilesTwigExtension.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
namespace Symfony\WebpackEncoreBundle\Twig;
1111

1212
use Psr\Container\ContainerInterface;
13+
use Symfony\WebpackEncoreBundle\Asset\BuildFileLocator;
1314
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookup;
1415
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface;
1516
use Symfony\WebpackEncoreBundle\Asset\TagRenderer;
@@ -30,6 +31,8 @@ public function getFunctions()
3031
return [
3132
new TwigFunction('encore_entry_js_files', [$this, 'getWebpackJsFiles']),
3233
new TwigFunction('encore_entry_css_files', [$this, 'getWebpackCssFiles']),
34+
new TwigFunction('encore_entry_js_source', [$this, 'getWebpackJsSource']),
35+
new TwigFunction('encore_entry_css_source', [$this, 'getWebpackCssSource']),
3336
new TwigFunction('encore_entry_script_tags', [$this, 'renderWebpackScriptTags'], ['is_safe' => ['html']]),
3437
new TwigFunction('encore_entry_link_tags', [$this, 'renderWebpackLinkTags'], ['is_safe' => ['html']]),
3538
new TwigFunction('encore_disable_file_tracking', [$this, 'disableReturnedFileTracking']),
@@ -49,6 +52,22 @@ public function getWebpackCssFiles(string $entryName, string $entrypointName = '
4952
->getCssFiles($entryName);
5053
}
5154

55+
public function getWebpackJsSource(string $entryName, string $entrypointName = '_default'): string
56+
{
57+
$files = $this->getEntrypointLookup($entrypointName)
58+
->getJavaScriptFiles($entryName);
59+
60+
return $this->concatenateFileSources($files);
61+
}
62+
63+
public function getWebpackCssSource(string $entryName, string $entrypointName = '_default'): string
64+
{
65+
$files = $this->getEntrypointLookup($entrypointName)
66+
->getCssFiles($entryName);
67+
68+
return $this->concatenateFileSources($files);
69+
}
70+
5271
public function renderWebpackScriptTags(string $entryName, string $packageName = null, string $entrypointName = '_default'): string
5372
{
5473
return $this->getTagRenderer()
@@ -82,6 +101,17 @@ private function changeReturnedFileTracking(bool $isEnabled, string $entrypointN
82101
$lookup->enableReturnedFileTracking($isEnabled);
83102
}
84103

104+
private function concatenateFileSources(array $files): string
105+
{
106+
$locator = $this->getBuildFileLocator();
107+
$source = '';
108+
foreach ($files as $file) {
109+
$source .= file_get_contents($locator->findFile($file));
110+
}
111+
112+
return $source;
113+
}
114+
85115
private function getEntrypointLookup(string $entrypointName): EntrypointLookupInterface
86116
{
87117
return $this->container->get('webpack_encore.entrypoint_lookup_collection')
@@ -92,4 +122,9 @@ private function getTagRenderer(): TagRenderer
92122
{
93123
return $this->container->get('webpack_encore.tag_renderer');
94124
}
125+
126+
private function getBuildFileLocator(): BuildFileLocator
127+
{
128+
return $this->container->get('webpack_encore.build_file_locator');
129+
}
95130
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony WebpackEncoreBundle package.
5+
* (c) Fabien Potencier <[email protected]>
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
namespace Symfony\WebpackEncoreBundle\Tests\Asset;
11+
12+
use PHPUnit\Framework\TestCase;
13+
use Symfony\WebpackEncoreBundle\Asset\BuildFileLocator;
14+
15+
class BuildFileLocatorTest extends TestCase
16+
{
17+
/**
18+
* @dataProvider getFindFileTests
19+
*/
20+
public function testFindFile(string $buildPath, string $assetPath, string $expectedPath)
21+
{
22+
$fileLocator = new BuildFileLocator(['_default' => $buildPath]);
23+
$fileLocator->disableFileExistsCheck();
24+
25+
$this->assertSame($expectedPath, $fileLocator->findFile($assetPath));
26+
}
27+
28+
public function getFindFileTests()
29+
{
30+
yield 'no_overlap' => [
31+
'/app/public',
32+
'foo.js',
33+
'/app/public/foo.js'
34+
];
35+
36+
yield 'simple_overlap' => [
37+
'/app/public/build',
38+
'build/foo.js',
39+
'/app/public/build/foo.js'
40+
];
41+
42+
yield 'simple_overlap_with_slash' => [
43+
'/app/public/build',
44+
'/build/foo.js',
45+
'/app/public/build/foo.js'
46+
];
47+
48+
yield 'simple_overlap_with_build_path_slash' => [
49+
'/app/public/build/',
50+
'build/foo.js',
51+
'/app/public/build/foo.js'
52+
];
53+
54+
yield 'partial_overlap' => [
55+
'/app/public/build',
56+
'build/subdirectory/foo.js',
57+
'/app/public/build/subdirectory/foo.js'
58+
];
59+
60+
yield 'overlap_in_wrong_spot' => [
61+
'/app/public/build',
62+
'subdirectory/build/foo.js',
63+
'/app/public/build/subdirectory/build/foo.js'
64+
];
65+
66+
yield 'windows_paths' => [
67+
'C:\\\\app\\public\\build',
68+
'build/foo.js',
69+
'C:\\\\app\\public\\build/build/foo.js'
70+
];
71+
}
72+
73+
// test only CSS & JS allowed
74+
}

tests/IntegrationTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use Symfony\WebpackEncoreBundle\Asset\TagRenderer;
3030
use Symfony\WebpackEncoreBundle\CacheWarmer\EntrypointCacheWarmer;
3131
use Symfony\WebpackEncoreBundle\WebpackEncoreBundle;
32+
use Twig\Extra\TwigExtraBundle\TwigExtraBundle;
3233

3334
class IntegrationTest extends TestCase
3435
{
@@ -85,6 +86,24 @@ public function testTwigIntegrationWithTrackingDisabled()
8586
);
8687
}
8788

89+
public function testTwigIntegrationWithSourceFiles()
90+
{
91+
$kernel = new WebpackEncoreIntegrationTestKernel(true);
92+
$kernel->boot();
93+
$container = $kernel->getContainer();
94+
95+
$html = $container->get('twig')->render('@integration_test/inline_css_template.twig');
96+
97+
$this->assertStringContainsString(
98+
'<h1 style="font-size: 20px;">Hello</h1>',
99+
$html
100+
);
101+
$this->assertStringContainsString(
102+
'<h2 style="color: purple;">World!</h2>',
103+
$html
104+
);
105+
}
106+
88107
public function testEntriesAreNotRepeatedWhenAlreadyOutputIntegration()
89108
{
90109
$kernel = new WebpackEncoreIntegrationTestKernel(true);
@@ -234,6 +253,7 @@ public function registerBundles()
234253
new FrameworkBundle(),
235254
new TwigBundle(),
236255
new WebpackEncoreBundle(),
256+
new TwigExtraBundle(),
237257
];
238258
}
239259

tests/fixtures/build/styles.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
h1 {
2+
font-size: 20px;
3+
}

tests/fixtures/build/styles2.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
h2 {
2+
color: purple;
3+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{% apply inline_css(encore_entry_css_source('my_entry')) %}
2+
<h1>Hello</h1>
3+
<h2>World!</h2>
4+
{% endapply %}

0 commit comments

Comments
 (0)